diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b5d8c8307..e8879b747a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ ChangeLog This document describes breaking changes only. +7.0.0 +----- + +### Migration + +起動する前に、`node cli/migration/7.0.0`してください。 + +Please run `node cli/migration/7.0.0` before launch. + 6.0.0 ----- diff --git a/README.md b/README.md index a52ead65a7..d288220219 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,12 @@ [![][dependencies-badge]][dependencies-link] [](http://makeapullrequest.com) [](https://greenkeeper.io/) -**Microblogging. Redefined.** +Sophisticated microblogging platform, evolving forever. -**[Misskey](https://misskey.xyz)** is a completely open source, -ultimately sophisticated professional microblogging software. +[Misskey](https://misskey.xyz) is a decentralized microblogging platform born on Earth. +Since it exists within the Fediverse (a universe where various social media platforms are organized), +it is mutually linked with other social media platforms. +Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet? <a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a> @@ -28,7 +30,7 @@ ultimately sophisticated professional microblogging software. and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz). -:package: Create your instance +:package: Create your own instance ---------------------------------------------------------------- If you want to run your own instance of Misskey, please see [Setup and installation guide](./docs/setup.en.md). @@ -43,6 +45,7 @@ If you want to... :heart: Backers & Sponsors ---------------------------------------------------------------- +<!-- PATREON_START --> <table> <tr> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=tB1e_r8RlZ5sFL0KV_e8dugapxatNBRK1Z3h67TO1g8%3D"></td> @@ -71,6 +74,7 @@ If you want to... <td><a href="https://www.patreon.com/gutfuckllc">gutfuckllc</a></td> </tr> </table> +<!-- PATREON_END --> :four_leaf_clover: Copyright ---------------------------------------------------------------- diff --git a/cli/clean-cached-remote-files.js b/cli/clean-cached-remote-files.js deleted file mode 100644 index 5b388c73b4..0000000000 --- a/cli/clean-cached-remote-files.js +++ /dev/null @@ -1,101 +0,0 @@ -const chalk = require('chalk'); -const log = require('single-line-log').stdout; -const sequential = require('promise-sequential'); -const { default: DriveFile, DriveFileChunk } = require('../built/models/drive-file'); -const { default: DriveFileThumbnail, DriveFileThumbnailChunk } = require('../built/models/drive-file-thumbnail'); -const { default: User } = require('../built/models/user'); - -const q = { - 'metadata._user.host': { - $ne: null - }, - 'metadata.withoutChunks': false -}; - -async function main() { - const promiseGens = []; - - const count = await DriveFile.count(q); - - let prev; - - for (let i = 0; i < count; i++) { - promiseGens.push(() => { - const promise = new Promise(async (res, rej) => { - const file = await DriveFile.findOne(prev ? Object.assign({ - _id: { $lt: prev._id } - }, q) : q, { - sort: { - _id: -1 - } - }); - - prev = file; - - function skip() { - res([i, file, false]); - } - - if (file == null) return skip(); - - log(chalk`{gray ${i}} scanning {bold ${file._id}} ${file.filename} ...`); - - const attachingUsersCount = await User.count({ - $or: [{ - avatarId: file._id - }, { - bannerId: file._id - }] - }, { limit: 1 }); - if (attachingUsersCount !== 0) return skip(); - - Promise.all([ - // チャンクをすべて削除 - DriveFileChunk.remove({ - files_id: file._id - }), - - DriveFile.update({ _id: file._id }, { - $set: { - 'metadata.withoutChunks': true - } - }) - ]).then(async () => { - res([i, file, true]); - - //#region サムネイルもあれば削除 - const thumbnail = await DriveFileThumbnail.findOne({ - 'metadata.originalId': file._id - }); - - if (thumbnail) { - DriveFileThumbnailChunk.remove({ - files_id: thumbnail._id - }); - - DriveFileThumbnail.remove({ _id: thumbnail._id }); - } - //#endregion - }); - }); - - promise.then(([i, file, deleted]) => { - if (deleted) { - log(chalk`{gray ${i}} {red deleted: {bold ${file._id}} ${file.filename}}`); - } else { - log(chalk`{gray ${i}} {green skipped: {bold ${file._id}} ${file.filename}}`); - } - log.clear(); - console.log(); - }); - - return promise; - }); - } - - return await sequential(promiseGens); -} - -main().then(() => { - console.log('ALL DONE'); -}).catch(console.error); diff --git a/cli/clean-unused-drive-files.js b/cli/clean-unused-drive-files.js deleted file mode 100644 index 87b158b9ee..0000000000 --- a/cli/clean-unused-drive-files.js +++ /dev/null @@ -1,80 +0,0 @@ -const chalk = require('chalk'); -const log = require('single-line-log').stdout; -const sequential = require('promise-sequential'); -const { default: DriveFile, deleteDriveFile } = require('../built/models/drive-file'); -const { default: Note } = require('../built/models/note'); -const { default: MessagingMessage } = require('../built/models/messaging-message'); -const { default: User } = require('../built/models/user'); - -async function main() { - const promiseGens = []; - - const count = await DriveFile.count({}); - - let prev; - - for (let i = 0; i < count; i++) { - promiseGens.push(() => { - const promise = new Promise(async (res, rej) => { - const file = await DriveFile.findOne(prev ? { - _id: { $lt: prev._id } - } : {}, { - sort: { - _id: -1 - } - }); - - prev = file; - - function skip() { - res([i, file, false]); - } - - if (file == null) return skip(); - - log(chalk`{gray ${i}} scanning {bold ${file._id}} ${file.filename} ...`); - - const attachingUsersCount = await User.count({ - $or: [{ - avatarId: file._id - }, { - bannerId: file._id - }] - }, { limit: 1 }); - if (attachingUsersCount !== 0) return skip(); - - const attachingNotesCount = await Note.count({ - mediaIds: file._id - }, { limit: 1 }); - if (attachingNotesCount !== 0) return skip(); - - const attachingMessagesCount = await MessagingMessage.count({ - fileId: file._id - }, { limit: 1 }); - if (attachingMessagesCount !== 0) return skip(); - - deleteDriveFile(file).then(() => { - res([i, file, true]); - }).catch(rej); - }); - - promise.then(([i, file, deleted]) => { - if (deleted) { - log(chalk`{gray ${i}} {red deleted: {bold ${file._id}} ${file.filename}}`); - } else { - log(chalk`{gray ${i}} {green skipped: {bold ${file._id}} ${file.filename}}`); - } - log.clear(); - console.log(); - }); - - return promise; - }); - } - - return await sequential(promiseGens); -} - -main().then(() => { - console.log('done'); -}).catch(console.error); diff --git a/cli/migration/7.0.0.js b/cli/migration/7.0.0.js new file mode 100644 index 0000000000..fa5e363db8 --- /dev/null +++ b/cli/migration/7.0.0.js @@ -0,0 +1,134 @@ +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 today = new Date(y, m, d); + +async function main() { + 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: today, + 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(); diff --git a/docs/manage.en.md b/docs/manage.en.md index a7296ce479..713070517c 100644 --- a/docs/manage.en.md +++ b/docs/manage.en.md @@ -33,14 +33,3 @@ node cli/suspend @syuilo@misskey.xyz ``` shell node cli/reset-password (User-ID or Username) ``` - -## Clean up cached remote files -``` shell -node cli/clean-cached-remote-files -``` - -## Clean up unused drive files -``` shell -node cli/clean-unused-drive-files -``` -> We recommend that you announce a user that unused drive files will be deleted before performing this operation, as it may delete the user's important files. diff --git a/docs/manage.ja.md b/docs/manage.ja.md index f289037ad3..897fae7ec2 100644 --- a/docs/manage.ja.md +++ b/docs/manage.ja.md @@ -33,14 +33,3 @@ node cli/suspend @syuilo@misskey.xyz ``` shell node cli/reset-password (ユーザーID または ユーザー名) ``` - -## キャッシュされたリモートファイルをクリーンアップする -``` shell -node cli/clean-cached-remote-files -``` - -## 使われていないドライブのファイルをクリーンアップする -``` shell -node cli/clean-unused-drive-files -``` -> ユーザーの大事なファイルを削除する可能性があるので、この操作を実行する前にユーザーに告知することをお勧めします。 diff --git a/locales/index.js b/locales/index.js index 5b525c77df..a17fb6c5ce 100644 --- a/locales/index.js +++ b/locales/index.js @@ -15,6 +15,7 @@ const langs = { 'en': loadLang('en'), 'fr': loadLang('fr'), 'ja': native, + 'ja-ks': loadLang('ja-ks'), 'pl': loadLang('pl'), 'es': loadLang('es') }; diff --git a/locales/ja-ks.yml b/locales/ja-ks.yml new file mode 100644 index 0000000000..283b3f3e7e --- /dev/null +++ b/locales/ja-ks.yml @@ -0,0 +1,1412 @@ +meta: + lang: "日本語" + divider: "関西弁" + +common: + misskey: "A ⭐ of fediverse" + about-title: "A ⭐ of fediverse." + about: "ようMisskeyを見つけてくれて、おおきにやで。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>やねん。Fediverse(ぎょうさんのSNSで構成されとる宇宙)っちゅうもんの中におるから、お隣さんのSNSとも仲良うさせてもろてんねん。ちょいとやかましい心斎橋から離れて、新しいインターネットにダイブしてみぃひん?" + adblock: + detected: "広告ブロッカーを切っとくれんか" + warning: "<strong>Misskeyは広告を掲載しとらん</strong>けど、広告をブロックする機能が有効やと一部の機能が利用できんくなったり、不具合が発生するかも分からん。知らんけど。" + application-authorization: "アプリの連携" + close: "ほなさいなら" + do-not-copy-paste: "ここにコードを入力したり張り付けたりせんといてください。アカウントが不正利用されるかも分からん。知らんけど。" + got-it: "ほい" + customization-tips: + title: "カスタマイズのヒント" + paragraph1: "ホームのカスタマイズでは、ウィジェットを追加/削除したり、ドラッグ&ドロップして並べ替えたりできんねやわ。。" + paragraph2: "一部のウィジェットは、<strong><strong>右</strong>クリック</strong>したったら表示を変更できんねやわ。" + paragraph3: "ウィジェットを削除するんやったら、ヘッダーの<strong>「ゴミ箱」</strong>と書いたぁるエリアにウィジェットをドラッグ&ドロップしてな。" + paragraph4: "カスタマイズを終了するんやったら、右上の「完了」をクリックしてな。" + gotit: "Got it!" + notification: + file-uploaded: "ファイルがアップロードされたで" + message-from: "{}はんからメッセージ:" + reversi-invited: "対局への招待がきとるで" + reversi-invited-by: "{}はんから" + notified-by: "{}はんから" + reply-from: "{}はんから返信:" + quoted-by: "{}はんが引用:" + time: + unknown: "なぞのじかん" + future: "未来" + just_now: "たった今" + seconds_ago: "{}秒前" + minutes_ago: "{}分前" + hours_ago: "{}時間前" + days_ago: "{}日前" + weeks_ago: "{}週間前" + months_ago: "{}ヶ月前" + years_ago: "{}年前" + month-and-day: "{month}月 {day}日" + + trash: "ゴミ箱" + + weekday-short: + sunday: "日" + monday: "月" + tuesday: "火" + wednesday: "水" + thursday: "木" + friday: "金" + saturday: "土" + + weekday: + sunday: "日曜日" + monday: "月曜日" + tuesday: "火曜日" + wednesday: "水曜日" + thursday: "木曜日" + friday: "金曜日" + saturday: "土曜日" + + reactions: + like: "いいね" + love: "しゅき" + laugh: "笑" + hmm: "ふぅ~む" + surprise: "わお" + congrats: "おめでとう" + angry: "おこ" + confused: "こまこまのこまり" + rip: "RIP" + pudding: "Pudding" + + note-placeholders: + a: "今なにしてん?" + b: "何かあったんか?" + c: "何考えとりますん?" + d: "言いたいことは?" + e: "ここに書いてや" + f: "あんさんが書くのを待っちょります..." + + search: "検索" + delete: "削除" + loading: "読み込み中" + ok: "ほい" + update-available-title: "更新があるで" + update-available: "Misskeyの新しいバージョンがあるで({newer}。現在{current}を利用中)。ページを再度読み込みしたると更新が適用されるわ。" + my-token-regenerated: "あんさんのトークンが更新されたらしい、知らんけど。まあとりあえずサインアウトすんで。" + i-like-sushi: "寿司(のほうがプリンよりむしろ)ウマい、タコ焼きはあらへんけど。" + show-reversi-board-labels: "リバーシのボードの行と列のラベルを表示" + verified-user: "公式アカウント" + disable-animated-mfm: "投稿内の動きのあるテキストを無効にする" + + reversi: + drawn: "引き分け" + my-turn: "あんさんのターンや" + opponent-turn: "相手のターンや" + turn-of: "{}のターンです" + past-turn-of: "{}のターン" + won: "{}の勝ち" + black: "黒" + white: "白" + total: "合計" + this-turn: "{}ターン目" + + widgets: + analog-clock: "アナログ時計" + profile: "プロフィール" + calendar: "カレンダー" + timemachine: "カレンダー(タイムマシン)" + activity: "アクティビティ" + rss: "RSSリーダー" + memo: "付箋" + trends: "トレンド" + photo-stream: "フォトストリーム" + posts-monitor: "投稿チャート" + slideshow: "スライドショー" + version: "バージョン" + broadcast: "ブロードキャスト" + notifications: "通知" + users: "おすすめユーザー" + polls: "アンケート" + post-form: "投稿フォーム" + messaging: "メッセージ" + server: "サーバー情報" + donation: "寄付のお願い" + nav: "ナビゲーション" + tips: "ヒント" + hashtags: "ハッシュタグ" + + deck: + widgets: "ウィジェット" + home: "ホーム" + local: "ローカル" + hybrid: "ソーシャル" + global: "グローバル" + notifications: "通知" + list: "リスト" + swap-left: "左に移動" + swap-right: "右に移動" + swap-up: "上に移動" + swap-down: "下に移動" + remove: "カラムを削除" + add-column: "カラムを追加" + rename: "名前を変更" + stack-left: "左に重ねる" + pop-right: "右に出す" + +auth/views/form.vue: + share-access: "<i>{{ app.name }}</i>があんさんのアカウントにアクセスすんのを<b>許可</b>してもええか?" + permission-ask: "このアプリは次の権限を要求しとるで:" + account-read: "アカウントの情報を見させてもらうで。" + account-write: "アカウントの情報を操作させてもらうで。" + note-write: "投稿させてもらうで。" + like-write: "いいねしたりいいね解除させてもらうで。" + following-write: "フォローしたりフォロー解除させてもらうで。" + drive-read: "ドライブを見させてもらうで。" + drive-write: "ドライブを操作させてもらうで。" + notification-read: "通知を見させてもらうで。" + notification-write: "通知を操作させてもらうで。" + cancel: "キャンセル" + accept: "アクセスを許可" + +auth/views/index.vue: + loading: "読み込み中" + denied: "アプリケーションの連携をキャンセルしたわ。" + denied-paragraph: "このアプリがあんさんのアカウントにアクセスすることはあらへん。知らんけど。" + already-authorized: "このアプリはもう連携済みやったわ" + allowed: "アプリケーションの連携を許可したで" + callback-url: "アプリケーションに戻っとります" + please-go-back: "アプリケーションに戻って、気張ってってな。" + error: "セッションが存在しとらん。" + sign-in: "サインインしてや" + +common/views/components/games/reversi/reversi.vue: + matching: + waiting-for: "{}を待っとる" + cancel: "やっぱやめ" + +common/views/components/games/reversi/reversi.game.vue: + surrender: "投了" + surrendered: "投了により" + is-llotheo: "石の少ない方が勝ち(ロセオ)" + looped-map: "ループマップ" + can-put-everywhere: "どこでも置けるモード" + +common/views/components/games/reversi/reversi.index.vue: + title: "Misskey Reversi" + sub-title: "お隣のミスキストはんらとリバーシで対戦や!" + invite: "招待" + rule: "遊び方" + rule-desc: "リバーシは、相手と交互に石をボードに置いて、相手の石を挟んで自分の色に変えてって、最終的に残った石が多い方が勝ちっちゅうボードゲームや。" + mode-invite: "招待" + mode-invite-desc: "指定したユーザーと対戦するモードや。" + invitations: "対局の招待がきとるで。" + my-games: "自分の対局" + all-games: "みんなの対局" + enter-username: "ユーザー名を入力してや" + game-state: + ended: "終了" + playing: "進行中" + +common/views/components/games/reversi/reversi.room.vue: + settings-of-the-game: "ゲームの設定" + choose-map: "マップを選択" + random: "ランダム" + black-or-white: "先手/後手" + black-is: "{}が黒" + rules: "ルール" + is-llotheo: "石の少ない方が勝ち(ロセオ)" + looped-map: "ループマップ" + can-put-everywhere: "どこでも置けるモード" + settings-of-the-bot: "Botの設定" + this-game-is-started-soon: "ゲームは数秒後に開始されんで" + waiting-for-other: "相手の準備が完了すんのを待っとるで" + waiting-for-me: "あんさんの準備が完了すんのを待っとるで" + waiting-for-both: "準備中" + cancel: "キャンセル" + ready: "準備完了" + cancel-ready: "準備続行" + +common/views/components/connect-failed.vue: + title: "サーバーに接続できんわ" + description: "インターネット回線に問題が起きとるか、サーバーがダウンまたはメンテナンスしとるっぽいわ。知らんけど。とりあえずあとで{再試行}してや。" + thanks: "いつもMisskeyをつこてくれてほんまおおきにやで。" + troubleshoot: "トラブルシュート" + +common/views/components/connect-failed.troubleshooter.vue: + title: "トラブルシューティング" + network: "ネットワーク接続" + checking-network: "ネットワーク接続を確認中" + internet: "インターネット接続" + checking-internet: "インターネット接続を確認中" + server: "サーバー接続" + checking-server: "サーバー接続を確認中" + finding: "問題を調べとります" + no-network: "ネットワークに接続されとりません" + no-network-desc: "つこてるPCのネットワーク接続が正常か確認してや。" + no-internet: "インターネットに接続されとりません" + no-internet-desc: "ネットワークには接続されとるけど、インターネットには接続されとらんようやわ。つこてるPCのインターネット接続が正常か確認してや。" + no-server: "Misskeyのサーバーに接続できん" + no-server-desc: "つこてるPCのインターネット接続は正常やけど、Misskeyのサーバーにはつながらんわ。多分サーバーがダウンまたはメンテナンスしとるわ、知らんけど。すまんけどしばらくしてから再度アクセスしてみてや。" + success: "Misskeyのサーバーに接続できたわ" + success-desc: "正常に接続できるようやわ。ページを再度読み込みしてな。" + flush: "キャッシュの削除" + set-version: "バージョン指定" + +common/views/components/messaging.vue: + search-user: "ユーザーを探す" + you: "あんさん" + no-history: "履歴はあらへん" + +common/views/components/messaging-room.vue: + empty: "このユーザーと話したことはあらへん" + more: "もっと読む" + no-history: "これより過去の履歴はあらへん" + resize-form: "ドラッグしてフォームの広さを調整" + new-message: "新しいメッセージがあるで" + only-one-file-attached: "メッセージに添付できんのはひとつのファイルのみや" + +common/views/components/messaging-room.form.vue: + input-message-here: "ここにメッセージを入力" + send: "送信" + attach-from-local: "PCからファイルを添付する" + attach-from-drive: "ドライブからファイルを添付する" + only-one-file-attached: "メッセージに添付できんのはひとつのファイルのみや" + +common/views/components/messaging-room.message.vue: + is-read: "既読" + deleted: "このメッセージは削除されたわ" + +common/views/components/nav.vue: + about: "Misskeyについて" + stats: "統計" + status: "ステータス" + wiki: "Wiki" + donors: "ドナー" + repository: "リポジトリ" + develop: "開発者" + feedback: "フィードバック" + +common/views/components/note-menu.vue: + favorite: "お気に入り" + pin: "ピン留め" + delete: "削除" + delete-confirm: "この投稿を削除してもええか?" + remote: "投稿元で見る" + +common/views/components/poll.vue: + vote-to: "「{}」に投票や!" + vote-count: "{}票" + total-users: "{}人が投票" + vote: "投票する" + show-result: "結果を見る" + voted: "投票済み" + +common/views/components/poll-editor.vue: + no-only-one-choice: "アンケートには、選択肢が最低2つ必要や" + choice-n: "選択肢{}" + remove: "この選択肢を削除" + add: "+選択肢を追加" + destroy: "アンケートを破棄" + +common/views/components/reaction-picker.vue: + choose-reaction: "リアクションを選択" + +common/views/components/signin.vue: + username: "ユーザー名" + password: "パスワード" + token: "トークン" + signing-in: "やっとります..." + signin: "サインイン" + or: "または" + signin-with-twitter: "Twitterでログイン" + login-failed: "ログインできんかったわ。ユーザー名とパスワードを確認してや。" + +common/views/components/signup.vue: + invitation-code: "招待コード" + invitation-info: "招待コードをもっとらんのやったら、<a href=\"{}\">管理者</a>まで連絡してや。" + username: "ユーザー名" + checking: "確認しとります..." + available: "利用できます" + unavailable: "既に利用されとります" + error: "通信エラー" + invalid-format: "a~z、A~Z、0~9、_が使えますねん" + too-short: "1文字以上でよろしゅうな!" + too-long: "20文字以内でよろしゅうな" + password: "パスワード" + password-placeholder: "8文字以上が推奨ですねん" + weak-password: "へぼいパスワード" + normal-password: "ぼちぼちパスワード" + strong-password: "ええ感じのパスワード" + retype: "再入力" + retype-placeholder: "確認のためもっぺん入力してや" + password-matched: "確認されたで" + password-not-matched: "一致しとらんで" + recaptcha: "認証" + create: "アカウント作成" + some-error: "何かよう分からんけど、アカウントの作成に失敗してしもたわ。すまんがもっぺん試してくれへんか?" + +common/views/components/special-message.vue: + new-year: "Happy New Year!" + christmas: "Merry Christmas!" + +common/views/components/stream-indicator.vue: + connecting: "接続中" + reconnecting: "再接続中" + connected: "接続完了" + +common/views/components/twitter-setting.vue: + description: "つことるTwitterアカウントをつことるMisskeyアカウントに接続しとくと、プロフィールでTwitterアカウント情報が表示されるようになったり、Twitterを用いた便利なサインインを利用できるようになったりすんで。" + connected-to: "次のTwitterアカウントに接続されとります" + detail: "詳細..." + reconnect: "再接続する" + connect: "Twitterと接続する" + disconnect: "切断する" + +common/views/components/uploader.vue: + waiting: "待機中" + +common/views/components/visibility-chooser.vue: + public: "公開" + home: "ホーム" + home-desc: "ホームタイムラインにのみ公開" + followers: "フォロワー" + followers-desc: "自分のフォロワーにのみ公開" + specified: "ダイレクト" + specified-desc: "指定したユーザーにのみ公開" + private: "非公開" + +common/views/widgets/broadcast.vue: + fetching: "確認中" + no-broadcasts: "お知らせはあらへん" + have-a-nice-day: "おおきに!" + next: "次" + +common/views/widgets/calendar.vue: + year: "{}年" + month: "{}月" + day: "{}日" + today: "今日:" + this-month: "今月:" + this-year: "今年:" + +common/views/widgets/donation.vue: + title: "寄付のお願い" + text: "Misskeyの運営にはドメイン、サーバー等のコストが掛かりますねん。Misskeyは広告を掲載したりせんから、収入を皆様からの寄付に頼ってますねん。もしご興味があれば、{}までご連絡よろしゅうな。ご協力おおきにやで。" + +common/views/widgets/photo-stream.vue: + title: "フォトストリーム" + no-photos: "写真はあらへん" + +common/views/widgets/posts-monitor.vue: + title: "投稿チャート" + toggle: "表示を切り替え" + +common/views/widgets/hashtags.vue: + title: "ハッシュタグ" + count: "{}人が投稿" + empty: "トレンドなし" + +common/views/widgets/server.vue: + title: "サーバー情報" + toggle: "表示を切り替え" + +common/views/widgets/memo.vue: + title: "付箋" + memo: "ここに書くねんで!" + save: "保存" + +common/views/widgets/slideshow.vue: + folder-customize-mode: "フォルダを指定するんやったら、カスタマイズモードを終了してや" + folder: "クリックしてフォルダを指定してや" + no-image: "このフォルダには画像があらへん" + +common/views/widgets/tips.vue: + tips-line1: "<kbd>t</kbd>でタイムラインにフォーカスできんで" + tips-line2: "<kbd>p</kbd>または<kbd>n</kbd>で投稿フォームを開くで" + tips-line3: "投稿フォームにはファイルをドラッグ&ドロップできんで" + tips-line4: "投稿フォームにクリップボードにおる画像データをペーストできんで" + tips-line5: "ドライブにファイルをドラッグ&ドロップしてアップロードできんで" + tips-line6: "ドライブでファイルをドラッグしてフォルダ移動できんで" + tips-line7: "ドライブでフォルダをドラッグしてフォルダ移動できんで" + tips-line8: "ホームは設定からカスタマイズできんで" + tips-line9: "MisskeyはAGPLv3やで" + tips-line10: "タイムマシンウィジェットを利用すると、簡単に過去のタイムラインに遡れんで" + tips-line11: "投稿の ... をクリックして、投稿をユーザーページにピン留めできんで" + tips-line13: "投稿に添付したファイルは全てドライブに保存されんで" + tips-line14: "ホームのカスタマイズ中、ウィジェットを右クリックしてデザインを変更できんで" + tips-line17: "「**」でテキストを囲ったると**強調表示**されんで" + tips-line19: "いくつかのウィンドウはブラウザの外に切り離すことができんで" + tips-line20: "カレンダーウィジェットのパーセンテージは、経過の割合を示してんねん" + tips-line21: "APIをつこてbotの開発なども行えんで" + tips-line23: "まゆかわいいよまゆ" + tips-line24: "Misskeyは2014年にサービスを開始してん" + tips-line25: "対応ブラウザやったらMisskeyを開いとらんでも通知を受け取れんで" + +common/views/pages/follow.vue: + signed-in-as: "{}としてサインイン中" + following: "フォロー中" + follow: "フォロー" + request-pending: "フォロー許可待ち" + follow-request: "フォロー申請" + +desktop: + banner-crop-title: "バナーとして表示する部分を選択" + banner: "バナー" + uploading-banner: "新しいバナーをアップロードしとります" + banner-updated: "バナーを更新したで" + choose-banner: "バナーにする画像を選択" + avatar-crop-title: "アバターとして表示する部分を選択" + avatar: "アバター" + uploading-avatar: "新しいアバターをアップロードしとります" + avatar-updated: "アバターを更新したで" + choose-avatar: "アバターにする画像を選択" + +desktop/views/components/activity.chart.vue: + total: "Black ... Total" + notes: "Blue ... Notes" + replies: "Red ... Replies" + renotes: "Green ... Renotes" + +desktop/views/components/activity.vue: + title: "アクティビティ" + toggle: "表示を切り替え" + +desktop/views/components/calendar.vue: + title: "{1}年 {2}月" + prev: "前の月" + next: "次の月" + go: "クリックして時間遡行" + +desktop/views/components/choose-file-from-drive-window.vue: + choose-file: "ファイル選択中" + upload: "PCからドライブにファイルをアップロード" + cancel: "キャンセル" + ok: "決定" + choose-prompt: "ファイルを選択" + +desktop/views/components/choose-folder-from-drive-window.vue: + cancel: "キャンセル" + ok: "決定" + choose-prompt: "フォルダを選択" + +desktop/views/components/crop-window.vue: + skip: "クロップをスキップ" + cancel: "キャンセル" + ok: "決定" + +desktop/views/components/drive-window.vue: + used: "使用中" + drive: "ドライブ" + +desktop/views/components/drive.file.vue: + avatar: "アイコン" + banner: "バナー" + contextmenu: + rename: "名前を変更" + mark-as-sensitive: "閲覧注意に設定" + unmark-as-sensitive: "閲覧注意を解除" + copy-url: "URLをコピー" + download: "ダウンロード" + else-files: "その他..." + set-as-avatar: "アイコンに設定" + set-as-banner: "バナーに設定" + open-in-app: "アプリで開く" + add-app: "アプリを追加" + rename-file: "ファイル名の変更" + input-new-file-name: "新しいファイル名を入力してや" + copied: "コピー完了" + copied-url-to-clipboard: "URLをクリップボードにコピーしたで" + +desktop/views/components/drive.folder.vue: + unable-to-process: "操作を完了できん" + circular-reference-detected: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。" + unhandled-error: "不明なエラー" + contextmenu: + move-to-this-folder: "このフォルダへ移動" + show-in-new-window: "新しいウィンドウで表示" + rename: "名前を変更" + rename-folder: "フォルダ名の変更" + input-new-folder-name: "新しいフォルダ名を入力してや" + +desktop/views/components/drive.nav-folder.vue: + drive: "ドライブ" + +desktop/views/components/drive.vue: + search: "検索" + load-more: "もっと読み込む" + empty-draghover: "ドロップですか?いいですよ、ボクはカワイイですからね" + empty-drive: "ドライブには何もあらへん。" + empty-drive-description: "右クリックして「ファイルをアップロード」を選んだり、ファイルをドラッグ&ドロップすることでもアップロードできんねん。" + empty-folder: "このフォルダーは空や" + unable-to-process: "操作を完了できん" + circular-reference-detected: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。" + unhandled-error: "不明なエラー" + url-upload: "URLアップロード" + url-of-file: "アップロードしたいファイルのURL" + url-upload-requested: "アップロードをリクエストしたで" + may-take-time: "アップロードが完了するまで時間がかかるかも分からん、知らんけど。" + create-folder: "フォルダー作成" + folder-name: "フォルダー名" + contextmenu: + create-folder: "フォルダーを作成" + upload: "ファイルをアップロード" + url-upload: "URLからアップロード" + +desktop/views/components/media-image.vue: + sensitive: "閲覧注意" + click-to-show: "クリックして表示" + +desktop/views/components/media-video.vue: + sensitive: "閲覧注意" + click-to-show: "クリックして表示" + +desktop/views/components/follow-button.vue: + following: "フォロー中" + follow: "フォロー" + request-pending: "フォロー許可待ち" + follow-request: "フォロー申請" + +desktop/views/components/followers-window.vue: + followers: "{} のフォロワー" + +desktop/views/components/followers.vue: + empty: "フォロワーはおらんっぽいで、知らんけど。" + +desktop/views/components/following-window.vue: + following: "{} のフォロー" + +desktop/views/components/following.vue: + empty: "フォロー中のユーザーはおらんっぽいで、知らんけど。" + +desktop/views/components/friends-maker.vue: + title: "気になるユーザーをフォロー:" + empty: "おすすめのユーザーはおらんかったわ。" + fetching: "読み込んどります" + refresh: "もっと見る" + close: "閉じる" + +desktop/views/components/game-window.vue: + game: "リバーシ" + +desktop/views/components/home.vue: + done: "完了" + add-widget: "ウィジェットを追加:" + add: "追加" + +desktop/views/input-dialog.vue: + cancel: "キャンセル" + ok: "決定" + +desktop/views/components/messaging-room-window.vue: + title: "メッセージ:" + +desktop/views/components/messaging-window.vue: + title: "メッセージ" + +desktop/views/components/note-detail.vue: + more: "会話をもっと読み込む" + private: "この投稿は非公開やねん" + deleted: "この投稿は削除されてん" + reposted-by: "{}がRenote" + location: "位置情報" + renote: "Renote" + add-reaction: "リアクション" + +desktop/views/components/notes.note.vue: + reposted-by: "{}がRenote" + reply: "返信" + renote: "Renote" + add-reaction: "リアクション" + detail: "詳細" + private: "この投稿は非公開やねん" + deleted: "この投稿は削除されてん" + hide: "隠す" + see-more: "もっと見る" + +desktop/views/components/notes.vue: + error: "読み込みに失敗したわ。" + retry: "リトライ" + load-more: "もっと読み込む" + +desktop/views/components/notifications.vue: + more: "もっと見る" + empty: "あらへん!" + +desktop/views/components/post-form.vue: + add-visible-user: "+ユーザーを追加" + attach-location-information: "位置情報を添付する" + hide-contents: "内容を隠す" + reply-placeholder: "この投稿への返信..." + quote-placeholder: "この投稿を引用..." + submit: "投稿" + reply: "返信" + renote: "Renote" + posted: "投稿したで!" + replied: "返信したで!" + reposted: "Renoteしたで!" + note-failed: "投稿に失敗したで" + reply-failed: "返信に失敗したで" + renote-failed: "Renoteに失敗したで" + posting: "投稿中" + attach-media-from-local: "PCからメディアを添付" + attach-media-from-drive: "ドライブからメディアを添付" + attach-cancel: "添付取り消し" + insert-a-kao: "v('ω')v" + create-poll: "アンケートを作成" + text-remain: "残り{}文字" + recent-tags: "最近" + click-to-tagging: "クリックでタグ付け" + visibility: "公開範囲" + geolocation-alert: "つこてる端末は位置情報に対応しとらんみたいやわ、知らんけど。" + error: "エラー" + enter-username: "ユーザー名を入力してや" + annotations: "内容への注釈 (オプション)" + +desktop/views/components/post-form-window.vue: + note: "新規投稿" + reply: "返信" + attaches: "添付: {}メディア" + uploading-media: "{}個のメディアをアップロード中" + +desktop/views/components/progress-dialog.vue: + waiting: "待機中" + +desktop/views/components/renote-form.vue: + quote: "引用する..." + cancel: "やっぱやめ" + renote: "Renote" + reposting: "やっとります..." + success: "Renoteしたで!" + failure: "Renoteに失敗したで" + +desktop/views/components/renote-form-window.vue: + title: "この投稿をRenoteしてもええか?" + +desktop/views/components/settings-window.vue: + settings: "設定" + +desktop/views/components/settings.vue: + profile: "プロフィール" + notification: "通知" + apps: "アプリ" + mute: "ミュート" + drive: "ドライブ" + security: "セキュリティ" + signin: "サインイン履歴" + password: "パスワード" + 2fa: "二段階認証" + other: "その他" + license: "ライセンス" + + behaviour: "動作" + fetch-on-scroll: "スクロールで自動読み込み" + fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込むで。" + auto-popout: "ウィンドウの自動ポップアウト" + auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトすんで。この設定はブラウザに記憶されんで。" + advanced: "詳細設定" + api-via-stream: "ストリームを経由したAPIリクエスト" + api-via-stream-desc: "この設定をオンにすると、WebSocket接続を経由してAPIリクエストが行われんで(パフォーマンス向上が期待できるかも分からん、知らんけど)。オフにすると、ネイティブの fetch API が利用されんねやわ。この設定はこのデバイスのみ有効やで。" + + display: "デザインと表示" + customize: "ホームをカスタマイズ" + choose-wallpaper: "壁紙を選択" + delete-wallpaper: "壁紙を削除" + dark-mode: "ダークモード" + circle-icons: "円形のアイコンを使用" + gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用" + post-form-on-timeline: "タイムライン上部に投稿フォームを表示すんで" + suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示すんで" + show-reply-target: "リプライ先を表示すんで" + show-my-renotes: "自分の行ったRenoteをタイムラインに表示すんで" + show-renoted-my-notes: "自分の投稿のRenoteをタイムラインに表示すんで" + show-local-renotes: "ローカルの投稿のRenoteをタイムラインに表示すんで" + show-maps: "マップの自動展開" + show-maps-desc: "位置情報が添付された投稿のマップを自動的に展開すんで。" + + sound: "サウンド" + enable-sounds: "サウンドを有効にすんで" + enable-sounds-desc: "投稿やメッセージを送受信したときなどにサウンドを再生すんで。この設定はブラウザに記憶されんで。" + volume: "ボリューム" + test: "テスト" + + mobile: "モバイル" + disable-via-mobile: "「モバイルからの投稿」フラグを付けへん" + + language: "言語" + pick-language: "言語を選択" + recommended: "推奨" + auto: "自動" + specify-language: "言語を指定" + language-desc: "変更はページの再度読み込み後に反映されんで。" + + cache: "キャッシュ" + clean-cache: "クリーンアップ" + cache-warn: "クリーンアップを行うと、ブラウザに記憶されたアカウント情報のキャッシュ、書きかけの投稿・返信・メッセージ、およびその他のデータ(設定情報含む)が削除されますねん。クリーンアップを行った後はページを再度読み込みしてや。" + cache-cleared: "キャッシュを削除したで" + cache-cleared-desc: "ページを再度読み込みしてや。" + + auto-watch: "投稿の自動ウォッチ" + auto-watch-desc: "リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにすんで。" + + about: "Misskeyについて" + operator: "このサーバーの運営者" + + update: "Misskey Update" + version: "バージョン:" + latest-version: "最新のバージョン:" + update-checking: "アップデートを確認中" + do-update: "アップデートを確認" + update-settings: "詳細設定" + prevent-update: "アップデートを延期する(非推奨)" + prevent-update-desc: "この設定をオンにしとってもアップデートが反映される場合があるかも分からん、知らんけど気ぃつけてや。この設定はこのデバイスのみ有効やで。" + no-updates: "利用可能な更新はあらへん" + no-updates-desc: "つことるMisskeyは最新や!" + update-available: "新しいバージョンが利用可能や" + update-available-desc: "ページを再度読み込みすると更新が適用されんで。" + + advanced-settings: "高度な設定" + debug-mode: "デバッグモードを有効にすんで" + debug-mode-desc: "この設定はブラウザに記憶されんで。" + experimental: "実験的機能を有効にすんで" + experimental-desc: "実験的機能を有効にするとMisskeyの動作が不安定になるかも分からん、知らんけど気ぃつけてや。この設定はブラウザに記憶されんで。" + tools: "ツール" + task-manager: "タスクマネージャ" + third-parties: "サードパーティ" + +desktop/views/components/settings.2fa.vue: + intro: "二段階認証を設定すると、サインイン時にパスワードだけとちゃうくて、予め登録しておいた物理的なデバイス(例えばあんさんのスマートフォンなど)も必要になり、よりセキュリティが向上すんで。" + detail: "詳細..." + url: "https://www.google.co.jp/intl/ja/landing/2step/" + caution: "登録したデバイスを紛失するなどした場合、Misskeyにサインインできんくなるから気ぃつけてや。" + register: "デバイスを登録する" + already-registered: "既に設定は完了してまんねん。" + unregister: "設定を解除" + unregistered: "二段階認証が無効になりはったわ。" + enter-password: "パスワードを入力してや" + authenticator: "まず、Google Authenticatorをつこてるデバイスにインストールするやろ:" + howtoinstall: "インストール方法はこちら" + scan: "ほんなら、表示されているQRコードをスキャンしてな:" + done: "つこてるデバイスに表示されとるトークンを入力して完了や!:" + submit: "完了" + success: "設定が完了したで!" + failed: "設定に失敗したで。トークンを間違えとらんか確認してや。" + info: "次回サインインからは、同様にパスワードに加えてデバイスに表示されとるトークンを入力してな。" + +desktop/views/components/settings.api.vue: + intro: "APIを利用するには、上記のトークンを「i」っちゅうキーでパラメータに付加してリクエストしてや。" + caution: "アカウントを不正利用されるかも知れんから、このトークンは第三者に教えたらあきまへん(アプリなどにも入力しんといてな)。" + regeneration-of-token: "万が一このトークンが漏れたりその可能性がある場合はトークンを再生成できんで。" + regenerate-token: "トークンを再生成" + token: "Token:" + enter-password: "パスワードを入力してや" + +desktop/views/components/settings.apps.vue: + no-apps: "連携しているアプリケーションはあらへん" + +desktop/views/components/settings.drive.vue: + max: "中" + in-use: "使用中" + +desktop/views/components/settings.mute.vue: + no-users: "ミュートしているユーザーはおらん" + +desktop/views/components/settings.password.vue: + reset: "パスワードを変更する" + enter-current-password: "現在のパスワードを入力してや" + enter-new-password: "新しいパスワードを入力してや" + enter-new-password-again: "もう一度新しいパスワードを入力してや" + not-match: "新しいパスワードが一致しとらん" + changed: "パスワードを変更したで" + +desktop/views/components/settings.profile.vue: + avatar: "アイコン" + choice-avatar: "画像を選択" + name: "名前" + location: "場所" + description: "自己紹介" + birthday: "誕生日" + save: "保存" + locked-account: "アカウントの保護" + is-locked: "投稿を非公開にする" + other: "その他" + is-bot: "このアカウントはBotやで" + is-cat: "このアカウントはCatやで" + profile-updated: "プロフィールを更新したで" + +desktop/views/components/sub-note-content.vue: + private: "この投稿は非公開やねん" + deleted: "この投稿は削除されてん" + media-count: "{}つのメディア" + poll: "アンケート" + +desktop/views/components/taskmanager.vue: + title: "タスクマネージャ" + +desktop/views/components/timeline.vue: + home: "ホーム" + local: "ローカル" + hybrid: "ソーシャル" + global: "グローバル" + list: "リスト" + +desktop/views/components/ui.header.vue: + welcome-back: "おかいり、" + adjective: "さん" + +desktop/views/components/ui.header.account.vue: + profile: "プロフィール" + drive: "ドライブ" + favorites: "お気に入り" + lists: "リスト" + follow-requests: "フォロー申請" + customize: "ホームのカスタマイズ" + settings: "設定" + signout: "サインアウト" + dark: "ナイトゲームじゃ!" + +desktop/views/components/ui.header.nav.vue: + home: "ホーム" + deck: "デッキ" + messaging: "メッセージ" + game: "ゲーム" + +desktop/views/components/ui.header.notifications.vue: + title: "通知" + +desktop/views/components/ui.header.post.vue: + post: "新規投稿" + +desktop/views/components/ui.header.search.vue: + placeholder: "検索" + +desktop/views/components/received-follow-requests-window.vue: + title: "フォロー申請" + accept: "承認" + reject: "拒否" + +desktop/views/components/user-lists-window.vue: + title: "リスト" + create-list: "リストを作成" + list-name: "リスト名" + +desktop/views/components/user-preview.vue: + notes: "投稿" + following: "フォロー" + followers: "フォロワー" + +desktop/views/components/users-list.vue: + all: "すべて" + iknow: "知り合い" + load-more: "もっと" + fetching: "読み込んどります" + +desktop/views/components/users-list-item.vue: + followed: "フォローされとります" + +desktop/views/components/window.vue: + popout: "ポップアウト" + close: "ばいなら" + +desktop/views/pages/admin/admin.vue: + dashboard: "ダッシュボード" + drive: "ドライブ" + users: "ユーザー" + update: "更新" + +desktop/views/pages/admin/admin.dashboard.vue: + dashboard: "ダッシュボード" + all-users: "全てのユーザー" + original-users: "このインスタンスのユーザー" + all-notes: "全てのノート" + original-notes: "このインスタンスのノート" + invite: "招待" + +desktop/views/pages/admin/admin.suspend-user.vue: + suspend-user: "ユーザーの凍結" + suspend: "凍結" + suspended: "凍結したで" + +desktop/views/pages/admin/admin.unsuspend-user.vue: + unsuspend-user: "ユーザーの凍結の解除" + unsuspend: "凍結の解除" + unsuspended: "凍結を解除したで" + +desktop/views/pages/admin/admin.verify-user.vue: + verify-user: "ユーザーの公式アカウント設定" + verify: "公式アカウントにする" + verified: "公式アカウントにしたで" + +desktop/views/pages/admin/admin.unverify-user.vue: + unverify-user: "ユーザーの公式アカウント解除" + unverify: "公式アカウントを解除する" + 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/deck/deck.tl-column.vue: + is-media-only: "メディア投稿のみ" + is-media-view: "メディアビュー" + edit: "オプション" + +desktop/views/pages/deck/deck.note.vue: + reposted-by: "{}がRenote" + private: "この投稿は非公開やねん" + deleted: "この投稿は削除されてん" + +desktop/views/pages/welcome.vue: + about: "詳しく..." + gotit: "ほい" + signin: "ログイン" + signup: "新規登録" + signin-button: "やっとる" + signup-button: "やる" + timeline: "タイムライン" + powered-by-misskey: "Powered by <b>Misskey</b>." + +desktop/views/pages/drive.vue: + title: "Misskey Drive" + +desktop/views/pages/favorites.vue: + more: "さらに読み込む" + +desktop/views/pages/home-customize.vue: + title: "ホームのカスタマイズ" + +desktop/views/pages/note.vue: + prev: "前の投稿" + next: "次の投稿" + +desktop/views/pages/selectdrive.vue: + title: "ファイルを選択してや" + ok: "決定" + cancel: "キャンセル" + upload: "PCからドライブにファイルをアップロード" + +desktop/views/pages/search.vue: + not-available: "検索機能を利用することができへん。" + not-found: "「{}」に関する投稿はあらへん。" + +desktop/views/pages/share.vue: + share-with: "{}で共有" + +desktop/views/pages/tag.vue: + no-posts-found: "ハッシュタグ「{}」が付けられた投稿はあらへん。" + +desktop/views/pages/user-list.users.vue: + users: "ユーザー" + add-user: "ユーザーを追加" + username: "ユーザー名" + +desktop/views/pages/user/user.followers-you-know.vue: + title: "知り合いのフォロワー" + loading: "読み込み中" + no-users: "知り合いのフォロワーはおらん" + +desktop/views/pages/user/user.friends.vue: + title: "よく話すユーザー" + loading: "読み込み中" + no-users: "よく話すユーザーはおらん" + +desktop/views/pages/user/user.vue: + is-suspended: "このユーザーは凍結されとります。" + is-remote: "このユーザーはリモートユーザーや。" + view-remote: "正確な情報を見る" + +desktop/views/pages/user/user.home.vue: + last-used-at: "最終アクセス" + +desktop/views/pages/user/user.photos.vue: + title: "フォト" + loading: "読み込み中" + no-photos: "写真はあらへん" + +desktop/views/pages/user/user.profile.vue: + follows-you: "フォローされとります" + stalk: "ストークする" + stalking: "ストーキングしとります" + unstalk: "ストーク解除" + mute: "ミュートする" + muted: "ミュートしとります" + unmute: "ミュート解除" + push-to-a-list: "リストに追加" + list-pushed: "{user}を{list}に追加したで。" + +desktop/views/pages/user/user.header.vue: + posts: "投稿" + following: "フォロー" + followers: "フォロワー" + is-bot: "このアカウントはBotや" + +desktop/views/pages/user/user.timeline.vue: + default: "投稿" + with-replies: "投稿と返信" + with-media: "メディア" + empty: "このユーザーはまだ何も投稿しとらんようや。" + +desktop/views/widgets/messaging.vue: + title: "メッセージ" + +desktop/views/widgets/notifications.vue: + title: "通知" + settings: "通知の設定" + +desktop/views/widgets/polls.vue: + title: "アンケート" + refresh: "他を見る" + nothing: "あらへん!" + +desktop/views/widgets/post-form.vue: + title: "投稿" + note: "投稿" + +desktop/views/widgets/profile.vue: + update-banner: "クリックでバナー編集" + update-avatar: "クリックでアバター編集" + +desktop/views/widgets/trends.vue: + title: "トレンド" + refresh: "他を見る" + nothing: "あらへん!" + +desktop/views/widgets/users.vue: + title: "おすすめユーザー" + refresh: "他を見る" + no-one: "おらん!" + +mobile/views/components/drive.vue: + drive: "ドライブ" + used: "使用中" + folder-count: "フォルダ" + count-separator: "、" + file-count: "ファイル" + load-more: "もっと読み込む" + nothing-in-drive: "ドライブには何もあらへん" + folder-is-empty: "このフォルダは空や" + prompt: "何すんの?(数字を入力してや): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>" + deletion-alert: "フォルダの削除は未実装やねん...。堪忍な!" + folder-name: "フォルダー名" + root-rename-alert: "現在おる場所はルートで、フォルダとちゃうから名前の変更はできへん。名前を変更したいフォルダに移動してからやってな。" + root-move-alert: "現在おる場所はルートで、フォルダとちゃうから移動はできへん。移動したいフォルダに移動してからやってな。" + url-prompt: "アップロードしたいファイルのURL" + uploading: "アップロードをリクエストしたで。アップロードが完了するまで時間がかかるかも分からん、知らんけど。" + +mobile/views/components/drive-file-detail.vue: + rename: "名前を変更" + +mobile/views/components/drive-file-chooser.vue: + select-file: "ファイルを選択" + +mobile/views/components/drive-folder-chooser.vue: + select-folder: "フォルダーを選択" + +mobile/views/components/drive.file-detail.vue: + download: "ダウンロード" + rename: "名前を変更" + move: "移動" + hash: "ハッシュ (md5)" + exif: "EXIF" + +mobile/views/components/media-image.vue: + sensitive: "閲覧注意" + click-to-show: "クリックして表示" + +mobile/views/components/media-video.vue: + sensitive: "閲覧注意" + click-to-show: "クリックして表示" + +mobile/views/components/follow-button.vue: + following: "フォロー中" + follow: "フォロー" + request-pending: "フォロー許可待ち" + follow-request: "フォロー申請" + +mobile/views/components/friends-maker.vue: + title: "気になるユーザーをフォロー" + empty: "おすすめのユーザーはおらん。" + fetching: "読み込んどります" + refresh: "もっと見る" + close: "閉じる" + +mobile/views/components/note.vue: + reposted-by: "{}がRenote" + more: "もっと見る" + less: "隠す" + private: "この投稿は非公開やねん" + deleted: "この投稿は削除されてん" + location: "位置情報" + +mobile/views/components/note-detail.vue: + reply: "返信" + reaction: "リアクション" + reposted-by: "{}がRenote" + private: "この投稿は非公開やねん" + deleted: "この投稿は削除されてん" + location: "位置情報" + +mobile/views/components/note-preview.vue: + admin: "admin" + bot: "bot" + cat: "cat" + +mobile/views/components/note-sub.vue: + admin: "admin" + bot: "bot" + cat: "cat" + +mobile/views/components/notes.vue: + failed: "読み込みに失敗したで。" + retry: "もっぺん" + +mobile/views/components/notifications.vue: + more: "もっと見る" + empty: "あらへん!" + +mobile/views/components/post-form.vue: + add-visible-user: "ユーザーを追加" + submit: "投稿" + reply: "返信" + renote: "Renote" + quote-placeholder: "この投稿を引用... (オプション)" + reply-placeholder: "この投稿への返信..." + cw-placeholder: "内容への注釈 (オプション)" + location-alert: "つこてる端末は位置情報に対応しとらんみたいやわ、知らんけど。" + error: "エラー" + username-prompt: "ユーザー名を入力してや" + +mobile/views/components/sub-note-content.vue: + private: "この投稿は非公開やねん" + deleted: "この投稿は削除されてん" + media-count: "{}つのメディア" + poll: "アンケート" + +mobile/views/components/timeline.vue: + empty: "投稿がありません" + load-more: "もっと" + +mobile/views/components/ui.header.vue: + welcome-back: "おかいり、" + adjective: "さん" + +mobile/views/components/ui.nav.vue: + timeline: "タイムライン" + notifications: "通知" + messaging: "メッセージ" + follow-requests: "フォロー申請" + search: "検索" + drive: "ドライブ" + favorites: "お気に入り" + user-lists: "リスト" + widgets: "ウィジェット" + game: "ゲーム" + darkmode: "ダークモード" + settings: "設定" + about: "Misskeyについて" + +mobile/views/components/user-timeline.vue: + no-notes: "このユーザーは投稿しとらんようや。" + no-notes-with-media: "メディア付き投稿はあらへん。" + load-more: "もっと" + +mobile/views/components/users-list.vue: + all: "すべて" + known: "知り合い" + load-more: "もっと" + +mobile/views/pages/favorites.vue: + title: "お気に入り" + +mobile/views/pages/user-lists.vue: + title: "リスト" + enter-list-name: "リスト名を入力してや" + +mobile/views/pages/drive.vue: + drive: "ドライブ" + more: "もっと見る" + +mobile/views/pages/signup.vue: + lets-start: "📦 始めようや" + +mobile/views/pages/followers.vue: + followers-of: "{}のフォロワー" + +mobile/views/pages/following.vue: + following-of: "{}のフォロー" + +mobile/views/pages/home.vue: + home: "ホーム" + local: "ローカル" + hybrid: "ソーシャル" + global: "グローバル" + +mobile/views/pages/tag.vue: + no-posts-found: "ハッシュタグ「{}」が付けられた投稿はあらへん。" + +mobile/views/pages/welcome.vue: + signup: "新規登録" + +mobile/views/pages/widgets.vue: + dashboard: "ダッシュボード" + widgets-hints: "ウィジェットを追加/削除したり並べ替えたりできんで。ウィジェットを移動するんやったら「三」をドラッグしてや。ウィジェットを削除するんやったら「x」をタップしてや。いくつかのウィジェットはタップしたったら表示を変更できるかも分からん、知らんけど。" + add-widget: "追加" + customization-tips: "カスタマイズのヒント" + +mobile/views/pages/widgets/activity.vue: + activity: "アクティビティ" + +mobile/views/pages/share.vue: + share-with: "{}で共有" + +mobile/views/pages/messaging.vue: + messaging: "メッセージ" + +mobile/views/pages/messaging-room.vue: + messaging: "メッセージ" + +mobile/views/pages/received-follow-requests.vue: + title: "フォロー申請" + accept: "承認" + reject: "拒否" + +mobile/views/pages/note.vue: + title: "投稿" + prev: "前の投稿" + next: "次の投稿" + +mobile/views/pages/notifications.vue: + notifications: "通知" + read-all: "すべての通知を既読にしてもええか?" + +mobile/views/pages/games/reversi.vue: + reversi: "リバーシ" + +mobile/views/pages/settings/settings.profile.vue: + title: "プロフィール" + name: "名前" + account: "アカウント" + location: "場所" + description: "自己紹介" + birthday: "誕生日" + avatar: "アイコン" + banner: "バナー" + is-cat: "このアカウントはCatや" + save: "保存" + saved: "プロフィールを保存したで" + uploading: "アップロード中" + upload-failed: "アップロードに失敗したで" + +mobile/views/pages/search.vue: + search: "検索" + empty: "「{}」に関する投稿はあらへん。" + not-found: "「{}」に関する投稿はあらへん。" + +mobile/views/pages/selectdrive.vue: + select-file: "ファイルを選択" + +mobile/views/pages/settings.vue: + signed-in-as: "{}としてサインイン中" + lang: "言語" + lang-tip: "変更はページの再読み込み後に反映されんで。" + recommended: "推奨" + auto: "自動" + specify-language: "言語を指定" + design: "デザインと表示" + dark-mode: "ダークモード" + i-am-under-limited-internet: "私は通信を制限されてんねん" + circle-icons: "円形のアイコンを使用" + timeline: "タイムライン" + show-reply-target: "リプライ先を表示すんで" + show-my-renotes: "自分の行ったRenoteを表示すんで" + show-renoted-my-notes: "自分の投稿のRenoteを表示すんで" + show-local-renotes: "ローカルの投稿のRenoteを表示すんで" + post-style: "投稿の表示スタイル" + post-style-standard: "標準" + post-style-smart: "スマート" + behavior: "動作" + fetch-on-scroll: "スクロールで自動読み込み" + disable-via-mobile: "「モバイルからの投稿」フラグを付けへん" + load-raw-images: "添付された画像を高画質で表示すんで" + load-remote-media: "リモートサーバーのメディアを表示すんで" + twitter: "Twitter連携" + twitter-connect: "Twitterアカウントに接続する" + twitter-reconnect: "再接続する" + twitter-disconnect: "切断する" + update: "Misskey Update" + version: "バージョン:" + latest-version: "最新のバージョン:" + update-checking: "アップデートを確認中" + check-for-updates: "アップデートを確認" + no-updates: "利用可能な更新はあらへん" + no-updates-desc: "つこてるMisskeyは最新や!" + update-available: "新しいバージョンが利用可能や" + update-available-desc: "ページを再度読み込みすると更新が適用されんで。" + settings: "設定" + signout: "サインアウト" + +mobile/views/pages/user.vue: + follows-you: "フォローされとります" + following: "フォロー" + followers: "フォロワー" + notes: "投稿" + overview: "概要" + timeline: "タイムライン" + media: "メディア" + is-suspended: "このユーザーは凍結されとります。" + is-remote: "このユーザーはリモートユーザーや。" + view-remote: "正確な情報を見る" + +mobile/views/pages/user/home.vue: + recent-notes: "最近の投稿" + images: "画像" + activity: "アクティビティ" + keywords: "キーワード" + domains: "頻出ドメイン" + frequently-replied-users: "よく会話するユーザー" + followers-you-know: "知り合いのフォロワー" + last-used-at: "最終ログイン" + +mobile/views/pages/user/home.followers-you-know.vue: + loading: "読み込み中" + no-users: "知り合いのユーザーはおらん" + +mobile/views/pages/user/home.friends.vue: + loading: "読み込み中" + no-users: "よく会話するユーザーはおらん" + +mobile/views/pages/user/home.notes.vue: + loading: "読み込み中" + no-notes: "投稿はありません" + +mobile/views/pages/user/home.photos.vue: + loading: "読み込み中" + no-photos: "写真はありません" + +docs: + edit-this-page-on-github: "間違いや改善点を見つけたん?" + edit-this-page-on-github-link: "このページをGitHubで編集" + + api: + entities: + properties: "プロパティ" + endpoints: + params: "パラメータ" + no-params: "パラメータはありません" + res: "レスポンス" + require-credential: "このエンドポイントは認証情報が必須や。" + require-permission: "このエンドポイントは{permission}の権限が必要や。" + has-limit: "レートリミットがあんで。" + duration-limit: "直近{duration}ミリ秒の間のこのエンドポイントへのリクエスト数の合計が{max}を超えてもうたらリクエストできへん。" + min-interval-limit: "前回のリクエストから{interval}ミリ秒経っとらんとリクエストできへん。" + show-src: "このエンドポイントのソースコードも閲覧できんで。" + show-src-link: "コードをGitHubで見る" + generated: "このドキュメントはAPI定義に基づき自動生成されてんねん。" + props: + name: "名前" + type: "型" + description: "説明" + + +dev/views/index.vue: + manage-apps: "アプリの管理" diff --git a/package.json b/package.json index 3d8df6aaee..ff5eeae3f5 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "misskey", "author": "syuilo <i@syuilo.com>", - "version": "6.2.0", - "clientVersion": "1.0.8417", + "version": "7.0.0", + "clientVersion": "1.0.8520", "codename": "nighthike", "main": "./built/index.js", "private": true, @@ -201,7 +201,7 @@ "typescript": "2.9.2", "typescript-eslint-parser": "18.0.0", "uglify-es": "3.3.9", - "url-loader": "1.1.0", + "url-loader": "1.1.1", "uuid": "3.3.2", "v-animate-css": "0.0.2", "vue": "2.5.17", diff --git a/src/client/app/common/scripts/get-face.ts b/src/client/app/common/scripts/get-face.ts new file mode 100644 index 0000000000..79cf7a1be4 --- /dev/null +++ b/src/client/app/common/scripts/get-face.ts @@ -0,0 +1,10 @@ +const faces = [ + '(=^・・^=)', + 'v(\'ω\')v', + '🐡( \'-\' 🐡 )フグパンチ!!!!', + '🖕(´・_・`)🖕', + '(。>﹏<。)', + '(Δ・x・Δ)' +]; + +export default () => faces[Math.floor(Math.random() * faces.length)]; diff --git a/src/client/app/common/scripts/get-kao.ts b/src/client/app/common/scripts/get-kao.ts deleted file mode 100644 index ca83153b96..0000000000 --- a/src/client/app/common/scripts/get-kao.ts +++ /dev/null @@ -1,9 +0,0 @@ -const kaos = [ - '(=^・・^=)', - 'v(\'ω\')v', - '🐡( \'-\' 🐡 )フグパンチ!!!!', - '🖕(´・_・`)🖕', - '(。>﹏<。)' -]; - -export default () => kaos[Math.floor(Math.random() * kaos.length)]; diff --git a/src/client/app/common/views/components/games/reversi/reversi.index.vue b/src/client/app/common/views/components/games/reversi/reversi.index.vue index d4d35f6a86..fa88aeaaf4 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.index.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.index.vue @@ -32,6 +32,7 @@ <mk-avatar class="avatar" :user="g.user2"/> <span><b>{{ g.user1 | userName }}</b> vs <b>{{ g.user2 | userName }}</b></span> <span class="state">{{ g.isEnded ? '%i18n:@game-state.ended%' : '%i18n:@game-state.playing%' }}</span> + <mk-time :time="g.createdAt" /> </a> </section> <section v-if="games.length > 0"> @@ -41,6 +42,7 @@ <mk-avatar class="avatar" :user="g.user2"/> <span><b>{{ g.user1 | userName }}</b> vs <b>{{ g.user2 | userName }}</b></span> <span class="state">{{ g.isEnded ? '%i18n:@game-state.ended%' : '%i18n:@game-state.playing%' }}</span> + <mk-time :time="g.createdAt" /> </a> </section> </div> diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue index 45a183e144..1d33702159 100644 --- a/src/client/app/common/views/components/signup.vue +++ b/src/client/app/common/views/components/signup.vue @@ -1,5 +1,10 @@ <template> <form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()"> + <ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required> + <span>%i18n:@invitation-code%</span> + <span slot="prefix">%fa:id-card-alt%</span> + <p slot="text" v-html="'%i18n:@invitation-info%'.replace('{}', meta.maintainer.url)"></p> + </ui-input> <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername"> <span>%i18n:@username%</span> <span slot="prefix">@</span> @@ -46,11 +51,13 @@ export default Vue.extend({ username: '', password: '', retypedPassword: '', + invitationCode: '', url, recaptchaSitekey, usernameState: null, passwordStrength: '', - passwordRetypeState: null + passwordRetypeState: null, + meta: null } }, computed: { @@ -61,6 +68,11 @@ export default Vue.extend({ this.usernameState != 'max-range'); } }, + created() { + (this as any).os.getMeta().then(meta => { + this.meta = meta; + }); + }, methods: { onChangeUsername() { if (this.username == '') { @@ -110,6 +122,7 @@ export default Vue.extend({ (this as any).api('signup', { username: this.username, password: this.password, + invitationCode: this.invitationCode, 'g-recaptcha-response': recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null }).then(() => { (this as any).api('signin', { diff --git a/src/client/app/common/views/components/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue index cc9c75095e..4691604e57 100644 --- a/src/client/app/common/views/components/visibility-chooser.vue +++ b/src/client/app/common/views/components/visibility-chooser.vue @@ -44,7 +44,12 @@ import Vue from 'vue'; import * as anime from 'animejs'; export default Vue.extend({ - props: ['source', 'compact', 'v'], + props: ['source', 'compact'], + data() { + return { + v: this.$store.state.device.visibility || 'public' + } + }, mounted() { this.$nextTick(() => { const popover = this.$refs.popover as any; @@ -92,6 +97,7 @@ export default Vue.extend({ }, methods: { choose(visibility) { + this.$store.commit('device/setVisibility', visibility); this.$emit('chosen', visibility); this.$destroy(); }, diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue index ea51144173..bacaea65ee 100644 --- a/src/client/app/desktop/views/components/post-form.vue +++ b/src/client/app/desktop/views/components/post-form.vue @@ -58,7 +58,7 @@ import Vue from 'vue'; import insertTextAtCursor from 'insert-text-at-cursor'; import * as XDraggable from 'vuedraggable'; -import getKao from '../../../common/scripts/get-kao'; +import getFace from '../../../common/scripts/get-face'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import parse from '../../../../../mfm/parse'; import { host } from '../../../config'; @@ -99,7 +99,7 @@ export default Vue.extend({ useCw: false, cw: null, geo: null, - visibility: 'public', + visibility: this.$store.state.device.visibility || 'public', visibleUsers: [], autocomplete: null, draghover: false, @@ -326,8 +326,7 @@ export default Vue.extend({ setVisibility() { const w = (this as any).os.new(MkVisibilityChooser, { - source: this.$refs.visibilityButton, - v: this.visibility + source: this.$refs.visibilityButton }); w.$once('chosen', v => { this.visibility = v; @@ -422,7 +421,7 @@ export default Vue.extend({ }, kao() { - this.text += getKao(); + this.text += getFace(); } } }); diff --git a/src/client/app/desktop/views/pages/admin/admin.cpu-memory.vue b/src/client/app/desktop/views/pages/admin/admin.cpu-memory.vue new file mode 100644 index 0000000000..572974e248 --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.cpu-memory.vue @@ -0,0 +1,145 @@ +<template> +<div class="zyknedwtlthezamcjlolyusmipqmjgxz"> + <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> + <defs> + <linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0"> + <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> + <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> + </linearGradient> + <mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> + <polygon + :points="cpuPolygonPoints" + fill="#fff" + fill-opacity="0.5"/> + <polyline + :points="cpuPolylinePoints" + fill="none" + stroke="#fff" + stroke-width="0.3"/> + </mask> + </defs> + <rect + x="0" y="0" + :width="viewBoxX" :height="viewBoxY" + :style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"/> + <text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text> + </svg> + <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> + <defs> + <linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0"> + <stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop> + <stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop> + </linearGradient> + <mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> + <polygon + :points="memPolygonPoints" + fill="#fff" + fill-opacity="0.5"/> + <polyline + :points="memPolylinePoints" + fill="none" + stroke="#fff" + stroke-width="0.3"/> + </mask> + </defs> + <rect + x="0" y="0" + :width="viewBoxX" :height="viewBoxY" + :style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"/> + <text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text> + </svg> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as uuid from 'uuid'; + +export default Vue.extend({ + props: ['connection'], + data() { + return { + viewBoxX: 50, + viewBoxY: 20, + stats: [], + cpuGradientId: uuid(), + cpuMaskId: uuid(), + memGradientId: uuid(), + memMaskId: uuid(), + cpuPolylinePoints: '', + memPolylinePoints: '', + cpuPolygonPoints: '', + memPolygonPoints: '', + cpuP: '', + memP: '' + }; + }, + mounted() { + this.connection.on('stats', this.onStats); + this.connection.on('statsLog', this.onStatsLog); + this.connection.send({ + type: 'requestLog', + id: Math.random().toString() + }); + }, + beforeDestroy() { + this.connection.off('stats', this.onStats); + this.connection.off('statsLog', this.onStatsLog); + }, + methods: { + onStats(stats) { + this.stats.push(stats); + if (this.stats.length > 50) this.stats.shift(); + + const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu_usage) * this.viewBoxY]); + const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.used / s.mem.total)) * this.viewBoxY]); + this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); + this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' '); + + this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; + this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`; + + this.cpuP = (stats.cpu_usage * 100).toFixed(0); + this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0); + }, + onStatsLog(statsLog) { + statsLog.forEach(stats => this.onStats(stats)); + } + } +}); +</script> + +<style lang="stylus" scoped> +root(isDark) + margin-bottom 16px + + > svg + display block + width 50% + float left + + &:first-child + padding-right 5px + + &:last-child + padding-left 5px + + > text + font-size 2px + fill isDark ? rgba(#fff, 0.55) : rgba(#000, 0.55) + + > tspan + opacity 0.5 + + &:after + content "" + display block + clear both + +.zyknedwtlthezamcjlolyusmipqmjgxz[data-darkmode] + root(true) + +.zyknedwtlthezamcjlolyusmipqmjgxz:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue index b10e829965..e68d3a749e 100644 --- a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue +++ b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue @@ -1,37 +1,80 @@ <template> -<div> - <h1>%i18n:@dashboard%</h1> - <div v-if="stats"> - <p><b>%i18n:@all-users%</b>: <span>{{ stats.usersCount | number }}</span></p> - <p><b>%i18n:@original-users%</b>: <span>{{ stats.originalUsersCount | number }}</span></p> - <p><b>%i18n:@all-notes%</b>: <span>{{ stats.notesCount | number }}</span></p> - <p><b>%i18n:@original-notes%</b>: <span>{{ stats.originalNotesCount | number }}</span></p> +<div class="obdskegsannmntldydackcpzezagxqfy card"> + <header>%i18n:@dashboard%</header> + <div v-if="stats" class="stats"> + <div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div> + <div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div> + <div><b>%fa:pen% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div> + <div><span>%fa:pen% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div> + </div> + <div class="cpu-memory"> + <x-cpu-memory :connection="connection"/> + </div> + <div> + <button class="ui" @click="invite">%i18n:@invite%</button> + <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> </div> </div> </template> <script lang="ts"> import Vue from "vue"; +import XCpuMemory from "./admin.cpu-memory.vue"; export default Vue.extend({ + components: { + XCpuMemory + }, data() { return { - stats: null + stats: null, + inviteCode: null, + connection: null, + connectionId: null }; }, created() { + this.connection = (this as any).os.streams.serverStatsStream.getConnection(); + this.connectionId = (this as any).os.streams.serverStatsStream.use(); + (this as any).api('stats').then(stats => { this.stats = stats; }); + }, + beforeDestroy() { + (this as any).os.streams.serverStatsStream.dispose(this.connectionId); + }, + methods: { + invite() { + (this as any).api('admin/invite').then(x => { + this.inviteCode = x.code; + }); + } } }); </script> <style lang="stylus" scoped> -h1 - margin 0 0 1em 0 - padding 0 0 8px 0 - font-size 1em - color #555 - border-bottom solid 1px #eee +@import '~const.styl' + +.obdskegsannmntldydackcpzezagxqfy + > .stats + display flex + justify-content center + margin-bottom 16px + padding 16px + border solid 1px #eee + border-radius 8px + + > div + flex 1 + text-align center + + > *:first-child + display block + color $theme-color + + > *:last-child + font-size 70% + </style> diff --git a/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue new file mode 100644 index 0000000000..3c537d8d6d --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue @@ -0,0 +1,51 @@ +<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> diff --git a/src/client/app/desktop/views/pages/admin/admin.drive-chart.vue b/src/client/app/desktop/views/pages/admin/admin.drive-chart.vue new file mode 100644 index 0000000000..4f94fd2372 --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.drive-chart.vue @@ -0,0 +1,34 @@ +<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> diff --git a/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue new file mode 100644 index 0000000000..83c61c1313 --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue @@ -0,0 +1,76 @@ +<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> diff --git a/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue b/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue new file mode 100644 index 0000000000..e4d396d9c6 --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue @@ -0,0 +1,34 @@ +<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> diff --git a/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue b/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue index 6eb82f0a51..59932f4be7 100644 --- a/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue +++ b/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue @@ -1,5 +1,5 @@ <template> -<div> +<div class="card"> <header>%i18n:@suspend-user%</header> <input v-model="username" type="text" class="ui"/> <button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button> diff --git a/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue b/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue index 8c6f63ce88..a75c0bd64e 100644 --- a/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue +++ b/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue @@ -1,5 +1,5 @@ <template> -<div> +<div class="card"> <header>%i18n:@unsuspend-user%</header> <input v-model="username" type="text" class="ui"/> <button class="ui" @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</button> diff --git a/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue b/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue new file mode 100644 index 0000000000..72962870d9 --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue @@ -0,0 +1,51 @@ +<template> +<div class="card"> + <header>%i18n:@unverify-user%</header> + <input v-model="username" type="text" class="ui"/> + <button class="ui" @click="unverifyUser" :disabled="unverifying">%i18n:@unverify%</button> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; +import parseAcct from "../../../../../../misc/acct/parse"; + +export default Vue.extend({ + data() { + return { + username: null, + unverifying: false + }; + }, + methods: { + async unverifyUser() { + this.unverifying = true; + + const user = await (this as any).os.api( + "users/show", + parseAcct(this.username) + ); + + await (this as any).os.api("admin/unverify-user", { + userId: user.id + }); + + this.unverifying = false; + + (this as any).os.apis.dialog({ text: "%i18n:@unverified%" }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +header + margin 10px 0 + + +button + margin 16px 0 + +</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue new file mode 100644 index 0000000000..c2ab4a78e3 --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue @@ -0,0 +1,51 @@ +<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> diff --git a/src/client/app/desktop/views/pages/admin/admin.users-chart.vue b/src/client/app/desktop/views/pages/admin/admin.users-chart.vue new file mode 100644 index 0000000000..e620012702 --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.users-chart.vue @@ -0,0 +1,34 @@ +<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> diff --git a/src/client/app/desktop/views/pages/admin/admin.verify-user.vue b/src/client/app/desktop/views/pages/admin/admin.verify-user.vue new file mode 100644 index 0000000000..3902d4bddd --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.verify-user.vue @@ -0,0 +1,51 @@ +<template> +<div class="card"> + <header>%i18n:@verify-user%</header> + <input v-model="username" type="text" class="ui"/> + <button class="ui" @click="verifyUser" :disabled="verifying">%i18n:@verify%</button> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; +import parseAcct from "../../../../../../misc/acct/parse"; + +export default Vue.extend({ + data() { + return { + username: null, + verifying: false + }; + }, + methods: { + async verifyUser() { + this.verifying = true; + + const user = await (this as any).os.api( + "users/show", + parseAcct(this.username) + ); + + await (this as any).os.api("admin/verify-user", { + userId: user.id + }); + + this.verifying = false; + + (this as any).os.apis.dialog({ text: "%i18n:@verified%" }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +header + margin 10px 0 + + +button + margin 16px 0 + +</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.vue b/src/client/app/desktop/views/pages/admin/admin.vue index b581bea465..cbb1890cc3 100644 --- a/src/client/app/desktop/views/pages/admin/admin.vue +++ b/src/client/app/desktop/views/pages/admin/admin.vue @@ -9,12 +9,17 @@ </ul> </nav> <main> - <div v-if="page == 'dashboard'"> + <div v-show="page == 'dashboard'"> <x-dashboard/> + <x-users-chart :chart="chart"/> + <x-notes-chart :chart="chart"/> + <x-drive-chart :chart="chart"/> </div> <div v-if="page == 'users'"> <x-suspend-user/> <x-unsuspend-user/> + <x-verify-user/> + <x-unverify-user/> </div> <div v-if="page == 'drive'"></div> <div v-if="page == 'update'"></div> @@ -27,18 +32,34 @@ import Vue from "vue"; import XDashboard from "./admin.dashboard.vue"; import XSuspendUser from "./admin.suspend-user.vue"; import XUnsuspendUser from "./admin.unsuspend-user.vue"; +import XVerifyUser from "./admin.verify-user.vue"; +import XUnverifyUser from "./admin.unverify-user.vue"; +import XUsersChart from "./admin.users-chart.vue"; +import XNotesChart from "./admin.notes-chart.vue"; +import XDriveChart from "./admin.drive-chart.vue"; export default Vue.extend({ components: { XDashboard, XSuspendUser, - XUnsuspendUser + XUnsuspendUser, + XVerifyUser, + XUnverifyUser, + XUsersChart, + XNotesChart, + XDriveChart }, data() { return { - page: 'dashboard' + page: 'dashboard', + chart: null }; }, + created() { + (this as any).api('admin/chart').then(chart => { + this.chart = chart; + }); + }, methods: { nav(page: string) { this.page = page; @@ -47,7 +68,7 @@ export default Vue.extend({ }); </script> -<style lang="stylus" scoped> +<style lang="stylus"> @import '~const.styl' .mk-admin @@ -90,13 +111,23 @@ export default Vue.extend({ width 100% padding 16px 32px -header - margin 10px 0 + > div + > div + max-width 800px +.card + padding 32px + background #fff + box-shadow 0 2px 8px rgba(#000, 0.1) -button - margin 16px 0 - position absolute - right 0 + &:not(:last-child) + margin-bottom 16px + + > header + margin 0 0 1em 0 + padding 0 0 8px 0 + font-size 1em + color #555 + border-bottom solid 1px #eee </style> diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue index bf19e6da57..87b35db259 100644 --- a/src/client/app/dev/views/new-app.vue +++ b/src/client/app/dev/views/new-app.vue @@ -95,7 +95,7 @@ export default Vue.extend({ callbackUrl: this.cb, permission: this.permission }).then(() => { - location.href = '/apps'; + location.href = '/dev/apps'; }).catch(() => { alert('アプリの作成に失敗しました。再度お試しください。'); }); diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue index 702bc4c9e1..a74df67c0a 100644 --- a/src/client/app/mobile/views/components/post-form.vue +++ b/src/client/app/mobile/views/components/post-form.vue @@ -56,7 +56,7 @@ import Vue from 'vue'; import insertTextAtCursor from 'insert-text-at-cursor'; import * as XDraggable from 'vuedraggable'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; -import getKao from '../../../common/scripts/get-kao'; +import getFace from '../../../common/scripts/get-face'; import parse from '../../../../../mfm/parse'; import { host } from '../../../config'; @@ -94,7 +94,7 @@ export default Vue.extend({ files: [], poll: false, geo: null, - visibility: 'public', + visibility: this.$store.state.device.visibility || 'public', visibleUsers: [], useCw: false, cw: null, @@ -240,8 +240,7 @@ export default Vue.extend({ setVisibility() { const w = (this as any).os.new(MkVisibilityChooser, { source: this.$refs.visibilityButton, - compact: true, - v: this.visibility + compact: true }); w.$once('chosen', v => { this.visibility = v; @@ -314,7 +313,7 @@ export default Vue.extend({ }, kao() { - this.text += getKao(); + this.text += getFace(); } } }); diff --git a/src/client/app/stats/style.styl b/src/client/app/stats/style.styl deleted file mode 100644 index 5ae230ea56..0000000000 --- a/src/client/app/stats/style.styl +++ /dev/null @@ -1,10 +0,0 @@ -@import "../app" -@import "../reset" - -html - color #456267 - background #fff - -body - margin 0 - padding 0 diff --git a/src/client/app/stats/tags/index.tag b/src/client/app/stats/tags/index.tag deleted file mode 100644 index f8944c0832..0000000000 --- a/src/client/app/stats/tags/index.tag +++ /dev/null @@ -1,209 +0,0 @@ -<mk-index> - <h1>Misskey<i>Statistics</i></h1> - <main v-if="!initializing"> - <mk-users stats={ stats }/> - <mk-notes stats={ stats }/> - </main> - <footer><a href={ _URL_ }>{ _HOST_ }</a></footer> - <style lang="stylus" scoped> - :scope - display block - margin 0 auto - padding 0 16px - max-width 700px - - > h1 - margin 0 - padding 24px 0 0 0 - font-size 24px - font-weight normal - - > i - font-style normal - color #f43b16 - - > main - > * - margin 24px 0 - padding-top 24px - border-top solid 1px #eee - - > h2 - margin 0 0 12px 0 - font-size 18px - font-weight normal - - > footer - margin 24px 0 - text-align center - - > a - color #546567 - </style> - <script lang="typescript"> - this.mixin('api'); - - this.initializing = true; - - this.on('mount', () => { - this.$root.$data.os.api('stats').then(stats => { - this.update({ - initializing: false, - stats - }); - }); - }); - </script> -</mk-index> - -<mk-notes> - <h2>%i18n:stats.notes-count% <b>{ stats.notesCount }</b></h2> - <mk-notes-chart v-if="!initializing" data={ data }/> - <style lang="stylus" scoped> - :scope - display block - </style> - <script lang="typescript"> - this.mixin('api'); - - this.initializing = true; - this.stats = this.opts.stats; - - this.on('mount', () => { - this.$root.$data.os.api('aggregation/notes', { - limit: 365 - }).then(data => { - this.update({ - initializing: false, - data - }); - }); - }); - </script> -</mk-notes> - -<mk-users> - <h2>%i18n:stats.users-count% <b>{ stats.usersCount }</b></h2> - <mk-users-chart v-if="!initializing" data={ data }/> - <style lang="stylus" scoped> - :scope - display block - </style> - <script lang="typescript"> - this.mixin('api'); - - this.initializing = true; - this.stats = this.opts.stats; - - this.on('mount', () => { - this.$root.$data.os.api('aggregation/users', { - limit: 365 - }).then(data => { - this.update({ - initializing: false, - data - }); - }); - }); - </script> -</mk-users> - -<mk-notes-chart> - <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none"> - <title>Black ... Total<br/>Blue ... Notes<br/>Red ... Replies<br/>Green ... Renotes</title> - <polyline - riot-points={ pointsNote } - fill="none" - stroke-width="1" - stroke="#41ddde"/> - <polyline - riot-points={ pointsReply } - fill="none" - stroke-width="1" - stroke="#f7796c"/> - <polyline - riot-points={ pointsRenote } - fill="none" - stroke-width="1" - stroke="#a1de41"/> - <polyline - riot-points={ pointsTotal } - fill="none" - stroke-width="1" - stroke="#555" - stroke-dasharray="2 2"/> - </svg> - <style lang="stylus" scoped> - :scope - display block - - > svg - display block - padding 1px - width 100% - </style> - <script lang="typescript"> - this.viewBoxX = 365; - this.viewBoxY = 80; - - this.data = this.opts.data.reverse(); - this.data.forEach(d => d.total = d.notes + d.replies + d.renotes); - const peak = Math.max.apply(null, this.data.map(d => d.total)); - - this.on('mount', () => { - this.render(); - }); - - this.render = () => { - this.update({ - pointsNote: this.data.map((d, i) => `${i},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' '), - pointsReply: this.data.map((d, i) => `${i},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '), - pointsRenote: this.data.map((d, i) => `${i},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' '), - pointsTotal: this.data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ') - }); - }; - </script> -</mk-notes-chart> - -<mk-users-chart> - <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none"> - <polyline - riot-points={ createdPoints } - fill="none" - stroke-width="1" - stroke="#1cde84"/> - <polyline - riot-points={ totalPoints } - fill="none" - stroke-width="1" - stroke="#555"/> - </svg> - <style lang="stylus" scoped> - :scope - display block - - > svg - display block - padding 1px - width 100% - </style> - <script lang="typescript"> - this.viewBoxX = 365; - this.viewBoxY = 80; - - this.data = this.opts.data.reverse(); - const totalPeak = Math.max.apply(null, this.data.map(d => d.total)); - const createdPeak = Math.max.apply(null, this.data.map(d => d.created)); - - this.on('mount', () => { - this.render(); - }); - - this.render = () => { - this.update({ - totalPoints: this.data.map((d, i) => `${i},${(1 - (d.total / totalPeak)) * this.viewBoxY}`).join(' '), - createdPoints: this.data.map((d, i) => `${i},${(1 - (d.created / createdPeak)) * this.viewBoxY}`).join(' ') - }); - }; - </script> -</mk-users-chart> diff --git a/src/client/app/stats/tags/index.ts b/src/client/app/stats/tags/index.ts deleted file mode 100644 index f41151949f..0000000000 --- a/src/client/app/stats/tags/index.ts +++ /dev/null @@ -1 +0,0 @@ -require('./index.tag'); diff --git a/src/client/app/status/style.styl b/src/client/app/status/style.styl deleted file mode 100644 index 5ae230ea56..0000000000 --- a/src/client/app/status/style.styl +++ /dev/null @@ -1,10 +0,0 @@ -@import "../app" -@import "../reset" - -html - color #456267 - background #fff - -body - margin 0 - padding 0 diff --git a/src/client/app/status/tags/index.tag b/src/client/app/status/tags/index.tag deleted file mode 100644 index 899467097a..0000000000 --- a/src/client/app/status/tags/index.tag +++ /dev/null @@ -1,201 +0,0 @@ -<mk-index> - <h1>Misskey<i>Status</i></h1> - <p>%fa:info-circle%%i18n:status.all-systems-maybe-operational%</p> - <main> - <mk-cpu-usage connection={ connection }/> - <mk-mem-usage connection={ connection }/> - </main> - <footer><a href={ _URL_ }>{ _HOST_ }</a></footer> - <style lang="stylus" scoped> - :scope - display block - margin 0 auto - padding 0 16px - max-width 700px - - > h1 - margin 0 - padding 24px 0 16px 0 - font-size 24px - font-weight normal - - > [data-fa] - font-style normal - color #f43b16 - - > p - display block - margin 0 - padding 12px 16px - background #eaf4ef - //border solid 1px #99ccb2 - border-radius 4px - - > [data-fa] - margin-right 5px - - > main - > * - margin 24px 0 - - > h2 - margin 0 0 12px 0 - font-size 18px - font-weight normal - - > footer - margin 24px 0 - text-align center - - > a - color #546567 - </style> - <script lang="typescript"> - import Connection from '../../common/scripts/streaming/server-stream'; - - this.mixin('api'); - - this.initializing = true; - this.connection = new Connection(); - - this.on('mount', () => { - this.$root.$data.os.api('meta').then(meta => { - this.update({ - initializing: false, - meta - }); - }); - }); - - this.on('unmount', () => { - this.connection.close(); - }); - - </script> -</mk-index> - -<mk-cpu-usage> - <h2>CPU <b>{ percentage }%</b></h2> - <mk-line-chart ref="chart"/> - <style lang="stylus" scoped> - :scope - display block - </style> - <script lang="typescript"> - this.connection = this.opts.connection; - - this.on('mount', () => { - this.connection.on('stats', this.onStats); - }); - - this.on('unmount', () => { - this.connection.off('stats', this.onStats); - }); - - this.onStats = stats => { - this.$refs.chart.addData(1 - stats.cpu_usage); - - const percentage = (stats.cpu_usage * 100).toFixed(0); - - this.update({ - percentage - }); - }; - </script> -</mk-cpu-usage> - -<mk-mem-usage> - <h2>MEM <b>{ percentage }%</b></h2> - <mk-line-chart ref="chart"/> - <style lang="stylus" scoped> - :scope - display block - </style> - <script lang="typescript"> - this.connection = this.opts.connection; - - this.on('mount', () => { - this.connection.on('stats', this.onStats); - }); - - this.on('unmount', () => { - this.connection.off('stats', this.onStats); - }); - - this.onStats = stats => { - stats.mem.used = stats.mem.total - stats.mem.free; - this.$refs.chart.addData(1 - (stats.mem.used / stats.mem.total)); - - const percentage = (stats.mem.used / stats.mem.total * 100).toFixed(0); - - this.update({ - percentage - }); - }; - </script> -</mk-mem-usage> - -<mk-line-chart> - <svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none"> - <defs> - <linearGradient id={ gradientId } x1="0" x2="0" y1="1" y2="0"> - <stop offset="0%" stop-color="rgba(244, 59, 22, 0)"></stop> - <stop offset="100%" stop-color="#f43b16"></stop> - </linearGradient> - <mask id={ maskId } x="0" y="0" riot-width={ viewBoxX } riot-height={ viewBoxY }> - <polygon - riot-points={ polygonPoints } - fill="#fff" - fill-opacity="0.5"/> - </mask> - </defs> - <line x1="0" y1="0" riot-x2={ viewBoxX } y2="0" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/> - <line x1="0" y1="25%" riot-x2={ viewBoxX } y2="25%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/> - <line x1="0" y1="50%" riot-x2={ viewBoxX } y2="50%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/> - <line x1="0" y1="75%" riot-x2={ viewBoxX } y2="75%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/> - <line x1="0" y1="100%" riot-x2={ viewBoxX } y2="100%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/> - <rect - x="-1" y="-1" - riot-width={ viewBoxX + 2 } riot-height={ viewBoxY + 2 } - style="stroke: none; fill: url(#{ gradientId }); mask: url(#{ maskId })"/> - <polyline - riot-points={ polylinePoints } - fill="none" - stroke="#f43b16" - stroke-width="0.5"/> - </svg> - <style lang="stylus" scoped> - :scope - display block - padding 16px - border-radius 8px - background #1c2531 - - > svg - display block - padding 1px - width 100% - </style> - <script lang="typescript"> - import uuid from 'uuid'; - - this.viewBoxX = 100; - this.viewBoxY = 30; - this.data = []; - this.gradientId = uuid(); - this.maskId = uuid(); - - this.addData = data => { - this.data.push(data); - if (this.data.length > 100) this.data.shift(); - - const polylinePoints = this.data.map((d, i) => `${this.viewBoxX - ((this.data.length - 1) - i)},${d * this.viewBoxY}`).join(' '); - const polygonPoints = `${this.viewBoxX - (this.data.length - 1)},${ this.viewBoxY } ${ polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; - - this.update({ - polylinePoints, - polygonPoints - }); - }; - </script> -</mk-line-chart> diff --git a/src/client/app/status/tags/index.ts b/src/client/app/status/tags/index.ts deleted file mode 100644 index f41151949f..0000000000 --- a/src/client/app/status/tags/index.ts +++ /dev/null @@ -1 +0,0 @@ -require('./index.tag'); diff --git a/src/client/app/store.ts b/src/client/app/store.ts index f85253a281..7e2cc3976b 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -110,6 +110,10 @@ export default (os: MiOS) => new Vuex.Store({ src: x.src, arg: x.arg }; + }, + + setVisibility(state, visibility) { + state.visibility = visibility; } } }, diff --git a/src/mfm/html-to-mfm.ts b/src/mfm/html-to-mfm.ts index daa228ec51..084578fc18 100644 --- a/src/mfm/html-to-mfm.ts +++ b/src/mfm/html-to-mfm.ts @@ -49,6 +49,9 @@ export default function(html: string): string { text += txt; break; } + // メンション以外 + } else { + text += `[${txt}](${node.attrs.find((x: any) => x.name == 'href').value})`; } if (node.childNodes) { diff --git a/src/mfm/html.ts b/src/mfm/html.ts index c11bd55cf4..c798ee410a 100644 --- a/src/mfm/html.ts +++ b/src/mfm/html.ts @@ -5,6 +5,10 @@ import config from '../config'; import { INote } from '../models/note'; import { TextElement } from './parse'; +function intersperse<T>(sep: T, xs: T[]): T[] { + return [].concat(...xs.map(x => [sep, x])).slice(1); +} + const handlers: { [key: string]: (window: any, token: any, mentionedRemoteUsers: INote['mentionedRemoteUsers']) => void } = { bold({ document }, { bold }) { const b = document.createElement('b'); @@ -80,12 +84,9 @@ const handlers: { [key: string]: (window: any, token: any, mentionedRemoteUsers: }, text({ document }, { content }) { - for (const text of content.split('\n')) { - const node = document.createTextNode(text); - document.body.appendChild(node); - - const br = document.createElement('br'); - document.body.appendChild(br); + const nodes = (content as string).split('\n').map(x => document.createTextNode(x)); + for (const x of intersperse(document.createElement('br'), nodes)) { + document.body.appendChild(x); } }, diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts index 2b9efc404d..dbbc1f1cd5 100644 --- a/src/models/drive-file.ts +++ b/src/models/drive-file.ts @@ -52,6 +52,11 @@ export type IDriveFile = { filename: string; contentType: string; metadata: IMetadata; + + /** + * ファイルサイズ + */ + length: number; }; export function validateFileName(name: string): boolean { diff --git a/src/models/meta.ts b/src/models/meta.ts index 11b9b186ce..aef0163dfe 100644 --- a/src/models/meta.ts +++ b/src/models/meta.ts @@ -11,4 +11,5 @@ export type IMeta = { usersCount: number; originalUsersCount: number; }; + disableRegistration: boolean; }; diff --git a/src/models/registration-tickets.ts b/src/models/registration-tickets.ts new file mode 100644 index 0000000000..846acefedf --- /dev/null +++ b/src/models/registration-tickets.ts @@ -0,0 +1,12 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const RegistrationTicket = db.get<IRegistrationTicket>('registrationTickets'); +RegistrationTicket.createIndex('code', { unique: true }); +export default RegistrationTicket; + +export interface IRegistrationTicket { + _id: mongo.ObjectID; + createdAt: Date; + code: string; +} diff --git a/src/models/stats.ts b/src/models/stats.ts new file mode 100644 index 0000000000..7bff475c63 --- /dev/null +++ b/src/models/stats.ts @@ -0,0 +1,153 @@ +import * as mongo from 'mongodb'; +import db from '../db/mongodb'; + +const Stats = db.get<IStats>('stats'); +Stats.createIndex({ date: -1 }, { unique: true }); +export default Stats; + +export interface IStats { + _id: mongo.ObjectID; + + date: Date; + + /** + * ユーザーに関する統計 + */ + users: { + local: { + /** + * この日時点での、ローカルのユーザーの総計 + */ + total: number; + + /** + * ローカルのユーザー数の前日比 + */ + diff: number; + }; + + remote: { + /** + * この日時点での、リモートのユーザーの総計 + */ + total: number; + + /** + * リモートのユーザー数の前日比 + */ + diff: number; + }; + }; + + /** + * 投稿に関する統計 + */ + notes: { + local: { + /** + * この日時点での、ローカルの投稿の総計 + */ + total: number; + + /** + * ローカルの投稿数の前日比 + */ + diff: number; + + diffs: { + /** + * ローカルの通常の投稿数の前日比 + */ + normal: number; + + /** + * ローカルのリプライの投稿数の前日比 + */ + reply: number; + + /** + * ローカルのRenoteの投稿数の前日比 + */ + renote: number; + }; + }; + + remote: { + /** + * この日時点での、リモートの投稿の総計 + */ + total: number; + + /** + * リモートの投稿数の前日比 + */ + diff: number; + + diffs: { + /** + * リモートの通常の投稿数の前日比 + */ + normal: number; + + /** + * リモートのリプライの投稿数の前日比 + */ + reply: number; + + /** + * リモートのRenoteの投稿数の前日比 + */ + renote: number; + }; + }; + }; + + /** + * ドライブ(のファイル)に関する統計 + */ + drive: { + local: { + /** + * この日時点での、ローカルのドライブファイル数の総計 + */ + totalCount: number; + + /** + * この日時点での、ローカルのドライブファイルサイズの総計 + */ + totalSize: number; + + /** + * ローカルのドライブファイル数の前日比 + */ + diffCount: number; + + /** + * ローカルのドライブファイルサイズの前日比 + */ + diffSize: number; + }; + + remote: { + /** + * この日時点での、リモートのドライブファイル数の総計 + */ + totalCount: number; + + /** + * この日時点での、リモートのドライブファイルサイズの総計 + */ + totalSize: number; + + /** + * リモートのドライブファイル数の前日比 + */ + diffCount: number; + + /** + * リモートのドライブファイルサイズの前日比 + */ + diffSize: number; + }; + }; +} diff --git a/src/server/api/endpoints/admin/chart.ts b/src/server/api/endpoints/admin/chart.ts new file mode 100644 index 0000000000..a0566b11f5 --- /dev/null +++ b/src/server/api/endpoints/admin/chart.ts @@ -0,0 +1,101 @@ +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); +}); diff --git a/src/server/api/endpoints/admin/invite.ts b/src/server/api/endpoints/admin/invite.ts new file mode 100644 index 0000000000..77608e715c --- /dev/null +++ b/src/server/api/endpoints/admin/invite.ts @@ -0,0 +1,26 @@ +import rndstr from 'rndstr'; +import RegistrationTicket from '../../../../models/registration-tickets'; + +export const meta = { + desc: { + ja: '招待コードを発行します。' + }, + + requireCredential: true, + requireAdmin: true, + + params: {} +}; + +export default (params: any) => new Promise(async (res, rej) => { + const code = rndstr({ length: 5, chars: '0-9' }); + + await RegistrationTicket.insert({ + createdAt: new Date(), + code: code + }); + + res({ + code: code + }); +}); diff --git a/src/server/api/endpoints/admin/suspend-user.ts b/src/server/api/endpoints/admin/suspend-user.ts index 8698120cdb..9c32ba987d 100644 --- a/src/server/api/endpoints/admin/suspend-user.ts +++ b/src/server/api/endpoints/admin/suspend-user.ts @@ -4,43 +4,43 @@ import getParams from '../../get-params'; import User from '../../../../models/user'; export const meta = { - desc: { - ja: '指定したユーザーを凍結します。', - en: 'Suspend a user.' - }, + desc: { + ja: '指定したユーザーを凍結します。', + en: 'Suspend a user.' + }, - requireCredential: true, - requireAdmin: true, + requireCredential: true, + requireAdmin: true, - params: { - userId: $.type(ID).note({ - desc: { - ja: '対象のユーザーID', - en: 'The user ID which you want to suspend' - } - }), - } + params: { + userId: $.type(ID).note({ + desc: { + ja: '対象のユーザーID', + en: 'The user ID which you want to suspend' + } + }), + } }; export default (params: any) => new Promise(async (res, rej) => { - const [ps, psErr] = getParams(meta, params); - if (psErr) return rej(psErr); + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); - const user = await User.findOne({ - _id: ps.userId - }); + const user = await User.findOne({ + _id: ps.userId + }); - if (user == null) { - return rej('user not found'); - } + if (user == null) { + return rej('user not found'); + } - await User.findOneAndUpdate({ - _id: user._id - }, { - $set: { - isSuspended: true - } - }); + await User.findOneAndUpdate({ + _id: user._id + }, { + $set: { + isSuspended: true + } + }); - res(); + res(); }); diff --git a/src/server/api/endpoints/admin/unverify-user.ts b/src/server/api/endpoints/admin/unverify-user.ts new file mode 100644 index 0000000000..34653cd78a --- /dev/null +++ b/src/server/api/endpoints/admin/unverify-user.ts @@ -0,0 +1,46 @@ +import $ from 'cafy'; +import ID from '../../../../misc/cafy-id'; +import getParams from '../../get-params'; +import User from '../../../../models/user'; + +export const meta = { + desc: { + ja: '指定したユーザーの公式アカウントを解除します。', + en: 'Mark a user as unverified.' + }, + + requireCredential: true, + requireAdmin: true, + + params: { + userId: $.type(ID).note({ + desc: { + ja: '対象のユーザーID', + en: 'The user ID which you want to unverify' + } + }), + } +}; + +export default (params: any) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); + + const user = await User.findOne({ + _id: ps.userId + }); + + if (user == null) { + return rej('user not found'); + } + + await User.findOneAndUpdate({ + _id: user._id + }, { + $set: { + isVerified: false + } + }); + + res(); +}); diff --git a/src/server/api/endpoints/admin/verify-user.ts b/src/server/api/endpoints/admin/verify-user.ts new file mode 100644 index 0000000000..5b826eb1c3 --- /dev/null +++ b/src/server/api/endpoints/admin/verify-user.ts @@ -0,0 +1,46 @@ +import $ from 'cafy'; +import ID from '../../../../misc/cafy-id'; +import getParams from '../../get-params'; +import User from '../../../../models/user'; + +export const meta = { + desc: { + ja: '指定したユーザーを公式アカウントにします。', + en: 'Mark a user as verified.' + }, + + requireCredential: true, + requireAdmin: true, + + params: { + userId: $.type(ID).note({ + desc: { + ja: '対象のユーザーID', + en: 'The user ID which you want to verify' + } + }), + } +}; + +export default (params: any) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); + + const user = await User.findOne({ + _id: ps.userId + }); + + if (user == null) { + return rej('user not found'); + } + + await User.findOneAndUpdate({ + _id: user._id + }, { + $set: { + isVerified: true + } + }); + + res(); +}); diff --git a/src/server/api/endpoints/aggregation/posts.ts b/src/server/api/endpoints/aggregation/posts.ts deleted file mode 100644 index 629bb19108..0000000000 --- a/src/server/api/endpoints/aggregation/posts.ts +++ /dev/null @@ -1,84 +0,0 @@ -import $ from 'cafy'; -import Note from '../../../../models/note'; - -/** - * Aggregate notes - */ -export default (params: any) => new Promise(async (res, rej) => { - // Get 'limit' parameter - const [limit = 365, limitErr] = $.num.optional.range(1, 365).get(params.limit); - if (limitErr) return rej('invalid limit param'); - - const datas = await Note - .aggregate([ - { $project: { - renoteId: '$renoteId', - replyId: '$replyId', - createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST - }}, - { $project: { - date: { - year: { $year: '$createdAt' }, - month: { $month: '$createdAt' }, - day: { $dayOfMonth: '$createdAt' } - }, - type: { - $cond: { - if: { $ne: ['$renoteId', null] }, - then: 'renote', - else: { - $cond: { - if: { $ne: ['$replyId', null] }, - then: 'reply', - else: 'note' - } - } - } - }} - }, - { $group: { _id: { - date: '$date', - type: '$type' - }, count: { $sum: 1 } } }, - { $group: { - _id: '$_id.date', - data: { $addToSet: { - type: '$_id.type', - count: '$count' - }} - } } - ]); - - datas.forEach((data: any) => { - data.date = data._id; - delete data._id; - - data.notes = (data.data.filter((x: any) => x.type == 'note')[0] || { count: 0 }).count; - data.renotes = (data.data.filter((x: any) => x.type == 'renote')[0] || { count: 0 }).count; - data.replies = (data.data.filter((x: any) => x.type == 'reply')[0] || { count: 0 }).count; - - delete data.data; - }); - - const graph = []; - - for (let i = 0; i < limit; i++) { - const day = new Date(new Date().setDate(new Date().getDate() - i)); - - const data = datas.filter((d: any) => - d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate() - )[0]; - - if (data) { - graph.push(data); - } else { - graph.push({ - notes: 0, - renotes: 0, - replies: 0 - }); - } - } - - res(graph); -}); diff --git a/src/server/api/endpoints/aggregation/users.ts b/src/server/api/endpoints/aggregation/users.ts deleted file mode 100644 index f1e41cf170..0000000000 --- a/src/server/api/endpoints/aggregation/users.ts +++ /dev/null @@ -1,55 +0,0 @@ -import $ from 'cafy'; -import User from '../../../../models/user'; - -/** - * Aggregate users - */ -export default (params: any) => new Promise(async (res, rej) => { - // Get 'limit' parameter - const [limit = 365, limitErr] = $.num.optional.range(1, 365).get(params.limit); - if (limitErr) return rej('invalid limit param'); - - const users = await User - .find({}, { - sort: { - _id: -1 - }, - fields: { - _id: false, - createdAt: true, - deletedAt: true - } - }); - - const graph = []; - - for (let i = 0; i < limit; i++) { - let dayStart = new Date(new Date().setDate(new Date().getDate() - i)); - dayStart = new Date(dayStart.setMilliseconds(0)); - dayStart = new Date(dayStart.setSeconds(0)); - dayStart = new Date(dayStart.setMinutes(0)); - dayStart = new Date(dayStart.setHours(0)); - - let dayEnd = new Date(new Date().setDate(new Date().getDate() - i)); - dayEnd = new Date(dayEnd.setMilliseconds(999)); - dayEnd = new Date(dayEnd.setSeconds(59)); - dayEnd = new Date(dayEnd.setMinutes(59)); - dayEnd = new Date(dayEnd.setHours(23)); - // day = day.getTime(); - - const total = users.filter(u => - u.createdAt < dayEnd && (u.deletedAt == null || u.deletedAt > dayEnd) - ).length; - - const created = users.filter(u => - u.createdAt < dayEnd && u.createdAt > dayStart - ).length; - - graph.push({ - total: total, - created: created - }); - } - - res(graph); -}); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index c2d93997a7..000a56024d 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -28,6 +28,7 @@ export default () => new Promise(async (res, rej) => { model: os.cpus()[0].model, cores: os.cpus().length }, - broadcasts: meta.broadcasts + broadcasts: meta.broadcasts, + disableRegistration: meta.disableRegistration }); }); diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index 66d018618c..9cdbec5270 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -16,8 +16,7 @@ export const meta = { limit: { duration: ms('1hour'), - max: 300, - minInterval: ms('1second') + max: 300 }, kind: 'note-write', diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index 16ec33bcbf..79e5e6ec48 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -6,6 +6,7 @@ import User, { IUser, validateUsername, validatePassword, pack } from '../../../ import generateUserToken from '../common/generate-native-user-token'; import config from '../../../config'; import Meta from '../../../models/meta'; +import RegistrationTicket from '../../../models/registration-tickets'; if (config.recaptcha) { recaptcha.init({ @@ -29,6 +30,29 @@ export default async (ctx: Koa.Context) => { const username = body['username']; const password = body['password']; + const invitationCode = body['invitationCode']; + + const meta = await Meta.findOne({}); + + if (meta && meta.disableRegistration) { + if (invitationCode == null || typeof invitationCode != 'string') { + ctx.status = 400; + return; + } + + const ticket = await RegistrationTicket.findOne({ + code: invitationCode + }); + + if (ticket == null) { + ctx.status = 400; + return; + } + + RegistrationTicket.remove({ + _id: ticket._id + }); + } // Validate username if (!validateUsername(username)) { diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index da0d3fd82f..b090d56cee 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -17,6 +17,7 @@ import { isLocalUser, IUser, IRemoteUser } from '../../models/user'; import delFile from './delete-file'; import config from '../../config'; import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail'; +import { updateDriveStats } from '../update-chart'; const log = debug('misskey:drive:add-file'); @@ -377,7 +378,8 @@ export default async function( publishDriveStream(user._id, 'file_created', packedFile); }); - // TODO: サムネイル生成 + // 統計を更新 + updateDriveStats(driveFile, true); return driveFile; } diff --git a/src/services/drive/delete-file.ts b/src/services/drive/delete-file.ts index 445d231d66..73532a2953 100644 --- a/src/services/drive/delete-file.ts +++ b/src/services/drive/delete-file.ts @@ -2,6 +2,7 @@ import * as Minio from 'minio'; import DriveFile, { DriveFileChunk, IDriveFile } from '../../models/drive-file'; import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail'; import config from '../../config'; +import { updateDriveStats } from '../update-chart'; export default async function(file: IDriveFile, isExpired = false) { if (file.metadata.storage == 'minio') { @@ -45,4 +46,7 @@ export default async function(file: IDriveFile, isExpired = false) { await DriveFileThumbnail.remove({ _id: thumbnail._id }); } //#endregion + + // 統計を更新 + updateDriveStats(file, false); } diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 521750dc84..d8f0f57b63 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -23,6 +23,7 @@ import registerHashtag from '../register-hashtag'; import isQuote from '../../misc/is-quote'; import { TextElementMention } from '../../mfm/parse/elements/mention'; import { TextElementHashtag } from '../../mfm/parse/elements/hashtag'; +import { updateNoteStats } from '../update-chart'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -142,6 +143,9 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< return; } + // 統計を更新 + updateNoteStats(note, true); + // ハッシュタグ登録 tags.map(tag => registerHashtag(user, tag)); diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts index 7f245958b0..d444b13a8b 100644 --- a/src/services/note/delete.ts +++ b/src/services/note/delete.ts @@ -6,6 +6,7 @@ import pack from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; import Following from '../../models/following'; import renderNote from '../../remote/activitypub/renderer/note'; +import { updateNoteStats } from '../update-chart'; /** * 投稿を削除します。 @@ -43,4 +44,7 @@ export default async function(user: IUser, note: INote) { }); } //#endregion + + // 統計を更新 + updateNoteStats(note, false); } diff --git a/src/services/update-chart.ts b/src/services/update-chart.ts new file mode 100644 index 0000000000..7998baca9d --- /dev/null +++ b/src/services/update-chart.ts @@ -0,0 +1,223 @@ +import { INote } from '../models/note'; +import Stats, { IStats } from '../models/stats'; +import { isLocalUser, IUser } from '../models/user'; +import { IDriveFile } from '../models/drive-file'; + +type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; + +async function getTodayStats(): Promise<IStats> { + const now = new Date(); + const y = now.getFullYear(); + const m = now.getMonth(); + const d = now.getDate(); + const today = new Date(y, m, d); + + // 今日の統計 + const todayStats = await Stats.findOne({ + date: today + }); + + // 日付が変わってから、初めてのチャート更新なら + if (todayStats == null) { + // 最も最近の統計を持ってくる + // * 昨日何もチャートを更新するような出来事がなかった場合は、 + // 統計がそもそも作られずドキュメントが存在しないということがあり得るため、 + // 「昨日の」と決め打ちせずに「もっとも最近の」とします + const mostRecentStats = await Stats.findOne({}, { + sort: { + date: -1 + } + }); + + // 統計が存在しなかったら + // * Misskeyインスタンスを建てて初めてのチャート更新時など + if (mostRecentStats == null) { + // 空の統計を作成 + const chart: Omit<IStats, '_id'> = { + date: today, + 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 + } + } + }; + + const stats = await Stats.insert(chart); + + return stats; + } else { + // 今日の統計を初期挿入 + const chart: Omit<IStats, '_id'> = { + date: today, + users: { + local: { + total: mostRecentStats.users.local.total, + diff: 0 + }, + remote: { + total: mostRecentStats.users.remote.total, + diff: 0 + } + }, + notes: { + local: { + total: mostRecentStats.notes.local.total, + diff: 0, + diffs: { + normal: 0, + reply: 0, + renote: 0 + } + }, + remote: { + total: mostRecentStats.notes.remote.total, + diff: 0, + diffs: { + normal: 0, + reply: 0, + renote: 0 + } + } + }, + drive: { + local: { + totalCount: mostRecentStats.drive.local.totalCount, + totalSize: mostRecentStats.drive.local.totalSize, + diffCount: 0, + diffSize: 0 + }, + remote: { + totalCount: mostRecentStats.drive.remote.totalCount, + totalSize: mostRecentStats.drive.remote.totalSize, + diffCount: 0, + diffSize: 0 + } + } + }; + + const stats = await Stats.insert(chart); + + return stats; + } + } else { + return todayStats; + } +} + +async function update(inc: any) { + const stats = await getTodayStats(); + + await Stats.findOneAndUpdate({ + _id: stats._id + }, { + $inc: inc + }); +} + +export async function updateUserStats(user: IUser, isAdditional: boolean) { + const inc = {} as any; + + const amount = isAdditional ? 1 : -1; + + if (isLocalUser(user)) { + inc['users.local.total'] = amount; + inc['users.local.diff'] = amount; + } else { + inc['users.remote.total'] = amount; + inc['users.remote.diff'] = amount; + } + + await update(inc); +} + +export async function updateNoteStats(note: INote, isAdditional: boolean) { + const inc = {} as any; + + const amount = isAdditional ? 1 : -1; + + if (isLocalUser(note._user)) { + inc['notes.local.total'] = amount; + inc['notes.local.diff'] = amount; + + if (note.replyId != null) { + inc['notes.local.diffs.reply'] = amount; + } else if (note.renoteId != null) { + inc['notes.local.diffs.renote'] = amount; + } else { + inc['notes.local.diffs.normal'] = amount; + } + } else { + inc['notes.remote.total'] = amount; + inc['notes.remote.diff'] = amount; + + if (note.replyId != null) { + inc['notes.remote.diffs.reply'] = amount; + } else if (note.renoteId != null) { + inc['notes.remote.diffs.renote'] = amount; + } else { + inc['notes.remote.diffs.normal'] = amount; + } + } + + await update(inc); +} + +export async function updateDriveStats(file: IDriveFile, isAdditional: boolean) { + const inc = {} as any; + + const amount = isAdditional ? 1 : -1; + const size = isAdditional ? file.length : -file.length; + + if (isLocalUser(file.metadata._user)) { + inc['drive.local.totalCount'] = amount; + inc['drive.local.diffCount'] = amount; + inc['drive.local.totalSize'] = size; + inc['drive.local.diffSize'] = size; + } else { + inc['drive.remote.total'] = amount; + inc['drive.remote.diff'] = amount; + inc['drive.remote.totalSize'] = size; + inc['drive.remote.diffSize'] = size; + } + + await update(inc); +}