1
0

Merge branch 'develop' into l10n_develop

This commit is contained in:
syuilo
2018-08-30 03:56:51 +09:00
committed by GitHub
171 changed files with 2193 additions and 14550 deletions
+3
View File
@@ -138,3 +138,6 @@ drive:
# Clustering # Clustering
# clusterLimit: 1 # clusterLimit: 1
# Summaly proxy
# summalyProxy: "http://example.com"
+10
View File
@@ -5,6 +5,16 @@ ChangeLog
This document describes breaking changes only. This document describes breaking changes only.
8.0.0
-----
### Migration
起動する前に、`node cli/migration/8.0.0`してください。
Please run `node cli/migration/8.0.0` before launch.
7.0.0 7.0.0
----- -----
-40
View File
@@ -1,40 +0,0 @@
const { default: User, deleteUser } = require('../built/models/user');
const { default: zip } = require('@prezzemolo/zip')
const migrate = async (user) => {
try {
await deleteUser(user._id);
return true;
} catch (e) {
return false;
}
}
async function main() {
const count = await User.count({
uri: /#/
});
const dop = 1
const idop = ((count - (count % dop)) / dop) + 1
return zip(
1,
async (time) => {
console.log(`${time} / ${idop}`)
const doc = await User.find({
uri: /#/
}, {
limit: dop, skip: time * dop
})
return Promise.all(doc.map(migrate))
},
idop
).then(a => {
const rv = []
a.forEach(e => rv.push(...e))
return rv
})
}
main().then(console.dir).catch(console.error)
+144
View File
@@ -0,0 +1,144 @@
const { default: Stats } = require('../../built/models/stats');
const { default: User } = require('../../built/models/user');
const { default: Note } = require('../../built/models/note');
const { default: DriveFile } = require('../../built/models/drive-file');
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
const h = now.getHours();
const date = new Date(y, m, d, h);
async function main() {
await Stats.update({}, {
$set: {
span: 'day'
}
}, {
multi: true
});
const localUsersCount = await User.count({
host: null
});
const remoteUsersCount = await User.count({
host: { $ne: null }
});
const localNotesCount = await Note.count({
'_user.host': null
});
const remoteNotesCount = await Note.count({
'_user.host': { $ne: null }
});
const localDriveFilesCount = await DriveFile.count({
'metadata._user.host': null
});
const remoteDriveFilesCount = await DriveFile.count({
'metadata._user.host': { $ne: null }
});
const localDriveFilesSize = await DriveFile
.aggregate([{
$match: {
'metadata._user.host': null,
'metadata.deletedAt': { $exists: false }
}
}, {
$project: {
length: true
}
}, {
$group: {
_id: null,
usage: { $sum: '$length' }
}
}])
.then(aggregates => {
if (aggregates.length > 0) {
return aggregates[0].usage;
}
return 0;
});
const remoteDriveFilesSize = await DriveFile
.aggregate([{
$match: {
'metadata._user.host': { $ne: null },
'metadata.deletedAt': { $exists: false }
}
}, {
$project: {
length: true
}
}, {
$group: {
_id: null,
usage: { $sum: '$length' }
}
}])
.then(aggregates => {
if (aggregates.length > 0) {
return aggregates[0].usage;
}
return 0;
});
await Stats.insert({
date: date,
span: 'hour',
users: {
local: {
total: localUsersCount,
diff: 0
},
remote: {
total: remoteUsersCount,
diff: 0
}
},
notes: {
local: {
total: localNotesCount,
diff: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
},
remote: {
total: remoteNotesCount,
diff: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
}
},
drive: {
local: {
totalCount: localDriveFilesCount,
totalSize: localDriveFilesSize,
diffCount: 0,
diffSize: 0
},
remote: {
totalCount: remoteDriveFilesCount,
totalSize: remoteDriveFilesSize,
diffCount: 0,
diffSize: 0
}
}
});
console.log('done');
}
main();
+10 -1
View File
@@ -59,7 +59,16 @@ gulp.task('build:copy:views', () =>
gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views')) gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views'))
); );
gulp.task('build:copy', ['build:copy:views'], () => // 互換性のため
gulp.task('build:copy:lang', () =>
gulp.src(['./built/client/assets/*.*-*.js'])
.pipe(rename(path => {
path.basename = path.basename.replace(/\-(.*)$/, '');
}))
.pipe(gulp.dest('./built/client/assets/'))
);
gulp.task('build:copy', ['build:copy:views', 'build:copy:lang'], () =>
gulp.src([ gulp.src([
'./build/Release/crypto_key.node', './build/Release/crypto_key.node',
'./src/const.json', './src/const.json',
-1219
View File
File diff suppressed because it is too large Load Diff
-1219
View File
File diff suppressed because it is too large Load Diff
-1219
View File
File diff suppressed because it is too large Load Diff
-1219
View File
File diff suppressed because it is too large Load Diff
-1219
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -11,13 +11,13 @@ const loadLang = lang => yaml.safeLoad(
const native = loadLang('ja-JP'); const native = loadLang('ja-JP');
const langs = { const langs = {
'de': loadLang('de'), 'de-DE': loadLang('de-DE'),
'en': loadLang('en'), 'en-US': loadLang('en-US'),
'fr': loadLang('fr'), 'fr-FR': loadLang('fr-FR'),
'ja': native, 'ja-JP': native,
'ja-KS': loadLang('ja-KS'), 'ja-KS': loadLang('ja-KS'),
'pl': loadLang('pl'), 'pl-PL': loadLang('pl-PL'),
'es': loadLang('es') 'es-ES': loadLang('es-ES')
}; };
Object.values(langs).forEach(locale => { Object.values(langs).forEach(locale => {
-1219
View File
File diff suppressed because it is too large Load Diff
+33 -17
View File
@@ -456,6 +456,7 @@ desktop:
uploading-avatar: "新しいアバターをアップロードしています" uploading-avatar: "新しいアバターをアップロードしています"
avatar-updated: "アバターを更新しました" avatar-updated: "アバターを更新しました"
choose-avatar: "アバターにする画像を選択" choose-avatar: "アバターにする画像を選択"
invalid-filetype: "この形式のファイルはサポートされていません"
desktop/views/components/activity.chart.vue: desktop/views/components/activity.chart.vue:
total: "Black ... Total" total: "Black ... Total"
@@ -473,6 +474,25 @@ desktop/views/components/calendar.vue:
next: "次の月" next: "次の月"
go: "クリックして時間遡行" go: "クリックして時間遡行"
desktop/views/components/charts.vue:
title: "チャート"
per-day: "1日ごと"
per-hour: "1時間ごと"
notes: "投稿"
users: "ユーザー"
drive: "ドライブ"
charts:
notes: "投稿の増減 (統合)"
local-notes: "投稿の増減 (ローカル)"
remote-notes: "投稿の増減 (リモート)"
notes-total: "投稿の累計"
users: "ユーザーの増減"
users-total: "ユーザーの累計"
drive: "ドライブ使用量の増減"
drive-total: "ドライブ使用量の累計"
drive-files: "ドライブのファイル数の増減"
drive-files-total: "ドライブのファイル数の累計"
desktop/views/components/choose-file-from-drive-window.vue: desktop/views/components/choose-file-from-drive-window.vue:
choose-file: "ファイル選択中" choose-file: "ファイル選択中"
upload: "PCからドライブにファイルをアップロード" upload: "PCからドライブにファイルをアップロード"
@@ -713,6 +733,7 @@ desktop/views/components/settings.vue:
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用" gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する" post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
show-clock-on-header: "右上に時計を表示する"
show-reply-target: "リプライ先を表示する" show-reply-target: "リプライ先を表示する"
show-my-renotes: "自分の行ったRenoteをタイムラインに表示する" show-my-renotes: "自分の行ったRenoteをタイムラインに表示する"
show-renoted-my-notes: "自分の投稿のRenoteをタイムラインに表示する" show-renoted-my-notes: "自分の投稿のRenoteをタイムラインに表示する"
@@ -857,6 +878,7 @@ desktop/views/components/ui.header.account.vue:
lists: "リスト" lists: "リスト"
follow-requests: "フォロー申請" follow-requests: "フォロー申請"
customize: "ホームのカスタマイズ" customize: "ホームのカスタマイズ"
admin: "管理"
settings: "設定" settings: "設定"
signout: "サインアウト" signout: "サインアウト"
dark: "闇に飲まれる" dark: "闇に飲まれる"
@@ -914,8 +936,8 @@ desktop/views/pages/admin/admin.dashboard.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
all-users: "全てのユーザー" all-users: "全てのユーザー"
original-users: "このインスタンスのユーザー" original-users: "このインスタンスのユーザー"
all-notes: "全てのノート" all-notes: "全ての投稿"
original-notes: "このインスタンスのノート" original-notes: "このインスタンスの投稿"
invite: "招待" invite: "招待"
desktop/views/pages/admin/admin.suspend-user.vue: desktop/views/pages/admin/admin.suspend-user.vue:
@@ -938,21 +960,6 @@ desktop/views/pages/admin/admin.unverify-user.vue:
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
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/admin/admin.drive-chart.vue:
title: "ドライブ"
local: "ローカル"
remote: "リモート"
desktop/views/pages/deck/deck.tl-column.vue: desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "メディア投稿のみ" is-media-only: "メディア投稿のみ"
is-media-view: "メディアビュー" is-media-view: "メディアビュー"
@@ -963,6 +970,12 @@ desktop/views/pages/deck/deck.note.vue:
private: "この投稿は非公開です" private: "この投稿は非公開です"
deleted: "この投稿は削除されました" deleted: "この投稿は削除されました"
desktop/views/pages/stats/stats.vue:
all-users: "全てのユーザー"
original-users: "このインスタンスのユーザー"
all-notes: "全ての投稿"
original-notes: "このインスタンスの投稿"
desktop/views/pages/welcome.vue: desktop/views/pages/welcome.vue:
about: "詳しく..." about: "詳しく..."
gotit: "わかった" gotit: "わかった"
@@ -1214,6 +1227,7 @@ mobile/views/components/ui.nav.vue:
game: "ゲーム" game: "ゲーム"
darkmode: "ダークモード" darkmode: "ダークモード"
settings: "設定" settings: "設定"
admin: "管理"
about: "Misskeyについて" about: "Misskeyについて"
mobile/views/components/user-timeline.vue: mobile/views/components/user-timeline.vue:
@@ -1355,6 +1369,8 @@ mobile/views/pages/settings.vue:
update-available-desc: "ページを再度読み込みすると更新が適用されます。" update-available-desc: "ページを再度読み込みすると更新が適用されます。"
settings: "設定" settings: "設定"
signout: "サインアウト" signout: "サインアウト"
sound: "サウンド"
enableSounds: "サウンドを有効にする"
mobile/views/pages/user.vue: mobile/views/pages/user.vue:
follows-you: "フォローされています" follows-you: "フォローされています"
-1219
View File
File diff suppressed because it is too large Load Diff
-1219
View File
File diff suppressed because it is too large Load Diff
-1219
View File
File diff suppressed because it is too large Load Diff
-1219
View File
File diff suppressed because it is too large Load Diff
-1219
View File
File diff suppressed because it is too large Load Diff
+15 -12
View File
@@ -1,8 +1,8 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "7.3.0", "version": "8.15.0",
"clientVersion": "1.0.8741", "clientVersion": "1.0.9031",
"codename": "nighthike", "codename": "nighthike",
"main": "./built/index.js", "main": "./built/index.js",
"private": true, "private": true,
@@ -32,7 +32,7 @@
"@types/debug": "0.0.30", "@types/debug": "0.0.30",
"@types/deep-equal": "1.0.1", "@types/deep-equal": "1.0.1",
"@types/double-ended-queue": "2.1.0", "@types/double-ended-queue": "2.1.0",
"@types/elasticsearch": "5.0.25", "@types/elasticsearch": "5.0.26",
"@types/file-type": "5.2.1", "@types/file-type": "5.2.1",
"@types/gulp": "3.8.36", "@types/gulp": "3.8.36",
"@types/gulp-htmlmin": "1.3.32", "@types/gulp-htmlmin": "1.3.32",
@@ -60,7 +60,7 @@
"@types/mocha": "5.2.3", "@types/mocha": "5.2.3",
"@types/mongodb": "3.1.4", "@types/mongodb": "3.1.4",
"@types/ms": "0.7.30", "@types/ms": "0.7.30",
"@types/node": "10.7.1", "@types/node": "10.9.3",
"@types/portscanner": "2.1.0", "@types/portscanner": "2.1.0",
"@types/pug": "2.0.4", "@types/pug": "2.0.4",
"@types/qrcode": "1.2.0", "@types/qrcode": "1.2.0",
@@ -70,14 +70,14 @@
"@types/request-promise-native": "1.0.15", "@types/request-promise-native": "1.0.15",
"@types/rimraf": "2.0.2", "@types/rimraf": "2.0.2",
"@types/seedrandom": "2.4.27", "@types/seedrandom": "2.4.27",
"@types/sharp": "0.17.9", "@types/sharp": "0.17.10",
"@types/showdown": "1.7.5", "@types/showdown": "1.7.5",
"@types/single-line-log": "1.1.0", "@types/single-line-log": "1.1.0",
"@types/speakeasy": "2.0.2", "@types/speakeasy": "2.0.2",
"@types/systeminformation": "3.23.0", "@types/systeminformation": "3.23.0",
"@types/tmp": "0.0.33", "@types/tmp": "0.0.33",
"@types/uuid": "3.4.3", "@types/uuid": "3.4.3",
"@types/webpack": "4.4.10", "@types/webpack": "4.4.11",
"@types/webpack-stream": "3.2.10", "@types/webpack-stream": "3.2.10",
"@types/websocket": "0.0.39", "@types/websocket": "0.0.39",
"@types/ws": "6.0.0", "@types/ws": "6.0.0",
@@ -89,6 +89,7 @@
"bootstrap-vue": "2.0.0-rc.11", "bootstrap-vue": "2.0.0-rc.11",
"cafy": "11.3.0", "cafy": "11.3.0",
"chalk": "2.4.1", "chalk": "2.4.1",
"chart.js": "2.7.2",
"commander": "2.17.1", "commander": "2.17.1",
"crc-32": "1.2.0", "crc-32": "1.2.0",
"css-loader": "1.0.0", "css-loader": "1.0.0",
@@ -149,6 +150,7 @@
"loader-utils": "1.1.0", "loader-utils": "1.1.0",
"lodash.assign": "4.2.0", "lodash.assign": "4.2.0",
"mecab-async": "0.1.2", "mecab-async": "0.1.2",
"merge-options": "1.0.1",
"minio": "7.0.0", "minio": "7.0.0",
"mkdirp": "0.5.1", "mkdirp": "0.5.1",
"mocha": "5.2.0", "mocha": "5.2.0",
@@ -156,7 +158,7 @@
"mongodb": "3.1.1", "mongodb": "3.1.1",
"monk": "6.0.6", "monk": "6.0.6",
"ms": "2.1.1", "ms": "2.1.1",
"nan": "2.10.0", "nan": "2.11.0",
"nested-property": "0.0.7", "nested-property": "0.0.7",
"node-sass": "4.9.3", "node-sass": "4.9.3",
"node-sass-json-importer": "3.3.1", "node-sass-json-importer": "3.3.1",
@@ -188,11 +190,11 @@
"single-line-log": "1.1.2", "single-line-log": "1.1.2",
"speakeasy": "2.0.0", "speakeasy": "2.0.0",
"stringz": "1.0.0", "stringz": "1.0.0",
"style-loader": "0.22.1", "style-loader": "0.23.0",
"stylus": "0.54.5", "stylus": "0.54.5",
"stylus-loader": "3.0.2", "stylus-loader": "3.0.2",
"summaly": "2.1.4", "summaly": "2.1.4",
"systeminformation": "3.42.9", "systeminformation": "3.44.2",
"syuilo-password-strength": "0.0.1", "syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"tmp": "0.0.33", "tmp": "0.0.33",
@@ -206,10 +208,11 @@
"uuid": "3.3.2", "uuid": "3.3.2",
"v-animate-css": "0.0.2", "v-animate-css": "0.0.2",
"vue": "2.5.17", "vue": "2.5.17",
"vue-chartjs": "3.4.0",
"vue-cropperjs": "2.2.1", "vue-cropperjs": "2.2.1",
"vue-js-modal": "1.3.17", "vue-js-modal": "1.3.23",
"vue-json-tree-view": "2.1.4", "vue-json-tree-view": "2.1.4",
"vue-loader": "15.4.0", "vue-loader": "15.4.1",
"vue-router": "3.0.1", "vue-router": "3.0.1",
"vue-style-loader": "4.1.2", "vue-style-loader": "4.1.2",
"vue-template-compiler": "2.5.17", "vue-template-compiler": "2.5.17",
@@ -218,7 +221,7 @@
"vuex-persistedstate": "2.5.4", "vuex-persistedstate": "2.5.4",
"web-push": "3.3.2", "web-push": "3.3.2",
"webfinger.js": "2.6.6", "webfinger.js": "2.6.6",
"webpack": "4.17.0", "webpack": "4.17.1",
"webpack-cli": "3.1.0", "webpack-cli": "3.1.0",
"websocket": "1.0.26", "websocket": "1.0.26",
"ws": "6.0.0", "ws": "6.0.0",
+13 -6
View File
@@ -38,15 +38,22 @@
//#endregion //#endregion
//#region Detect the user language //#region Detect the user language
let lang = navigator.language; let lang = null;
if (!LANGS.includes(lang)) lang = lang.split('-')[0]; if (LANGS.includes(navigator.language)) {
lang = navigator.language;
} else {
lang = LANGS.find(x => x.split('-')[0] == navigator.language);
// The default language is English if (lang == null) {
if (!LANGS.includes(lang)) lang = 'en'; // Fallback
lang = 'en-US';
}
}
if (settings) { if (settings && settings.device.lang &&
if (settings.device.lang) lang = settings.device.lang; LANGS.includes(settings.device.lang)) {
lang = settings.device.lang;
} }
//#endregion //#endregion
@@ -26,8 +26,8 @@ export default Vue.extend({
}, },
created() { created() {
(this as any).os.getMeta().then(meta => { (this as any).os.getMeta().then(meta => {
if (meta.repositoryUrl) this.repositoryUrl = meta.repositoryUrl; if (meta.maintainer.repository_url) this.repositoryUrl = meta.maintainer.repository_url;
if (meta.feedbackUrl) this.feedbackUrl = meta.feedbackUrl; if (meta.maintainer.feedback_url) this.feedbackUrl = meta.maintainer.feedback_url;
}); });
} }
}); });
@@ -28,18 +28,99 @@
import Vue from 'vue'; import Vue from 'vue';
import { url as misskeyUrl } from '../../../config'; import { url as misskeyUrl } from '../../../config';
// THIS IS THE WHITELIST FOR THE EMBED PLAYER
const whiteList = [
'afreecatv.com',
'aparat.com',
'applemusic.com',
'amazon.com',
'awa.fm',
'bandcamp.com',
'bbc.co.uk',
'beatport.com',
'bilibili.com',
'boomstream.com',
'breakers.tv',
'cam4.com',
'cavelis.net',
'chaturbate.com',
'cnn.com',
'cybergame.tv',
'dailymotion.com',
'deezer.com',
'djlive.pl',
'e-onkyo.com',
'eventials.com',
'facebook.com',
'fc2.com',
'gameplank.tv',
'goodgame.ru',
'google.com',
'hardtunes.com',
'instagram.com',
'johnnylooch.com',
'kexp.org',
'lahzenegar.com',
'liveedu.tv',
'livetube.cc',
'livestream.com',
'meridix.com',
'mixcloud.com',
'mixer.com',
'mobcrush.com',
'mylive.in.th',
'myspace.com',
'netflix.com',
'newretrowave.com',
'nhk.or.jp',
'nicovideo.jp',
'nico.ms',
'noisetrade.com',
'nood.tv',
'npr.org',
'openrec.tv',
'pandora.com',
'pandora.tv',
'picarto.tv',
'pscp.tv',
'restream.io',
'reverbnation.com',
'sermonaudio.com',
'smashcast.tv',
'songkick.com',
'soundcloud.com',
'spinninrecords.com',
'spotify.com',
'stitcher.com',
'stream.me',
'switchboard.live',
'tunein.com',
'twitcasting.tv',
'twitch.tv',
'twitter.com',
'vaughnlive.tv',
'veoh.com',
'vimeo.com',
'watchpeoplecode.com',
'web.tv',
'youtube.com',
'youtu.be'
];
export default Vue.extend({ export default Vue.extend({
props: { props: {
url: { url: {
type: String, type: String,
require: true require: true
}, },
detail: { detail: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
} }
}, },
data() { data() {
return { return {
fetching: true, fetching: true,
@@ -57,6 +138,7 @@ export default Vue.extend({
misskeyUrl misskeyUrl
}; };
}, },
created() { created() {
const url = new URL(this.url); const url = new URL(this.url);
@@ -81,102 +163,27 @@ export default Vue.extend({
} }
return; return;
} }
fetch('/url?url=' + encodeURIComponent(this.url)).then(res => { fetch('/url?url=' + encodeURIComponent(this.url)).then(res => {
res.json().then(info => { res.json().then(info => {
if (info.url != null) { if (info.url == null) return;
this.title = info.title; this.title = info.title;
this.description = info.description; this.description = info.description;
this.thumbnail = info.thumbnail; this.thumbnail = info.thumbnail;
this.icon = info.icon; this.icon = info.icon;
this.sitename = info.sitename; this.sitename = info.sitename;
this.fetching = false; this.fetching = false;
if ([ // THIS IS THE WHITELIST FOR THE EMBED PLAYER if (whiteList.some(x => x == url.hostname || url.hostname.endsWith(`.${x}`))) {
'afreecatv.com', this.player = info.player;
'aparat.com', }
'applemusic.com', })
'amazon.com', });
'awa.fm', }
'bandcamp.com',
'bbc.co.uk',
'beatport.com',
'bilibili.com',
'boomstream.com',
'breakers.tv',
'cam4.com',
'cavelis.net',
'chaturbate.com',
'cnn.com',
'cybergame.tv',
'dailymotion.com',
'deezer.com',
'djlive.pl',
'e-onkyo.com',
'eventials.com',
'facebook.com',
'fc2.com',
'gameplank.tv',
'goodgame.ru',
'google.com',
'hardtunes.com',
'instagram.com',
'johnnylooch.com',
'kexp.org',
'lahzenegar.com',
'liveedu.tv',
'livetube.cc',
'livestream.com',
'meridix.com',
'mixcloud.com',
'mixer.com',
'mobcrush.com',
'mylive.in.th',
'myspace.com',
'netflix.com',
'newretrowave.com',
'nhk.or.jp',
'nicovideo.jp',
'nico.ms',
'noisetrade.com',
'nood.tv',
'npr.org',
'openrec.tv',
'pandora.com',
'pandora.tv',
'picarto.tv',
'pscp.tv',
'restream.io',
'reverbnation.com',
'sermonaudio.com',
'smashcast.tv',
'songkick.com',
'soundcloud.com',
'spinninrecords.com',
'spotify.com',
'stitcher.com',
'stream.me',
'switchboard.live',
'tunein.com',
'twitcasting.tv',
'twitch.tv',
'twitter.com',
'vaughnlive.tv',
'veoh.com',
'vimeo.com',
'watchpeoplecode.com',
'web.tv',
'youtube.com',
'youtu.be'
].some(x => x == url.hostname || url.hostname.endsWith(`.${x}`)))
this.player = info.player;
} // info.url
}) // json
}); // fetch
} // created
}); });
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.twitter .player
position relative position relative
width 100% width 100%
+5 -3
View File
@@ -1,8 +1,10 @@
import Vue from 'vue'; import Vue from 'vue';
Vue.filter('bytes', (v, digits = 0) => { Vue.filter('bytes', (v, digits = 0) => {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
if (v == 0) return '0Byte'; if (v == 0) return '0';
const isMinus = v < 0;
if (isMinus) v = -v;
const i = Math.floor(Math.log(v) / Math.log(1024)); const i = Math.floor(Math.log(v) / Math.log(1024));
return (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; return (isMinus ? '-' : '') + (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i];
}); });
@@ -2,9 +2,9 @@
<div class="mkw-donation" :data-mobile="platform == 'mobile'"> <div class="mkw-donation" :data-mobile="platform == 'mobile'">
<article> <article>
<h1>%fa:heart%%i18n:@title%</h1> <h1>%fa:heart%%i18n:@title%</h1>
<p> <p v-if="meta">
{{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }} {{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }}
<a href="https://syuilo.com">@syuilo</a> <a :href="meta.maintainer.url">{{ meta.maintainer.name }}</a>
{{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }} {{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }}
</p> </p>
</article> </article>
@@ -15,6 +15,17 @@
import define from '../../../common/define-widget'; import define from '../../../common/define-widget';
export default define({ export default define({
name: 'donation' name: 'donation'
}).extend({
data() {
return {
meta: null
};
},
created() {
(this as any).os.getMeta().then(meta => {
this.meta = meta;
});
}
}); });
</script> </script>
+44 -25
View File
@@ -3,8 +3,21 @@ import { apiUrl } from '../../config';
import CropWindow from '../views/components/crop-window.vue'; import CropWindow from '../views/components/crop-window.vue';
import ProgressDialog from '../views/components/progress-dialog.vue'; import ProgressDialog from '../views/components/progress-dialog.vue';
export default (os: OS) => (cb, file = null) => { export default (os: OS) => {
const fileSelected = file => {
const cropImage = file => new Promise((resolve, reject) => {
const regex = RegExp('\.(jpg|jpeg|png|gif|webp|bmp|tiff)$');
if (!regex.test(file.name) ) {
os.apis.dialog({
title: '%fa:info-circle% %i18n:desktop.invalid-filetype%',
text: null,
actions: [{
text: '%i18n:common.got-it%'
}]
});
reject();
}
const w = os.new(CropWindow, { const w = os.new(CropWindow, {
image: file, image: file,
@@ -19,27 +32,29 @@ export default (os: OS) => (cb, file = null) => {
os.api('drive/folders/find', { os.api('drive/folders/find', {
name: '%i18n:desktop.avatar%' name: '%i18n:desktop.avatar%'
}).then(iconFolder => { }).then(avatarFolder => {
if (iconFolder.length === 0) { if (avatarFolder.length === 0) {
os.api('drive/folders/create', { os.api('drive/folders/create', {
name: '%i18n:desktop.avatar%' name: '%i18n:desktop.avatar%'
}).then(iconFolder => { }).then(iconFolder => {
upload(data, iconFolder); resolve(upload(data, iconFolder));
}); });
} else { } else {
upload(data, iconFolder[0]); resolve(upload(data, avatarFolder[0]));
} }
}); });
}); });
w.$once('skipped', () => { w.$once('skipped', () => {
set(file); resolve(file);
}); });
document.body.appendChild(w.$el); w.$once('cancelled', reject);
};
const upload = (data, folder) => { document.body.appendChild(w.$el);
});
const upload = (data, folder) => new Promise((resolve, reject) => {
const dialog = os.new(ProgressDialog, { const dialog = os.new(ProgressDialog, {
title: '%i18n:desktop.uploading-avatar%' title: '%i18n:desktop.uploading-avatar%'
}); });
@@ -52,18 +67,19 @@ export default (os: OS) => (cb, file = null) => {
xhr.onload = e => { xhr.onload = e => {
const file = JSON.parse((e.target as any).response); const file = JSON.parse((e.target as any).response);
(dialog as any).close(); (dialog as any).close();
set(file); resolve(file);
}; };
xhr.onerror = reject;
xhr.upload.onprogress = e => { xhr.upload.onprogress = e => {
if (e.lengthComputable) (dialog as any).update(e.loaded, e.total); if (e.lengthComputable) (dialog as any).update(e.loaded, e.total);
}; };
xhr.send(data); xhr.send(data);
}; });
const set = file => { const setAvatar = file => {
os.api('i/update', { return os.api('i/update', {
avatarId: file.id avatarId: file.id
}).then(i => { }).then(i => {
os.store.commit('updateIKeyValue', { os.store.commit('updateIKeyValue', {
@@ -83,18 +99,21 @@ export default (os: OS) => (cb, file = null) => {
}] }]
}); });
if (cb) cb(i); return i;
}); });
}; };
if (file) { return (file = null) => {
fileSelected(file); const selectedFile = file
} else { ? Promise.resolve(file)
os.apis.chooseDriveFile({ : os.apis.chooseDriveFile({
multiple: false, multiple: false,
title: '%fa:image% %i18n:desktop.choose-avatar%' title: '%fa:image% %i18n:desktop.choose-avatar%'
}).then(file => { });
fileSelected(file);
}); return selectedFile
} .then(cropImage)
.then(setAvatar)
.catch(err => err && console.warn(err));
};
}; };
@@ -6,6 +6,19 @@ import ProgressDialog from '../views/components/progress-dialog.vue';
export default (os: OS) => { export default (os: OS) => {
const cropImage = file => new Promise((resolve, reject) => { const cropImage = file => new Promise((resolve, reject) => {
const regex = RegExp('\.(jpg|jpeg|png|gif|webp|bmp|tiff)$');
if (!regex.test(file.name) ) {
os.apis.dialog({
title: '%fa:info-circle% %i18n:desktop.invalid-filetype%',
text: null,
actions: [{
text: '%i18n:common.got-it%'
}]
});
reject();
}
const w = os.new(CropWindow, { const w = os.new(CropWindow, {
image: file, image: file,
title: '%i18n:desktop.banner-crop-title%', title: '%i18n:desktop.banner-crop-title%',
+3 -1
View File
@@ -25,6 +25,7 @@ import updateBanner from './api/update-banner';
import MkIndex from './views/pages/index.vue'; import MkIndex from './views/pages/index.vue';
import MkDeck from './views/pages/deck/deck.vue'; import MkDeck from './views/pages/deck/deck.vue';
import MkAdmin from './views/pages/admin/admin.vue'; import MkAdmin from './views/pages/admin/admin.vue';
import MkStats from './views/pages/stats/stats.vue';
import MkUser from './views/pages/user/user.vue'; import MkUser from './views/pages/user/user.vue';
import MkFavorites from './views/pages/favorites.vue'; import MkFavorites from './views/pages/favorites.vue';
import MkSelectDrive from './views/pages/selectdrive.vue'; import MkSelectDrive from './views/pages/selectdrive.vue';
@@ -57,6 +58,7 @@ init(async (launch) => {
{ path: '/', name: 'index', component: MkIndex }, { path: '/', name: 'index', component: MkIndex },
{ path: '/deck', name: 'deck', component: MkDeck }, { path: '/deck', name: 'deck', component: MkDeck },
{ path: '/admin', name: 'admin', component: MkAdmin }, { path: '/admin', name: 'admin', component: MkAdmin },
{ path: '/stats', name: 'stats', component: MkStats },
{ path: '/i/customize-home', component: MkHomeCustomize }, { path: '/i/customize-home', component: MkHomeCustomize },
{ path: '/i/favorites', component: MkFavorites }, { path: '/i/favorites', component: MkFavorites },
{ path: '/i/messaging/:user', component: MkMessagingRoom }, { path: '/i/messaging/:user', component: MkMessagingRoom },
@@ -94,7 +96,7 @@ init(async (launch) => {
/** /**
* Init Notification * Init Notification
*/ */
if ('Notification' in window) { if ('Notification' in window && os.store.getters.isSignedIn) {
// 許可を得ていなかったらリクエスト // 許可を得ていなかったらリクエスト
if ((Notification as any).permission == 'default') { if ((Notification as any).permission == 'default') {
await Notification.requestPermission(); await Notification.requestPermission();
@@ -0,0 +1,42 @@
import Vue from 'vue';
import { Line } from 'vue-chartjs';
import * as mergeOptions from 'merge-options';
export default Vue.extend({
extends: Line,
props: {
data: {
required: true
},
opts: {
required: false
}
},
watch: {
data() {
this.render();
}
},
mounted() {
this.render();
},
methods: {
render() {
this.renderChart(this.data, mergeOptions({
responsive: true,
maintainAspectRatio: false,
scales: {
xAxes: [{
type: 'time',
distribution: 'series'
}]
},
tooltips: {
intersect: false,
mode: 'x',
position: 'nearest'
}
}, this.opts || {}));
}
}
});
@@ -0,0 +1,587 @@
<template>
<div class="gkgckalzgidaygcxnugepioremxvxvpt">
<header>
<b>%i18n:@title%:</b>
<select v-model="chartType">
<optgroup label="%i18n:@users%">
<option value="users">%i18n:@charts.users%</option>
<option value="users-total">%i18n:@charts.users-total%</option>
</optgroup>
<optgroup label="%i18n:@notes%">
<option value="notes">%i18n:@charts.notes%</option>
<option value="local-notes">%i18n:@charts.local-notes%</option>
<option value="remote-notes">%i18n:@charts.remote-notes%</option>
<option value="notes-total">%i18n:@charts.notes-total%</option>
</optgroup>
<optgroup label="%i18n:@drive%">
<option value="drive-files">%i18n:@charts.drive-files%</option>
<option value="drive-files-total">%i18n:@charts.drive-files-total%</option>
<option value="drive">%i18n:@charts.drive%</option>
<option value="drive-total">%i18n:@charts.drive-total%</option>
</optgroup>
</select>
<div>
<span @click="span = 'day'" :class="{ active: span == 'day' }">%i18n:@per-day%</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">%i18n:@per-hour%</span>
</div>
</header>
<div>
<x-chart v-if="chart" :data="data[0]" :opts="data[1]"/>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import XChart from './charts.chart.ts';
const colors = {
local: 'rgb(246, 88, 79)',
remote: 'rgb(65, 221, 222)',
localPlus: 'rgb(52, 178, 118)',
remotePlus: 'rgb(158, 255, 209)',
localMinus: 'rgb(255, 97, 74)',
remoteMinus: 'rgb(255, 149, 134)'
};
const rgba = (color: string): string => {
return color.replace('rgb', 'rgba').replace(')', ', 0.1)');
};
export default Vue.extend({
components: {
XChart
},
data() {
return {
chart: null,
chartType: 'notes',
span: 'hour'
};
},
computed: {
data(): any {
if (this.chart == null) return null;
switch (this.chartType) {
case 'users': return this.usersChart(false);
case 'users-total': return this.usersChart(true);
case 'notes': return this.notesChart('combined');
case 'local-notes': return this.notesChart('local');
case 'remote-notes': return this.notesChart('remote');
case 'notes-total': return this.notesTotalChart();
case 'drive': return this.driveChart();
case 'drive-total': return this.driveTotalChart();
case 'drive-files': return this.driveFilesChart();
case 'drive-files-total': return this.driveFilesTotalChart();
}
},
stats(): any[] {
return (
this.span == 'day' ? this.chart.perDay :
this.span == 'hour' ? this.chart.perHour :
null
);
}
},
created() {
(this as any).api('chart', {
limit: 32
}).then(chart => {
this.chart = chart;
});
},
methods: {
notesChart(type: string): any {
const data = this.stats.slice().reverse().map(x => ({
date: new Date(x.date),
normal: type == 'local' ? x.notes.local.diffs.normal : type == 'remote' ? x.notes.remote.diffs.normal : x.notes.local.diffs.normal + x.notes.remote.diffs.normal,
reply: type == 'local' ? x.notes.local.diffs.reply : type == 'remote' ? x.notes.remote.diffs.reply : x.notes.local.diffs.reply + x.notes.remote.diffs.reply,
renote: type == 'local' ? x.notes.local.diffs.renote : type == 'remote' ? x.notes.remote.diffs.renote : x.notes.local.diffs.renote + x.notes.remote.diffs.renote,
all: type == 'local' ? (x.notes.local.inc + -x.notes.local.dec) : type == 'remote' ? (x.notes.remote.inc + -x.notes.remote.dec) : (x.notes.local.inc + -x.notes.local.dec) + (x.notes.remote.inc + -x.notes.remote.dec)
}));
return [{
datasets: [{
label: 'All',
fill: false,
borderColor: '#555',
borderWidth: 2,
borderDash: [4, 4],
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.all }))
}, {
label: 'Renotes',
fill: true,
backgroundColor: 'rgba(161, 222, 65, 0.1)',
borderColor: '#a1de41',
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.renote }))
}, {
label: 'Replies',
fill: true,
backgroundColor: 'rgba(247, 121, 108, 0.1)',
borderColor: '#f7796c',
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.reply }))
}, {
label: 'Normal',
fill: true,
backgroundColor: 'rgba(65, 221, 222, 0.1)',
borderColor: '#41ddde',
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.normal }))
}]
}, {
scales: {
yAxes: [{
ticks: {
callback: value => {
return Vue.filter('number')(value);
}
}
}]
},
tooltips: {
callbacks: {
label: (tooltipItem, data) => {
const label = data.datasets[tooltipItem.datasetIndex].label || '';
return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
}
}
}
}];
},
notesTotalChart(): any {
const data = this.stats.slice().reverse().map(x => ({
date: new Date(x.date),
localCount: x.notes.local.total,
remoteCount: x.notes.remote.total
}));
return [{
datasets: [{
label: 'Combined',
fill: false,
borderColor: '#555',
borderWidth: 2,
borderDash: [4, 4],
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.remoteCount + x.localCount }))
}, {
label: 'Local',
fill: true,
backgroundColor: rgba(colors.local),
borderColor: colors.local,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.localCount }))
}, {
label: 'Remote',
fill: true,
backgroundColor: rgba(colors.remote),
borderColor: colors.remote,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.remoteCount }))
}]
}, {
scales: {
yAxes: [{
ticks: {
callback: value => {
return Vue.filter('number')(value);
}
}
}]
},
tooltips: {
callbacks: {
label: (tooltipItem, data) => {
const label = data.datasets[tooltipItem.datasetIndex].label || '';
return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
}
}
}
}];
},
usersChart(total: boolean): any {
const data = this.stats.slice().reverse().map(x => ({
date: new Date(x.date),
localCount: total ? x.users.local.total : (x.users.local.inc + -x.users.local.dec),
remoteCount: total ? x.users.remote.total : (x.users.remote.inc + -x.users.remote.dec)
}));
return [{
datasets: [{
label: 'Combined',
fill: false,
borderColor: '#555',
borderWidth: 2,
borderDash: [4, 4],
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.remoteCount + x.localCount }))
}, {
label: 'Local',
fill: true,
backgroundColor: rgba(colors.local),
borderColor: colors.local,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.localCount }))
}, {
label: 'Remote',
fill: true,
backgroundColor: rgba(colors.remote),
borderColor: colors.remote,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.remoteCount }))
}]
}, {
scales: {
yAxes: [{
ticks: {
callback: value => {
return Vue.filter('number')(value);
}
}
}]
},
tooltips: {
callbacks: {
label: (tooltipItem, data) => {
const label = data.datasets[tooltipItem.datasetIndex].label || '';
return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
}
}
}
}];
},
driveChart(): any {
const data = this.stats.slice().reverse().map(x => ({
date: new Date(x.date),
localInc: x.drive.local.incSize,
localDec: -x.drive.local.decSize,
remoteInc: x.drive.remote.incSize,
remoteDec: -x.drive.remote.decSize,
}));
return [{
datasets: [{
label: 'All',
fill: false,
borderColor: '#555',
borderWidth: 2,
borderDash: [4, 4],
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.localInc + x.localDec + x.remoteInc + x.remoteDec }))
}, {
label: 'Local +',
fill: true,
backgroundColor: rgba(colors.localPlus),
borderColor: colors.localPlus,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.localInc }))
}, {
label: 'Local -',
fill: true,
backgroundColor: rgba(colors.localMinus),
borderColor: colors.localMinus,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.localDec }))
}, {
label: 'Remote +',
fill: true,
backgroundColor: rgba(colors.remotePlus),
borderColor: colors.remotePlus,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.remoteInc }))
}, {
label: 'Remote -',
fill: true,
backgroundColor: rgba(colors.remoteMinus),
borderColor: colors.remoteMinus,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.remoteDec }))
}]
}, {
scales: {
yAxes: [{
ticks: {
callback: value => {
return Vue.filter('bytes')(value, 1);
}
}
}]
},
tooltips: {
callbacks: {
label: (tooltipItem, data) => {
const label = data.datasets[tooltipItem.datasetIndex].label || '';
return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`;
}
}
}
}];
},
driveTotalChart(): any {
const data = this.stats.slice().reverse().map(x => ({
date: new Date(x.date),
localSize: x.drive.local.totalSize,
remoteSize: x.drive.remote.totalSize
}));
return [{
datasets: [{
label: 'Combined',
fill: false,
borderColor: '#555',
borderWidth: 2,
borderDash: [4, 4],
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.remoteSize + x.localSize }))
}, {
label: 'Local',
fill: true,
backgroundColor: rgba(colors.local),
borderColor: colors.local,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.localSize }))
}, {
label: 'Remote',
fill: true,
backgroundColor: rgba(colors.remote),
borderColor: colors.remote,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.remoteSize }))
}]
}, {
scales: {
yAxes: [{
ticks: {
callback: value => {
return Vue.filter('bytes')(value, 1);
}
}
}]
},
tooltips: {
callbacks: {
label: (tooltipItem, data) => {
const label = data.datasets[tooltipItem.datasetIndex].label || '';
return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`;
}
}
}
}];
},
driveFilesChart(): any {
const data = this.stats.slice().reverse().map(x => ({
date: new Date(x.date),
localInc: x.drive.local.incCount,
localDec: -x.drive.local.decCount,
remoteInc: x.drive.remote.incCount,
remoteDec: -x.drive.remote.decCount
}));
return [{
datasets: [{
label: 'All',
fill: false,
borderColor: '#555',
borderWidth: 2,
borderDash: [4, 4],
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.localInc + x.localDec + x.remoteInc + x.remoteDec }))
}, {
label: 'Local +',
fill: true,
backgroundColor: rgba(colors.localPlus),
borderColor: colors.localPlus,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.localInc }))
}, {
label: 'Local -',
fill: true,
backgroundColor: rgba(colors.localMinus),
borderColor: colors.localMinus,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.localDec }))
}, {
label: 'Remote +',
fill: true,
backgroundColor: rgba(colors.remotePlus),
borderColor: colors.remotePlus,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.remoteInc }))
}, {
label: 'Remote -',
fill: true,
backgroundColor: rgba(colors.remoteMinus),
borderColor: colors.remoteMinus,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.remoteDec }))
}]
}, {
scales: {
yAxes: [{
ticks: {
callback: value => {
return Vue.filter('number')(value);
}
}
}]
},
tooltips: {
callbacks: {
label: (tooltipItem, data) => {
const label = data.datasets[tooltipItem.datasetIndex].label || '';
return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
}
}
}
}];
},
driveFilesTotalChart(): any {
const data = this.stats.slice().reverse().map(x => ({
date: new Date(x.date),
localCount: x.drive.local.totalCount,
remoteCount: x.drive.remote.totalCount,
}));
return [{
datasets: [{
label: 'Combined',
fill: false,
borderColor: '#555',
borderWidth: 2,
borderDash: [4, 4],
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.localCount + x.remoteCount }))
}, {
label: 'Local',
fill: true,
backgroundColor: rgba(colors.local),
borderColor: colors.local,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.localCount }))
}, {
label: 'Remote',
fill: true,
backgroundColor: rgba(colors.remote),
borderColor: colors.remote,
borderWidth: 2,
pointBackgroundColor: '#fff',
lineTension: 0,
data: data.map(x => ({ t: x.date, y: x.remoteCount }))
}]
}, {
scales: {
yAxes: [{
ticks: {
callback: value => {
return Vue.filter('number')(value);
}
}
}]
},
tooltips: {
callbacks: {
label: (tooltipItem, data) => {
const label = data.datasets[tooltipItem.datasetIndex].label || '';
return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
}
}
}
}];
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
.gkgckalzgidaygcxnugepioremxvxvpt
padding 32px
background #fff
box-shadow 0 2px 8px rgba(#000, 0.1)
*
user-select none
> header
display flex
margin 0 0 1em 0
padding 0 0 8px 0
font-size 1em
color #555
border-bottom solid 1px #eee
> b
margin-right 8px
> *:last-child
margin-left auto
*
&:not(.active)
color $theme-color
cursor pointer
> div
> *
display block
height 320px
</style>
@@ -47,7 +47,7 @@
</div> </div>
<mk-poll v-if="p.poll" :note="p"/> <mk-poll v-if="p.poll" :note="p"/>
<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<div class="map" v-if="p.geo" ref="map"></div> <div class="map" v-if="p.geo" ref="map"></div>
<div class="renote" v-if="p.renote"> <div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote"/> <mk-note-preview :note="p.renote"/>
@@ -32,7 +32,7 @@
<mk-media-list :media-list="p.media"/> <mk-media-list :media-list="p.media"/>
</div> </div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
<div class="map" v-if="p.geo" ref="map"></div> <div class="map" v-if="p.geo" ref="map"></div>
<div class="renote" v-if="p.renote"> <div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote"/> <mk-note-preview :note="p.renote"/>
@@ -49,6 +49,7 @@
</div> </div>
<mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/> <mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/>
<mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/> <mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/>
<mk-switch v-model="$store.state.settings.showClockOnHeader" @change="onChangeShowClockOnHeader" text="%i18n:@show-clock-on-header%"/>
<mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/> <mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/>
<mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/> <mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/>
<mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/> <mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/>
@@ -333,6 +334,12 @@ export default Vue.extend({
value: v value: v
}); });
}, },
onChangeShowClockOnHeader(v) {
this.$store.dispatch('settings/set', {
key: 'showClockOnHeader',
value: v
});
},
onChangeShowReplyTarget(v) { onChangeShowReplyTarget(v) {
this.$store.dispatch('settings/set', { this.$store.dispatch('settings/set', {
key: 'showReplyTarget', key: 'showReplyTarget',
@@ -30,10 +30,8 @@
<li @click="settings"> <li @click="settings">
<p>%fa:cog%<span>%i18n:@settings%</span>%fa:angle-right%</p> <p>%fa:cog%<span>%i18n:@settings%</span>%fa:angle-right%</p>
</li> </li>
</ul> <li v-if="$store.state.i.isAdmin">
<ul> <router-link to="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</router-link>
<li @click="signout">
<p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p>
</li> </li>
</ul> </ul>
<ul> <ul>
@@ -41,6 +39,11 @@
<p><span>%i18n:@dark%</span><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></p> <p><span>%i18n:@dark%</span><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></p>
</li> </li>
</ul> </ul>
<ul>
<li @click="signout">
<p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p>
</li>
</ul>
</div> </div>
</transition> </transition>
</div> </div>
@@ -11,7 +11,7 @@
<li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop"> <li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop">
<router-link to="/deck"> <router-link to="/deck">
%fa:columns% %fa:columns%
<p>%i18n:@deck% <small>(beta)</small></p> <p>%i18n:@deck%</p>
</router-link> </router-link>
</li> </li>
<li class="messaging"> <li class="messaging">
@@ -17,7 +17,7 @@
<x-account v-if="$store.getters.isSignedIn"/> <x-account v-if="$store.getters.isSignedIn"/>
<x-notifications v-if="$store.getters.isSignedIn"/> <x-notifications v-if="$store.getters.isSignedIn"/>
<x-post v-if="$store.getters.isSignedIn"/> <x-post v-if="$store.getters.isSignedIn"/>
<x-clock/> <x-clock v-if="$store.state.settings.showClockOnHeader"/>
</div> </div>
</div> </div>
</div> </div>
@@ -48,7 +48,7 @@ export default Vue.extend({
this.open(); this.open();
}); });
} else { } else {
const query = this.user[0] == '@' ? const query = this.user.startsWith('@') ?
parseAcct(this.user.substr(1)) : parseAcct(this.user.substr(1)) :
{ userId: this.user }; { userId: this.user };
@@ -1,16 +1,20 @@
<template> <template>
<div class="obdskegsannmntldydackcpzezagxqfy card"> <div class="obdskegsannmntldydackcpzezagxqfy mk-admin-card">
<header>%i18n:@dashboard%</header> <header>%i18n:@dashboard%</header>
<div v-if="stats" class="stats"> <div v-if="stats" class="stats">
<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div> <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><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><b>%fa:pencil-alt% {{ 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><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
</div> </div>
<div class="cpu-memory"> <div class="cpu-memory">
<x-cpu-memory :connection="connection"/> <x-cpu-memory :connection="connection"/>
</div> </div>
<div> <div>
<label>
<input type="checkbox" v-model="disableRegistration" @change="updateMeta">
<span>disableRegistration</span>
</label>
<button class="ui" @click="invite">%i18n:@invite%</button> <button class="ui" @click="invite">%i18n:@invite%</button>
<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
</div> </div>
@@ -28,6 +32,7 @@ export default Vue.extend({
data() { data() {
return { return {
stats: null, stats: null,
disableRegistration: false,
inviteCode: null, inviteCode: null,
connection: null, connection: null,
connectionId: null connectionId: null
@@ -37,6 +42,10 @@ export default Vue.extend({
this.connection = (this as any).os.streams.serverStatsStream.getConnection(); this.connection = (this as any).os.streams.serverStatsStream.getConnection();
this.connectionId = (this as any).os.streams.serverStatsStream.use(); this.connectionId = (this as any).os.streams.serverStatsStream.use();
(this as any).os.getMeta().then(meta => {
this.disableRegistration = meta.disableRegistration;
});
(this as any).api('stats').then(stats => { (this as any).api('stats').then(stats => {
this.stats = stats; this.stats = stats;
}); });
@@ -49,6 +58,11 @@ export default Vue.extend({
(this as any).api('admin/invite').then(x => { (this as any).api('admin/invite').then(x => {
this.inviteCode = x.code; this.inviteCode = x.code;
}); });
},
updateMeta() {
(this as any).api('admin/update-meta', {
disableRegistration: this.disableRegistration
});
} }
} }
}); });
@@ -1,51 +0,0 @@
<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: {
chart: {
required: true
},
type: {
type: String,
required: true
}
},
data() {
return {
viewBoxX: 365,
viewBoxY: 70,
points: null
};
},
created() {
const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.drive.local.totalSize : d.drive.remote.totalSize));
if (peak != 0) {
const data = this.chart.slice().reverse().map(x => ({
size: this.type == 'local' ? x.drive.local.totalSize : x.drive.remote.totalSize
}));
this.points = data.map((d, i) => `${i},${(1 - (d.size / peak)) * this.viewBoxY}`).join(' ');
}
}
});
</script>
<style lang="stylus" scoped>
svg
display block
padding 10px
width 100%
</style>
@@ -1,34 +0,0 @@
<template>
<div class="card">
<header>%i18n:@title%</header>
<div class="card">
<header>%i18n:@local%</header>
<x-chart v-if="chart" :chart="chart" type="local"/>
</div>
<div class="card">
<header>%i18n:@remote%</header>
<x-chart v-if="chart" :chart="chart" type="remote"/>
</div>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import XChart from "./admin.drive-chart.chart.vue";
export default Vue.extend({
components: {
XChart
},
props: {
chart: {
required: true
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
</style>
@@ -1,76 +0,0 @@
<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: {
chart: {
required: true
},
type: {
type: String,
required: true
}
},
data() {
return {
viewBoxX: 365,
viewBoxY: 70,
pointsNote: null,
pointsReply: null,
pointsRenote: null,
pointsTotal: null
};
},
created() {
const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.notes.local.diff : d.notes.remote.diff));
if (peak != 0) {
const data = this.chart.slice().reverse().map(x => ({
normal: this.type == 'local' ? x.notes.local.diffs.normal : x.notes.remote.diffs.normal,
reply: this.type == 'local' ? x.notes.local.diffs.reply : x.notes.remote.diffs.reply,
renote: this.type == 'local' ? x.notes.local.diffs.renote : x.notes.remote.diffs.renote,
total: this.type == 'local' ? x.notes.local.diff : x.notes.remote.diff
}));
this.pointsNote = data.map((d, i) => `${i},${(1 - (d.normal / peak)) * this.viewBoxY}`).join(' ');
this.pointsReply = data.map((d, i) => `${i},${(1 - (d.reply / peak)) * this.viewBoxY}`).join(' ');
this.pointsRenote = data.map((d, i) => `${i},${(1 - (d.renote / 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>
@@ -1,34 +0,0 @@
<template>
<div class="card">
<header>%i18n:@title%</header>
<div class="card">
<header>%i18n:@local%</header>
<x-chart v-if="chart" :chart="chart" type="local"/>
</div>
<div class="card">
<header>%i18n:@remote%</header>
<x-chart v-if="chart" :chart="chart" 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
},
props: {
chart: {
required: true
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
</style>
@@ -1,5 +1,5 @@
<template> <template>
<div class="card"> <div class="mk-admin-card">
<header>%i18n:@suspend-user%</header> <header>%i18n:@suspend-user%</header>
<input v-model="username" type="text" class="ui"/> <input v-model="username" type="text" class="ui"/>
<button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button> <button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button>
@@ -1,5 +1,5 @@
<template> <template>
<div class="card"> <div class="mk-admin-card">
<header>%i18n:@unsuspend-user%</header> <header>%i18n:@unsuspend-user%</header>
<input v-model="username" type="text" class="ui"/> <input v-model="username" type="text" class="ui"/>
<button class="ui" @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</button> <button class="ui" @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</button>
@@ -1,5 +1,5 @@
<template> <template>
<div class="card"> <div class="mk-admin-card">
<header>%i18n:@unverify-user%</header> <header>%i18n:@unverify-user%</header>
<input v-model="username" type="text" class="ui"/> <input v-model="username" type="text" class="ui"/>
<button class="ui" @click="unverifyUser" :disabled="unverifying">%i18n:@unverify%</button> <button class="ui" @click="unverifyUser" :disabled="unverifying">%i18n:@unverify%</button>
@@ -1,51 +0,0 @@
<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: {
chart: {
required: true
},
type: {
type: String,
required: true
}
},
data() {
return {
viewBoxX: 365,
viewBoxY: 70,
points: null
};
},
created() {
const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.users.local.diff : d.users.remote.diff));
if (peak != 0) {
const data = this.chart.slice().reverse().map(x => ({
count: this.type == 'local' ? x.users.local.diff : x.users.remote.diff
}));
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>
@@ -1,34 +0,0 @@
<template>
<div class="card">
<header>%i18n:@title%</header>
<div class="card">
<header>%i18n:@local%</header>
<x-chart v-if="chart" :chart="chart" type="local"/>
</div>
<div class="card">
<header>%i18n:@remote%</header>
<x-chart v-if="chart" :chart="chart" 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
},
props: {
chart: {
required: true
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
</style>
@@ -1,5 +1,5 @@
<template> <template>
<div class="card"> <div class="mk-admin-card">
<header>%i18n:@verify-user%</header> <header>%i18n:@verify-user%</header>
<input v-model="username" type="text" class="ui"/> <input v-model="username" type="text" class="ui"/>
<button class="ui" @click="verifyUser" :disabled="verifying">%i18n:@verify%</button> <button class="ui" @click="verifyUser" :disabled="verifying">%i18n:@verify%</button>
@@ -11,9 +11,7 @@
<main> <main>
<div v-show="page == 'dashboard'"> <div v-show="page == 'dashboard'">
<x-dashboard/> <x-dashboard/>
<x-users-chart :chart="chart"/> <x-charts/>
<x-notes-chart :chart="chart"/>
<x-drive-chart :chart="chart"/>
</div> </div>
<div v-if="page == 'users'"> <div v-if="page == 'users'">
<x-suspend-user/> <x-suspend-user/>
@@ -34,9 +32,7 @@ import XSuspendUser from "./admin.suspend-user.vue";
import XUnsuspendUser from "./admin.unsuspend-user.vue"; import XUnsuspendUser from "./admin.unsuspend-user.vue";
import XVerifyUser from "./admin.verify-user.vue"; import XVerifyUser from "./admin.verify-user.vue";
import XUnverifyUser from "./admin.unverify-user.vue"; import XUnverifyUser from "./admin.unverify-user.vue";
import XUsersChart from "./admin.users-chart.vue"; import XCharts from "../../components/charts.vue";
import XNotesChart from "./admin.notes-chart.vue";
import XDriveChart from "./admin.drive-chart.vue";
export default Vue.extend({ export default Vue.extend({
components: { components: {
@@ -45,21 +41,13 @@ export default Vue.extend({
XUnsuspendUser, XUnsuspendUser,
XVerifyUser, XVerifyUser,
XUnverifyUser, XUnverifyUser,
XUsersChart, XCharts
XNotesChart,
XDriveChart
}, },
data() { data() {
return { return {
page: 'dashboard', page: 'dashboard'
chart: null
}; };
}, },
created() {
(this as any).api('admin/chart').then(chart => {
this.chart = chart;
});
},
methods: { methods: {
nav(page: string) { nav(page: string) {
this.page = page; this.page = page;
@@ -115,7 +103,7 @@ export default Vue.extend({
> div > div
max-width 800px max-width 800px
.card .mk-admin-card
padding 32px padding 32px
background #fff background #fff
box-shadow 0 2px 8px rgba(#000, 0.1) box-shadow 0 2px 8px rgba(#000, 0.1)
@@ -32,7 +32,7 @@
<mk-media-list :media-list="p.media"/> <mk-media-list :media-list="p.media"/>
</div> </div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<div class="renote" v-if="p.renote"> <div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote" :mini="true"/> <mk-note-preview :note="p.renote" :mini="true"/>
</div> </div>
+6 -1
View File
@@ -16,7 +16,7 @@ import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
data() { data() {
return { return {
name: (this as any).os.instanceName, name: null,
posted: false, posted: false,
text: new URLSearchParams(location.search).get('text') text: new URLSearchParams(location.search).get('text')
}; };
@@ -25,6 +25,11 @@ export default Vue.extend({
close() { close() {
window.close(); window.close();
} }
},
mounted() {
(this as any).os.getMeta().then(meta => {
this.name = meta.name;
});
} }
}); });
</script> </script>
@@ -0,0 +1,64 @@
<template>
<div class="tcrwdhwpuxrwmcttxjcsehgpagpstqey">
<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:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
<div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
</div>
<div>
<x-charts/>
</div>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import XCharts from "../../components/charts.vue";
export default Vue.extend({
components: {
XCharts
},
data() {
return {
stats: null
};
},
created() {
(this as any).api('stats').then(stats => {
this.stats = stats;
});
},
});
</script>
<style lang="stylus">
@import '~const.styl'
.tcrwdhwpuxrwmcttxjcsehgpagpstqey
width 100%
padding 16px
> .stats
display flex
justify-content center
margin-bottom 16px
padding 32px
background #fff
box-shadow 0 2px 8px rgba(#000, 0.1)
> div
flex 1
text-align center
> *:first-child
display block
color $theme-color
> *:last-child
font-size 70%
> div
max-width 850px
</style>
@@ -40,10 +40,12 @@ export default Vue.extend({
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
root(isDark)
.friends .friends
background #fff background isDark ? #282C37 : #fff
border solid 1px rgba(#000, 0.075) border solid 1px rgba(#000, 0.075)
border-radius 6px border-radius 6px
overflow hidden
> .title > .title
z-index 1 z-index 1
@@ -52,7 +54,8 @@ export default Vue.extend({
line-height 42px line-height 42px
font-size 0.9em font-size 0.9em
font-weight bold font-weight bold
color #888 background isDark ? #313543 : inherit
color isDark ? #e3e5e8 : #888
box-shadow 0 1px rgba(#000, 0.07) box-shadow 0 1px rgba(#000, 0.07)
> i > i
@@ -70,7 +73,7 @@ export default Vue.extend({
> .user > .user
padding 16px padding 16px
border-bottom solid 1px #eee border-bottom solid 1px isDark ? #21242f : #eee
&:last-child &:last-child
border-bottom none border-bottom none
@@ -96,18 +99,24 @@ export default Vue.extend({
margin 0 margin 0
font-size 16px font-size 16px
line-height 24px line-height 24px
color #555 color isDark ? #ccc : #555
> .username > .username
display block display block
margin 0 margin 0
font-size 15px font-size 15px
line-height 16px line-height 16px
color #ccc color isDark ? #555 : #ccc
> .mk-follow-button > .mk-follow-button
position absolute position absolute
top 16px top 16px
right 16px right 16px
.friends[data-darkmode]
root(true)
.friends:not([data-darkmode])
root(false)
</style> </style>
@@ -39,10 +39,12 @@ export default Vue.extend({
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
root(isDark)
.photos .photos
background #fff background isDark ? #282C37 : #fff
border solid 1px rgba(#000, 0.075) border solid 1px rgba(#000, 0.075)
border-radius 6px border-radius 6px
overflow hidden
> .title > .title
z-index 1 z-index 1
@@ -51,7 +53,8 @@ export default Vue.extend({
line-height 42px line-height 42px
font-size 0.9em font-size 0.9em
font-weight bold font-weight bold
color #888 background: isDark ? #313543 : inherit
color isDark ? #e3e5e8 : #888
box-shadow 0 1px rgba(#000, 0.07) box-shadow 0 1px rgba(#000, 0.07)
> i > i
@@ -85,4 +88,10 @@ export default Vue.extend({
> i > i
margin-right 4px margin-right 4px
.photos[data-darkmode]
root(true)
.photos:not([data-darkmode])
root(false)
</style> </style>
@@ -138,7 +138,7 @@ root(isDark)
padding 16px padding 16px
font-size 12px font-size 12px
color #aaa color #aaa
background #fff background isDark ? #21242f : #fff
border solid 1px rgba(#000, 0.075) border solid 1px rgba(#000, 0.075)
border-radius 6px border-radius 6px
+2 -2
View File
@@ -19,8 +19,8 @@ import { version, codename, lang } from './config';
let elementLocale; let elementLocale;
switch (lang) { switch (lang) {
case 'ja': elementLocale = ElementLocaleJa; break; case 'ja-JP': elementLocale = ElementLocaleJa; break;
case 'en': elementLocale = ElementLocaleEn; break; case 'en-US': elementLocale = ElementLocaleEn; break;
default: elementLocale = ElementLocaleEn; break; default: elementLocale = ElementLocaleEn; break;
} }
@@ -45,7 +45,7 @@
</div> </div>
<mk-poll v-if="p.poll" :note="p"/> <mk-poll v-if="p.poll" :note="p"/>
<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<div class="map" v-if="p.geo" ref="map"></div> <div class="map" v-if="p.geo" ref="map"></div>
<div class="renote" v-if="p.renote"> <div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote"/> <mk-note-preview :note="p.renote"/>
@@ -33,7 +33,7 @@
</div> </div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/> <mk-url-preview v-for="url in urls" :url="url" :key="url"/>
<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<div class="map" v-if="p.geo" ref="map"></div> <div class="map" v-if="p.geo" ref="map"></div>
<div class="renote" v-if="p.renote"> <div class="renote" v-if="p.renote">
<mk-note-preview :note="p.renote"/> <mk-note-preview :note="p.renote"/>
@@ -30,6 +30,7 @@
<ul> <ul>
<li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li> <li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li>
<li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li> <li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li>
<li v-if="$store.getters.isSignedIn && $store.state.i.isAdmin"><router-link to="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</router-link></li>
<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li> <li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li>
</ul> </ul>
</div> </div>
@@ -41,6 +41,12 @@
<ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch> <ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
</ui-card> </ui-card>
<ui-card>
<div slot="title">%fa:volume-up% %i18n:@sound%</div>
<ui-switch v-model="enableSounds">%i18n:@enable-sounds%</ui-switch>
</ui-card>
<ui-card> <ui-card>
<div slot="title">%fa:language% %i18n:@lang%</div> <div slot="title">%fa:language% %i18n:@lang%</div>
@@ -142,6 +148,11 @@ export default Vue.extend({
get() { return this.$store.state.device.lang; }, get() { return this.$store.state.device.lang; },
set(value) { this.$store.commit('device/set', { key: 'lang', value }); } set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
}, },
enableSounds: {
get() { return this.$store.state.device.enableSounds; },
set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }
},
}, },
mounted() { mounted() {
+6 -1
View File
@@ -16,7 +16,7 @@ import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
data() { data() {
return { return {
name: (this as any).os.instanceName, name: null,
posted: false, posted: false,
text: new URLSearchParams(location.search).get('text') text: new URLSearchParams(location.search).get('text')
}; };
@@ -25,6 +25,11 @@ export default Vue.extend({
close() { close() {
window.close(); window.close();
} }
},
mounted() {
(this as any).os.getMeta().then(meta => {
this.name = meta.name;
});
} }
}); });
</script> </script>
+1 -1
View File
@@ -11,7 +11,7 @@
<a class="avatar"> <a class="avatar">
<img :src="user.avatarUrl" alt="avatar"/> <img :src="user.avatarUrl" alt="avatar"/>
</a> </a>
<mk-mute-button v-if="$store.state.i.id != user.id" :user="user"/> <mk-mute-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
<mk-follow-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/> <mk-follow-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
</div> </div>
<div class="title"> <div class="title">
+1
View File
@@ -13,6 +13,7 @@ const defaultSettings = {
showMaps: true, showMaps: true,
showPostFormOnTopOfTl: false, showPostFormOnTopOfTl: false,
suggestRecentHashtags: true, suggestRecentHashtags: true,
showClockOnHeader: true,
circleIcons: true, circleIcons: true,
gradientWindowHeader: false, gradientWindowHeader: false,
showReplyTarget: true, showReplyTarget: true,
+1 -1
View File
@@ -53,5 +53,5 @@ export default function load() {
} }
function normalizeUrl(url: string) { function normalizeUrl(url: string) {
return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url; return url.endsWith('/') ? url.substr(0, url.length - 1) : url;
} }
+2
View File
@@ -62,6 +62,8 @@ export type Source = {
*/ */
ghost?: string; ghost?: string;
summalyProxy?: string;
accesslog?: string; accesslog?: string;
twitter?: { twitter?: {
consumer_key: string; consumer_key: string;
+1 -1
View File
@@ -15,7 +15,7 @@ block main
span.path= endpointUrl.path span.path= endpointUrl.path
if endpoint.desc if endpoint.desc
p#desc= endpoint.desc[lang] || endpoint.desc['ja'] p#desc= endpoint.desc[lang] || endpoint.desc['ja-JP']
if endpoint.requireCredential if endpoint.requireCredential
div.ui.info: p div.ui.info: p
+26 -26
View File
@@ -1,90 +1,90 @@
name: "DriveFile" name: "DriveFile"
desc: desc:
ja: "ドライブのファイル。" ja-JP: "ドライブのファイル。"
en: "A file of Drive." en-US: "A file of Drive."
props: props:
id: id:
type: "id" type: "id"
optional: false optional: false
desc: desc:
ja: "ファイルID" ja-JP: "ファイルID"
en: "The ID of this file" en-US: "The ID of this file"
createdAt: createdAt:
type: "date" type: "date"
optional: false optional: false
desc: desc:
ja: "アップロード日時" ja-JP: "アップロード日時"
en: "The upload date of this file" en-US: "The upload date of this file"
userId: userId:
type: "id(User)" type: "id(User)"
optional: false optional: false
desc: desc:
ja: "所有者ID" ja-JP: "所有者ID"
en: "The ID of the owner of this file" en-US: "The ID of the owner of this file"
user: user:
type: "entity(User)" type: "entity(User)"
optional: true optional: true
desc: desc:
ja: "所有者" ja-JP: "所有者"
en: "The owner of this file" en-US: "The owner of this file"
name: name:
type: "string" type: "string"
optional: false optional: false
desc: desc:
ja: "ファイル名" ja-JP: "ファイル名"
en: "The name of this file" en-US: "The name of this file"
md5: md5:
type: "string" type: "string"
optional: false optional: false
desc: desc:
ja: "ファイルのMD5ハッシュ値" ja-JP: "ファイルのMD5ハッシュ値"
en: "The md5 hash value of this file" en-US: "The md5 hash value of this file"
type: type:
type: "string" type: "string"
optional: false optional: false
desc: desc:
ja: "ファイルの種類" ja-JP: "ファイルの種類"
en: "The type of this file" en-US: "The type of this file"
datasize: datasize:
type: "number" type: "number"
optional: false optional: false
desc: desc:
ja: "ファイルサイズ(bytes)" ja-JP: "ファイルサイズ(bytes)"
en: "The size of this file (bytes)" en-US: "The size of this file (bytes)"
url: url:
type: "string" type: "string"
optional: false optional: false
desc: desc:
ja: "ファイルのURL" ja-JP: "ファイルのURL"
en: "The URL of this file" en-US: "The URL of this file"
folderId: folderId:
type: "id(DriveFolder)" type: "id(DriveFolder)"
optional: true optional: true
desc: desc:
ja: "フォルダID" ja-JP: "フォルダID"
en: "The ID of the folder of this file" en-US: "The ID of the folder of this file"
folder: folder:
type: "entity(DriveFolder)" type: "entity(DriveFolder)"
optional: true optional: true
desc: desc:
ja: "フォルダ" ja-JP: "フォルダ"
en: "The folder of this file" en-US: "The folder of this file"
isSensitive: isSensitive:
type: "boolean" type: "boolean"
optional: true optional: true
desc: desc:
ja: "このメディアが「閲覧注意」(NSFW)かどうか" ja-JP: "このメディアが「閲覧注意」(NSFW)かどうか"
en: "Whether this media is NSFW" en-US: "Whether this media is NSFW"
+12 -12
View File
@@ -1,41 +1,41 @@
name: "DriveFolder" name: "DriveFolder"
desc: desc:
ja: "ドライブのフォルダを表します。" ja-JP: "ドライブのフォルダを表します。"
en: "A folder of Drive." en-US: "A folder of Drive."
props: props:
id: id:
type: "id" type: "id"
optional: false optional: false
desc: desc:
ja: "フォルダID" ja-JP: "フォルダID"
en: "The ID of this folder" en-US: "The ID of this folder"
createdAt: createdAt:
type: "date" type: "date"
optional: false optional: false
desc: desc:
ja: "作成日時" ja-JP: "作成日時"
en: "The created date of this folder" en-US: "The created date of this folder"
userId: userId:
type: "id(User)" type: "id(User)"
optional: false optional: false
desc: desc:
ja: "所有者ID" ja-JP: "所有者ID"
en: "The ID of the owner of this folder" en-US: "The ID of the owner of this folder"
parentId: parentId:
type: "entity(DriveFolder)" type: "entity(DriveFolder)"
optional: false optional: false
desc: desc:
ja: "親フォルダのID (ルートなら null)" ja-JP: "親フォルダのID (ルートなら null)"
en: "The ID of parent folder" en-US: "The ID of parent folder"
name: name:
type: "string" type: "string"
optional: false optional: false
desc: desc:
ja: "フォルダ名" ja-JP: "フォルダ名"
en: "The name of this folder" en-US: "The name of this folder"
+49 -49
View File
@@ -1,190 +1,190 @@
name: "Note" name: "Note"
desc: desc:
ja: "投稿。" ja-JP: "投稿。"
en: "A note." en-US: "A note."
props: props:
id: id:
type: "id" type: "id"
optional: false optional: false
desc: desc:
ja: "投稿ID" ja-JP: "投稿ID"
en: "The ID of this note" en-US: "The ID of this note"
createdAt: createdAt:
type: "date" type: "date"
optional: false optional: false
desc: desc:
ja: "投稿日時" ja-JP: "投稿日時"
en: "The posted date of this note" en-US: "The posted date of this note"
viaMobile: viaMobile:
type: "boolean" type: "boolean"
optional: true optional: true
desc: desc:
ja: "モバイル端末から投稿したか否か(自己申告であることに留意)" ja-JP: "モバイル端末から投稿したか否か(自己申告であることに留意)"
en: "Whether this note sent via a mobile device" en-US: "Whether this note sent via a mobile device"
text: text:
type: "string" type: "string"
optional: true optional: true
desc: desc:
ja: "投稿の本文" ja-JP: "投稿の本文"
en: "The text of this note" en-US: "The text of this note"
mediaIds: mediaIds:
type: "id(DriveFile)[]" type: "id(DriveFile)[]"
optional: true optional: true
desc: desc:
ja: "添付されているメディアのID (なければレスポンスでは空配列)" ja-JP: "添付されているメディアのID (なければレスポンスでは空配列)"
en: "The IDs of the attached media (empty array for response if no media is attached)" en-US: "The IDs of the attached media (empty array for response if no media is attached)"
media: media:
type: "entity(DriveFile)[]" type: "entity(DriveFile)[]"
optional: true optional: true
desc: desc:
ja: "添付されているメディア" ja-JP: "添付されているメディア"
en: "The attached media" en-US: "The attached media"
userId: userId:
type: "id(User)" type: "id(User)"
optional: false optional: false
desc: desc:
ja: "投稿者ID" ja-JP: "投稿者ID"
en: "The ID of author of this note" en-US: "The ID of author of this note"
user: user:
type: "entity(User)" type: "entity(User)"
optional: true optional: true
desc: desc:
ja: "投稿者" ja-JP: "投稿者"
en: "The author of this note" en-US: "The author of this note"
myReaction: myReaction:
type: "string" type: "string"
optional: true optional: true
desc: desc:
ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>" ja-JP: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>"
en: "The your <a href='/docs/api/reactions'>reaction</a> of this note" en-US: "The your <a href='/docs/api/reactions'>reaction</a> of this note"
reactionCounts: reactionCounts:
type: "object" type: "object"
optional: false optional: false
desc: desc:
ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト" ja-JP: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト"
replyId: replyId:
type: "id(Note)" type: "id(Note)"
optional: true optional: true
desc: desc:
ja: "返信した投稿のID" ja-JP: "返信した投稿のID"
en: "The ID of the replyed note" en-US: "The ID of the replyed note"
reply: reply:
type: "entity(Note)" type: "entity(Note)"
optional: true optional: true
desc: desc:
ja: "返信した投稿" ja-JP: "返信した投稿"
en: "The replyed note" en-US: "The replyed note"
renoteId: renoteId:
type: "id(Note)" type: "id(Note)"
optional: true optional: true
desc: desc:
ja: "引用した投稿のID" ja-JP: "引用した投稿のID"
en: "The ID of the quoted note" en-US: "The ID of the quoted note"
renote: renote:
type: "entity(Note)" type: "entity(Note)"
optional: true optional: true
desc: desc:
ja: "引用した投稿" ja-JP: "引用した投稿"
en: "The quoted note" en-US: "The quoted note"
poll: poll:
type: "object" type: "object"
optional: true optional: true
desc: desc:
ja: "投票" ja-JP: "投票"
en: "The poll" en-US: "The poll"
props: props:
choices: choices:
type: "object[]" type: "object[]"
optional: false optional: false
desc: desc:
ja: "投票の選択肢" ja-JP: "投票の選択肢"
en: "The choices of this poll" en-US: "The choices of this poll"
props: props:
id: id:
type: "number" type: "number"
optional: false optional: false
desc: desc:
ja: "選択肢ID" ja-JP: "選択肢ID"
en: "The ID of this choice" en-US: "The ID of this choice"
isVoted: isVoted:
type: "boolean" type: "boolean"
optional: true optional: true
desc: desc:
ja: "自分がこの選択肢に投票したかどうか" ja-JP: "自分がこの選択肢に投票したかどうか"
en: "Whether you voted to this choice" en-US: "Whether you voted to this choice"
text: text:
type: "string" type: "string"
optional: false optional: false
desc: desc:
ja: "選択肢本文" ja-JP: "選択肢本文"
en: "The text of this choice" en-US: "The text of this choice"
votes: votes:
type: "number" type: "number"
optional: false optional: false
desc: desc:
ja: "この選択肢に投票された数" ja-JP: "この選択肢に投票された数"
en: "The number voted for this choice" en-US: "The number voted for this choice"
geo: geo:
type: "object" type: "object"
optional: true optional: true
desc: desc:
ja: "位置情報" ja-JP: "位置情報"
en: "Geo location" en-US: "Geo location"
props: props:
coordinates: coordinates:
type: "number[]" type: "number[]"
optional: false optional: false
desc: desc:
ja: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。" ja-JP: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。"
altitude: altitude:
type: "number" type: "number"
optional: false optional: false
desc: desc:
ja: "高度。メートル単位で表す。" ja-JP: "高度。メートル単位で表す。"
accuracy: accuracy:
type: "number" type: "number"
optional: false optional: false
desc: desc:
ja: "緯度、経度の精度。メートル単位で表す。" ja-JP: "緯度、経度の精度。メートル単位で表す。"
altitudeAccuracy: altitudeAccuracy:
type: "number" type: "number"
optional: false optional: false
desc: desc:
ja: "高度の精度。メートル単位で表す。" ja-JP: "高度の精度。メートル単位で表す。"
heading: heading:
type: "number" type: "number"
optional: false optional: false
desc: desc:
ja: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。" ja-JP: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。"
speed: speed:
type: "number" type: "number"
optional: false optional: false
desc: desc:
ja: "速度。メートル / 秒数で表す。" ja-JP: "速度。メートル / 秒数で表す。"
+48 -48
View File
@@ -1,174 +1,174 @@
name: "User" name: "User"
desc: desc:
ja: "ユーザー。" ja-JP: "ユーザー。"
en: "A user." en-US: "A user."
props: props:
id: id:
type: "id" type: "id"
optional: false optional: false
desc: desc:
ja: "ユーザーID" ja-JP: "ユーザーID"
en: "The ID of this user" en-US: "The ID of this user"
createdAt: createdAt:
type: "date" type: "date"
optional: false optional: false
desc: desc:
ja: "アカウント作成日時" ja-JP: "アカウント作成日時"
en: "The registered date of this user" en-US: "The registered date of this user"
username: username:
type: "string" type: "string"
optional: false optional: false
desc: desc:
ja: "ユーザー名" ja-JP: "ユーザー名"
en: "The username of this user" en-US: "The username of this user"
description: description:
type: "string" type: "string"
optional: false optional: false
desc: desc:
ja: "アカウントの説明(自己紹介)" ja-JP: "アカウントの説明(自己紹介)"
en: "The description of this user" en-US: "The description of this user"
avatarId: avatarId:
type: "id(DriveFile)" type: "id(DriveFile)"
optional: true optional: true
desc: desc:
ja: "アバターのID" ja-JP: "アバターのID"
en: "The ID of the avatar of this user" en-US: "The ID of the avatar of this user"
avatarUrl: avatarUrl:
type: "string" type: "string"
optional: false optional: false
desc: desc:
ja: "アバターのURL" ja-JP: "アバターのURL"
en: "The URL of the avatar of this user" en-US: "The URL of the avatar of this user"
bannerId: bannerId:
type: "id(DriveFile)" type: "id(DriveFile)"
optional: true optional: true
desc: desc:
ja: "バナーのID" ja-JP: "バナーのID"
en: "The ID of the banner of this user" en-US: "The ID of the banner of this user"
bannerUrl: bannerUrl:
type: "string" type: "string"
optional: false optional: false
desc: desc:
ja: "バナーのURL" ja-JP: "バナーのURL"
en: "The URL of the banner of this user" en-US: "The URL of the banner of this user"
followersCount: followersCount:
type: "number" type: "number"
optional: false optional: false
desc: desc:
ja: "フォロワーの数" ja-JP: "フォロワーの数"
en: "The number of the followers for this user" en-US: "The number of the followers for this user"
followingCount: followingCount:
type: "number" type: "number"
optional: false optional: false
desc: desc:
ja: "フォローしているユーザーの数" ja-JP: "フォローしているユーザーの数"
en: "The number of the following users for this user" en-US: "The number of the following users for this user"
isFollowing: isFollowing:
type: "boolean" type: "boolean"
optional: true optional: true
desc: desc:
ja: "自分がこのユーザーをフォローしているか" ja-JP: "自分がこのユーザーをフォローしているか"
isFollowed: isFollowed:
type: "boolean" type: "boolean"
optional: true optional: true
desc: desc:
ja: "自分がこのユーザーにフォローされているか" ja-JP: "自分がこのユーザーにフォローされているか"
isMuted: isMuted:
type: "boolean" type: "boolean"
optional: true optional: true
desc: desc:
ja: "自分がこのユーザーをミュートしているか" ja-JP: "自分がこのユーザーをミュートしているか"
en: "Whether you muted this user" en-US: "Whether you muted this user"
notesCount: notesCount:
type: "number" type: "number"
optional: false optional: false
desc: desc:
ja: "投稿の数" ja-JP: "投稿の数"
en: "The number of the notes of this user" en-US: "The number of the notes of this user"
pinnedNote: pinnedNote:
type: "entity(Note)" type: "entity(Note)"
optional: true optional: true
desc: desc:
ja: "ピン留めされた投稿" ja-JP: "ピン留めされた投稿"
en: "The pinned note of this user" en-US: "The pinned note of this user"
pinnedNoteId: pinnedNoteId:
type: "id(Note)" type: "id(Note)"
optional: true optional: true
desc: desc:
ja: "ピン留めされた投稿のID" ja-JP: "ピン留めされた投稿のID"
en: "The ID of the pinned note of this user" en-US: "The ID of the pinned note of this user"
host: host:
type: "string | null" type: "string | null"
optional: false optional: false
desc: desc:
ja: "ホスト (例: example.com:3000)" ja-JP: "ホスト (例: example.com:3000)"
en: "Host (e.g. example.com:3000)" en-US: "Host (e.g. example.com:3000)"
twitter: twitter:
type: "object" type: "object"
optional: true optional: true
desc: desc:
ja: "連携されているTwitterアカウント情報" ja-JP: "連携されているTwitterアカウント情報"
en: "The info of the connected twitter account of this user" en-US: "The info of the connected twitter account of this user"
props: props:
userId: userId:
type: "string" type: "string"
optional: false optional: false
desc: desc:
ja: "ユーザーID" ja-JP: "ユーザーID"
en: "The user ID" en-US: "The user ID"
screenName: screenName:
type: "string" type: "string"
optional: false optional: false
desc: desc:
ja: "ユーザー名" ja-JP: "ユーザー名"
en: "The screen name of this user" en-US: "The screen name of this user"
isBot: isBot:
type: "boolean" type: "boolean"
optional: true optional: true
desc: desc:
ja: "botか否か(自己申告であることに留意)" ja-JP: "botか否か(自己申告であることに留意)"
en: "Whether is bot or not" en-US: "Whether is bot or not"
profile: profile:
type: "object" type: "object"
optional: false optional: false
desc: desc:
ja: "プロフィール" ja-JP: "プロフィール"
en: "The profile of this user" en-US: "The profile of this user"
props: props:
location: location:
type: "string" type: "string"
optional: true optional: true
desc: desc:
ja: "場所" ja-JP: "場所"
en: "The location of this user" en-US: "The location of this user"
birthday: birthday:
type: "string" type: "string"
optional: true optional: true
desc: desc:
ja: "誕生日 (YYYY-MM-DD)" ja-JP: "誕生日 (YYYY-MM-DD)"
en: "The birthday of this user (YYYY-MM-DD)" en-US: "The birthday of this user (YYYY-MM-DD)"
+1 -1
View File
@@ -7,7 +7,7 @@ block meta
block main block main
h1= name h1= name
p#desc= desc[lang] || desc['ja'] p#desc= desc[lang] || desc['ja-JP']
section section
h2= i18n('docs.api.entities.properties') h2= i18n('docs.api.entities.properties')
+1 -1
View File
@@ -31,4 +31,4 @@ mixin propTable(props)
td.name= prop.name td.name= prop.name
td.type td.type
+type(prop) +type(prop)
td.desc!= prop.desc ? prop.desc[lang] || prop.desc['ja'] : null td.desc!= prop.desc ? prop.desc[lang] || prop.desc['ja-JP'] : null
+1 -1
View File
@@ -16,7 +16,7 @@ html(lang= lang)
nav nav
ul ul
each doc in docs each doc in docs
li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja'] li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja-JP']
section section
h2 API h2 API
ul ul
+1 -1
View File
@@ -197,7 +197,7 @@ const elements: Element[] = [
if (thisIsNotARegexp) return null; if (thisIsNotARegexp) return null;
if (regexp == '') return null; if (regexp == '') return null;
if (regexp[0] == ' ' && regexp[regexp.length - 1] == ' ') return null; if (regexp.startsWith(' ') && regexp.endsWith(' ')) return null;
return { return {
html: `<span class="regexp">/${escape(regexp)}/</span>`, html: `<span class="regexp">/${escape(regexp)}/</span>`,
+1 -1
View File
@@ -10,7 +10,7 @@ export type TextElementHashtag = {
export default function(text: string, i: number) { export default function(text: string, i: number) {
if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null; if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null;
const isHead = text[0] == '#'; const isHead = text.startsWith('#');
const hashtag = text.match(/^\s?#[^\s]+/)[0]; const hashtag = text.match(/^\s?#[^\s]+/)[0];
const res: any[] = !isHead ? [{ const res: any[] = !isHead ? [{
type: 'text', type: 'text',
+1 -1
View File
@@ -13,7 +13,7 @@ export type TextElementLink = {
export default function(text: string) { export default function(text: string) {
const match = text.match(/^\??\[([^\[\]]+?)\]\((https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+?)\)/); const match = text.match(/^\??\[([^\[\]]+?)\]\((https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+?)\)/);
if (!match) return null; if (!match) return null;
const silent = text[0] == '?'; const silent = text.startsWith('?');
const link = match[0]; const link = match[0];
const title = match[1]; const title = match[1];
const url = match[2]; const url = match[2];
+2 -2
View File
@@ -25,9 +25,9 @@ export const replacement = (match: string, key: string) => {
arg == 'S' ? 'fas' : arg == 'S' ? 'fas' :
arg == 'B' ? 'fab' : arg == 'B' ? 'fab' :
''; '';
} else if (arg[0] == '.') { } else if (arg.startsWith('.')) {
classes.push('fa-' + arg.substr(1)); classes.push('fa-' + arg.substr(1));
} else if (arg[0] == '-') { } else if (arg.startsWith('-')) {
transform = arg.substr(1).split('|').join(' '); transform = arg.substr(1).split('|').join(' ');
} else { } else {
name = arg; name = arg;
+5 -3
View File
@@ -27,10 +27,12 @@ export default class Replacer {
let text = texts; let text = texts;
if (path) { if (path) {
path = path.replace('.ts', '');
if (text.hasOwnProperty(path)) { if (text.hasOwnProperty(path)) {
text = text[path]; text = text[path];
} else { } else {
if (this.lang === 'ja') console.warn(`path '${path}' not found`); if (this.lang === 'ja-JP') console.warn(`path '${path}' not found`);
return key; // Fallback return key; // Fallback
} }
} }
@@ -46,10 +48,10 @@ export default class Replacer {
}); });
if (error) { if (error) {
if (this.lang === 'ja') console.warn(`key '${key}' not found in '${path}'`); if (this.lang === 'ja-JP') console.warn(`key '${key}' not found in '${path}'`);
return key; // Fallback return key; // Fallback
} else if (typeof text !== 'string') { } else if (typeof text !== 'string') {
if (this.lang === 'ja') console.warn(`key '${key}' is not string in '${path}'`); if (this.lang === 'ja-JP') console.warn(`key '${key}' is not string in '${path}'`);
return key; // Fallback return key; // Fallback
} else { } else {
return text; return text;
+80 -31
View File
@@ -2,40 +2,59 @@ import * as mongo from 'mongodb';
import db from '../db/mongodb'; import db from '../db/mongodb';
const Stats = db.get<IStats>('stats'); const Stats = db.get<IStats>('stats');
Stats.createIndex({ date: -1 }, { unique: true }); Stats.dropIndex({ date: -1 }); // 後方互換性のため
Stats.createIndex({ span: -1, date: -1 }, { unique: true });
export default Stats; export default Stats;
export interface IStats { export interface IStats {
_id: mongo.ObjectID; _id: mongo.ObjectID;
/**
*
*/
date: Date; date: Date;
/**
*
*/
span: 'day' | 'hour';
/** /**
* *
*/ */
users: { users: {
local: { local: {
/** /**
* * ()
*/ */
total: number; total: number;
/** /**
* * ()
*/ */
diff: number; inc: number;
/**
* ()
*/
dec: number;
}; };
remote: { remote: {
/** /**
* * ()
*/ */
total: number; total: number;
/** /**
* * ()
*/ */
diff: number; inc: number;
/**
* ()
*/
dec: number;
}; };
}; };
@@ -45,28 +64,33 @@ export interface IStats {
notes: { notes: {
local: { local: {
/** /**
* 稿 * 稿 ()
*/ */
total: number; total: number;
/** /**
* 稿 * 稿 ()
*/ */
diff: number; inc: number;
/**
* 稿 ()
*/
dec: number;
diffs: { diffs: {
/** /**
* 稿 * 稿 ()
*/ */
normal: number; normal: number;
/** /**
* 稿 * 稿 ()
*/ */
reply: number; reply: number;
/** /**
* Renoteの投稿数の前日比 * Renoteの投稿数の差分 ()
*/ */
renote: number; renote: number;
}; };
@@ -74,28 +98,33 @@ export interface IStats {
remote: { remote: {
/** /**
* 稿 * 稿 ()
*/ */
total: number; total: number;
/** /**
* 稿 * 稿 ()
*/ */
diff: number; inc: number;
/**
* 稿 ()
*/
dec: number;
diffs: { diffs: {
/** /**
* 稿 * 稿 ()
*/ */
normal: number; normal: number;
/** /**
* 稿 * 稿 ()
*/ */
reply: number; reply: number;
/** /**
* Renoteの投稿数の前日比 * Renoteの投稿数の差分 ()
*/ */
renote: number; renote: number;
}; };
@@ -108,46 +137,66 @@ export interface IStats {
drive: { drive: {
local: { local: {
/** /**
* * ()
*/ */
totalCount: number; totalCount: number;
/** /**
* * ()
*/ */
totalSize: number; totalSize: number;
/** /**
* * ()
*/ */
diffCount: number; incCount: number;
/** /**
* * 使 ()
*/ */
diffSize: number; incSize: number;
/**
* ()
*/
decCount: number;
/**
* 使 ()
*/
decSize: number;
}; };
remote: { remote: {
/** /**
* * ()
*/ */
totalCount: number; totalCount: number;
/** /**
* * ()
*/ */
totalSize: number; totalSize: number;
/** /**
* * ()
*/ */
diffCount: number; incCount: number;
/** /**
* * 使 ()
*/ */
diffSize: number; incSize: number;
/**
* ()
*/
decCount: number;
/**
* 使 ()
*/
decSize: number;
}; };
}; };
} }
+1 -1
View File
@@ -46,7 +46,7 @@ export default async (job: bq.Job, done: any): Promise<void> => {
// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する // アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
if (user === null) { if (user === null) {
user = await resolvePerson(signature.keyId) as IRemoteUser; user = await resolvePerson(activity.actor) as IRemoteUser;
} }
} }
+3 -1
View File
@@ -131,5 +131,7 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver):
//#endregion //#endregion
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録
return await createNote(value, resolver); // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
return await createNote(uri, resolver);
} }
+48 -22
View File
@@ -4,18 +4,25 @@ import * as debug from 'debug';
import config from '../../../config'; import config from '../../../config';
import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user'; import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user';
import webFinger from '../../webfinger';
import Resolver from '../resolver'; import Resolver from '../resolver';
import { resolveImage } from './image'; import { resolveImage } from './image';
import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type'; import { isCollectionOrOrderedCollection, IPerson } from '../type';
import { IDriveFile } from '../../../models/drive-file'; import { IDriveFile } from '../../../models/drive-file';
import Meta from '../../../models/meta'; import Meta from '../../../models/meta';
import htmlToMFM from '../../../mfm/html-to-mfm'; import htmlToMFM from '../../../mfm/html-to-mfm';
import { updateUserStats } from '../../../services/update-chart'; import { updateUserStats } from '../../../services/update-chart';
import { URL } from 'url';
const log = debug('misskey:activitypub'); const log = debug('misskey:activitypub');
function validatePerson(x: any) { /**
* Validate Person object
* @param x Fetched person object
* @param uri Fetch target URI
*/
function validatePerson(x: any, uri: string) {
const expectHost = toUnicode(new URL(uri).hostname.toLowerCase());
if (x == null) { if (x == null) {
return new Error('invalid person: object is null'); return new Error('invalid person: object is null');
} }
@@ -40,6 +47,24 @@ function validatePerson(x: any) {
return new Error('invalid person: invalid name'); return new Error('invalid person: invalid name');
} }
if (typeof x.id !== 'string') {
return new Error('invalid person: id is not a string');
}
const idHost = toUnicode(new URL(x.id).hostname.toLowerCase());
if (idHost !== expectHost) {
return new Error('invalid person: id has different host');
}
if (typeof x.publicKey.id !== 'string') {
return new Error('invalid person: publicKey.id is not a string');
}
const publicKeyIdHost = toUnicode(new URL(x.publicKey.id).hostname.toLowerCase());
if (publicKeyIdHost !== expectHost) {
return new Error('invalid person: publicKey.id has different host');
}
return null; return null;
} }
@@ -48,8 +73,8 @@ function validatePerson(x: any) {
* *
* Misskeyに対象のPersonが登録されていればそれを返します * Misskeyに対象のPersonが登録されていればそれを返します
*/ */
export async function fetchPerson(value: string | IObject, resolver?: Resolver): Promise<IUser> { export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUser> {
const uri = typeof value == 'string' ? value : value.id; if (typeof uri !== 'string') throw 'uri is not string';
// URIがこのサーバーを指しているならデータベースからフェッチ // URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(config.url + '/')) { if (uri.startsWith(config.url + '/')) {
@@ -71,12 +96,14 @@ export async function fetchPerson(value: string | IObject, resolver?: Resolver):
/** /**
* Personを作成します * Personを作成します
*/ */
export async function createPerson(value: any, resolver?: Resolver): Promise<IUser> { export async function createPerson(uri: string, resolver?: Resolver): Promise<IUser> {
if (typeof uri !== 'string') throw 'uri is not string';
if (resolver == null) resolver = new Resolver(); if (resolver == null) resolver = new Resolver();
const object = await resolver.resolve(value) as any; const object = await resolver.resolve(uri) as any;
const err = validatePerson(object); const err = validatePerson(object, uri);
if (err) { if (err) {
throw err; throw err;
@@ -86,7 +113,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
log(`Creating the Person: ${person.id}`); log(`Creating the Person: ${person.id}`);
const [followersCount = 0, followingCount = 0, notesCount = 0, finger] = await Promise.all([ const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([
resolver.resolve(person.followers).then( resolver.resolve(person.followers).then(
resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
() => undefined () => undefined
@@ -98,11 +125,10 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
resolver.resolve(person.outbox).then( resolver.resolve(person.outbox).then(
resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
() => undefined () => undefined
), )
webFinger(person.id)
]); ]);
const host = toUnicode(finger.subject.replace(/^.*?@/, '')).toLowerCase(); const host = toUnicode(new URL(object.id).hostname.toLowerCase());
const isBot = object.type == 'Service'; const isBot = object.type == 'Service';
@@ -166,8 +192,8 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
const avatarId = avatar ? avatar._id : null; const avatarId = avatar ? avatar._id : null;
const bannerId = banner ? banner._id : null; const bannerId = banner ? banner._id : null;
const avatarUrl = avatar && avatar.metadata.url ? avatar.metadata.url : null; const avatarUrl = (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : null;
const bannerUrl = banner && banner.metadata.url ? banner.metadata.url : null; const bannerUrl = (banner && banner.metadata.url) ? banner.metadata.url : null;
await User.update({ _id: user._id }, { await User.update({ _id: user._id }, {
$set: { $set: {
@@ -192,8 +218,8 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
* *
* Misskeyに対象のPersonが登録されていなければ無視します * Misskeyに対象のPersonが登録されていなければ無視します
*/ */
export async function updatePerson(value: string | IObject, resolver?: Resolver): Promise<void> { export async function updatePerson(uri: string, resolver?: Resolver): Promise<void> {
const uri = typeof value == 'string' ? value : value.id; if (typeof uri !== 'string') throw 'uri is not string';
// URIがこのサーバーを指しているならスキップ // URIがこのサーバーを指しているならスキップ
if (uri.startsWith(config.url + '/')) { if (uri.startsWith(config.url + '/')) {
@@ -210,9 +236,9 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
if (resolver == null) resolver = new Resolver(); if (resolver == null) resolver = new Resolver();
const object = await resolver.resolve(value) as any; const object = await resolver.resolve(uri) as any;
const err = validatePerson(object); const err = validatePerson(object, uri);
if (err) { if (err) {
throw err; throw err;
@@ -255,7 +281,7 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
sharedInbox: person.sharedInbox, sharedInbox: person.sharedInbox,
avatarId: avatar ? avatar._id : null, avatarId: avatar ? avatar._id : null,
bannerId: banner ? banner._id : null, bannerId: banner ? banner._id : null,
avatarUrl: avatar && avatar.metadata.url ? avatar.metadata.url : null, avatarUrl: (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : null,
bannerUrl: banner && banner.metadata.url ? banner.metadata.url : null, bannerUrl: banner && banner.metadata.url ? banner.metadata.url : null,
description: htmlToMFM(person.summary), description: htmlToMFM(person.summary),
followersCount, followersCount,
@@ -275,8 +301,8 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
* Misskeyに対象のPersonが登録されていればそれを返し * Misskeyに対象のPersonが登録されていればそれを返し
* Misskeyに登録しそれを返します * Misskeyに登録しそれを返します
*/ */
export async function resolvePerson(value: string | IObject, verifier?: string): Promise<IUser> { export async function resolvePerson(uri: string, verifier?: string): Promise<IUser> {
const uri = typeof value == 'string' ? value : value.id; if (typeof uri !== 'string') throw 'uri is not string';
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
const exist = await fetchPerson(uri); const exist = await fetchPerson(uri);
@@ -287,5 +313,5 @@ export async function resolvePerson(value: string | IObject, verifier?: string):
//#endregion //#endregion
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録
return await createPerson(value); return await createPerson(uri);
} }
@@ -6,6 +6,7 @@ export default (object: any, note: INote) => {
return { return {
id: `${config.url}/notes/${note._id}`, id: `${config.url}/notes/${note._id}`,
actor: `${config.url}/users/${note.userId}`,
type: 'Announce', type: 'Announce',
published: note.createdAt.toISOString(), published: note.createdAt.toISOString(),
to: ['https://www.w3.org/ns/activitystreams#Public'], to: ['https://www.w3.org/ns/activitystreams#Public'],
+17 -4
View File
@@ -1,4 +1,17 @@
export default (object: any) => ({ import config from '../../../config';
type: 'Create', import { INote } from '../../../models/note';
object
}); export default (object: any, note: INote) => {
const activity = {
id: `${config.url}/notes/${note._id}/activity`,
actor: `${config.url}/users/${note.userId}`,
type: 'Create',
published: note.createdAt.toISOString(),
object
} as any;
if (object.to) activity.to = object.to;
if (object.cc) activity.cc = object.cc;
return activity;
};
+5 -1
View File
@@ -1,4 +1,8 @@
export default (object: any) => ({ import config from '../../../config';
import { ILocalUser } from "../../../models/user";
export default (object: any, user: ILocalUser) => ({
type: 'Delete', type: 'Delete',
actor: `${config.url}/users/${user._id}`,
object object
}); });
+16 -7
View File
@@ -1,7 +1,16 @@
export default (x: any) => Object.assign({ import config from '../../../config';
'@context': [ import * as uuid from 'uuid';
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1', export default (x: any) => {
{ Hashtag: 'as:Hashtag' } if (x !== null && typeof x === 'object' && x.id == null) {
] x.id = `${config.url}/${uuid.v4()}`;
}, x); }
return Object.assign({
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{ Hashtag: 'as:Hashtag' }
]
}, x);
};
+5 -1
View File
@@ -1,4 +1,8 @@
export default (object: any) => ({ import config from '../../../config';
import { ILocalUser, IUser } from "../../../models/user";
export default (object: any, user: ILocalUser | IUser) => ({
type: 'Undo', type: 'Undo',
actor: `${config.url}/users/${user._id}`,
object object
}); });
+4 -1
View File
@@ -19,6 +19,9 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso
port, port,
method: 'POST', method: 'POST',
path: pathname + search, path: pathname + search,
headers: {
'Content-Type': 'application/activity+json'
}
}, res => { }, res => {
log(`${url} --> ${res.statusCode}`); log(`${url} --> ${res.statusCode}`);
@@ -32,7 +35,7 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso
sign(req, { sign(req, {
authorizationHeaderName: 'Signature', authorizationHeaderName: 'Signature',
key: user.keypair, key: user.keypair,
keyId: `acct:${user.username}@${config.host}` keyId: `${config.url}/users/${user._id}/publickey`
}); });
// Signature: Signature ... => Signature: ... // Signature: Signature ... => Signature: ...
+1 -1
View File
@@ -15,7 +15,7 @@ export default async (username: string, _host: string, option?: any): Promise<IU
const host = toUnicode(hostAscii); const host = toUnicode(hostAscii);
if (config.host == host) { if (config.host == host) {
return await User.findOne({ usernameLower }); return await User.findOne({ usernameLower, host: null });
} }
let user = await User.findOne({ usernameLower, host }, option); let user = await User.findOne({ usernameLower, host }, option);
+1 -1
View File
@@ -25,7 +25,7 @@ function inbox(ctx: Router.IRouterContext) {
ctx.req.headers.authorization = 'Signature ' + ctx.req.headers.signature; ctx.req.headers.authorization = 'Signature ' + ctx.req.headers.signature;
try { try {
signature = httpSignature.parseRequest(ctx.req); signature = httpSignature.parseRequest(ctx.req, { 'headers': [] });
} catch (e) { } catch (e) {
ctx.status = 401; ctx.status = 401;
return; return;
+1 -1
View File
@@ -1 +1 @@
export default (token: string) => token[0] == '!'; export default (token: string) => token.startsWith('!');
-101
View File
@@ -1,101 +0,0 @@
import Stats, { IStats } from '../../../../models/stats';
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export const meta = {
requireCredential: true,
requireAdmin: true
};
export default (params: any) => new Promise(async (res, rej) => {
const now = new Date();
const y = now.getFullYear();
const m = now.getMonth();
const d = now.getDate();
const stats = await Stats.find({
date: {
$gt: new Date(y - 1, m, d)
}
}, {
sort: {
date: -1
},
fields: {
_id: 0
}
});
const chart: Array<Omit<IStats, '_id'>> = [];
for (let i = 364; i >= 0; i--) {
const day = new Date(y, m, d - i);
const stat = stats.find(s => s.date.getTime() == day.getTime());
if (stat) {
chart.unshift(stat);
} else { // 隙間埋め
const mostRecent = stats.find(s => s.date.getTime() < day.getTime());
if (mostRecent) {
chart.unshift(Object.assign({}, mostRecent, {
date: day
}));
} else {
chart.unshift({
date: day,
users: {
local: {
total: 0,
diff: 0
},
remote: {
total: 0,
diff: 0
}
},
notes: {
local: {
total: 0,
diff: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
},
remote: {
total: 0,
diff: 0,
diffs: {
normal: 0,
reply: 0,
renote: 0
}
}
},
drive: {
local: {
totalCount: 0,
totalSize: 0,
diffCount: 0,
diffSize: 0
},
remote: {
totalCount: 0,
totalSize: 0,
diffCount: 0,
diffSize: 0
}
}
});
}
}
}
chart.forEach(x => {
delete x.date;
});
res(chart);
});
+1 -1
View File
@@ -3,7 +3,7 @@ import RegistrationTicket from '../../../../models/registration-tickets';
export const meta = { export const meta = {
desc: { desc: {
ja: '招待コードを発行します。' 'ja-JP': '招待コードを発行します。'
}, },
requireCredential: true, requireCredential: true,

Some files were not shown because too many files have changed in this diff Show More