diff --git a/migration/1599577510614-mode.ts b/migration/1599577510614-mode.ts new file mode 100644 index 0000000..9b1e3dc --- /dev/null +++ b/migration/1599577510614-mode.ts @@ -0,0 +1,16 @@ +import {MigrationInterface, QueryRunner} from 'typeorm'; + +export class mode1599577510614 implements MigrationInterface { + name = 'mode1599577510614' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('CREATE TYPE "user_alertmode_enum" AS ENUM(\'note\', \'notification\', \'nothing\')'); + await queryRunner.query('ALTER TABLE "user" ADD "alertMode" "user_alertmode_enum" NOT NULL DEFAULT \'note\''); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "user" DROP COLUMN "alertMode"'); + await queryRunner.query('DROP TYPE "user_alertmode_enum"'); + } + +} diff --git a/package.json b/package.json index 6d755c8..408d3b6 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dependencies": { "@types/koa-bodyparser": "^4.3.0", "@types/koa-mount": "^4.0.0", + "@types/koa-multer": "^1.0.0", "@types/koa-static": "^4.0.1", "@types/node-cron": "^2.0.3", "@types/uuid": "^8.0.0", @@ -29,6 +30,7 @@ "koa": "^2.13.0", "koa-bodyparser": "^4.3.0", "koa-mount": "^4.0.0", + "koa-multer": "^1.0.2", "koa-router": "^9.1.0", "koa-session": "^6.0.0", "koa-static": "^5.0.0", diff --git a/src/functions/format.ts b/src/functions/format.ts index 725c69c..65c64e5 100644 --- a/src/functions/format.ts +++ b/src/functions/format.ts @@ -2,32 +2,15 @@ import { api } from '../services/misskey'; import { config } from '../config'; import { User } from '../models/entities/user'; import { updateUser } from './users'; +import { Score } from '../types/Score'; -export const format = async (user: User): Promise => { - const miUser = await api>(user.host, 'users/show', { username: user.username }, user.token); - if (miUser.error) { - throw miUser.error; - } - const notesDelta = toSignedString(miUser.notesCount - user.prevNotesCount); - const followingDelta = toSignedString(miUser.followingCount - user.prevFollowingCount); - const followersDelta = toSignedString(miUser.followersCount - user.prevFollowersCount); - - await updateUser(user.username, user.host, { - prevNotesCount: miUser.notesCount, - prevFollowingCount: miUser.followingCount, - prevFollowersCount: miUser.followersCount, - }); +export const format = (score: Score): string => `昨日のMisskeyの活動は - return `昨日のMisskeyの活動は - -ノート: ${miUser.notesCount}(${notesDelta}) -フォロー : ${miUser.followingCount}(${followingDelta}) -フォロワー :${miUser.followersCount}(${followersDelta}) +ノート: ${score.notesCount}(${score.notesDelta}) +フォロー : ${score.followingCount}(${score.followingDelta}) +フォロワー :${score.followersCount}(${score.followersDelta}) でした。 ${config.url} #misshaialert`; -}; - -export const toSignedString = (num: number): string => num < 0 ? num.toString() : '+' + num; \ No newline at end of file diff --git a/src/functions/get-scores.ts b/src/functions/get-scores.ts new file mode 100644 index 0000000..0ddbb7b --- /dev/null +++ b/src/functions/get-scores.ts @@ -0,0 +1,19 @@ +import { User } from '../models/entities/user'; +import { Score } from '../types/Score'; +import { api } from '../services/misskey'; +import { toSignedString } from './to-signed-string'; + +export const getScores = async (user: User): Promise => { + const miUser = await api>(user.host, 'users/show', { username: user.username }, user.token); + if (miUser.error) { + throw miUser.error; + } + return { + notesCount: miUser.notesCount, + followingCount: miUser.followingCount, + followersCount: miUser.followersCount, + notesDelta: toSignedString(miUser.notesCount - user.prevNotesCount), + followingDelta: toSignedString(miUser.followingCount - user.prevFollowingCount), + followersDelta: toSignedString(miUser.followersCount - user.prevFollowersCount), + }; +}; \ No newline at end of file diff --git a/src/functions/to-signed-string.ts b/src/functions/to-signed-string.ts new file mode 100644 index 0000000..4517067 --- /dev/null +++ b/src/functions/to-signed-string.ts @@ -0,0 +1 @@ +export const toSignedString = (num: number): string => num < 0 ? num.toString() : '+' + num; diff --git a/src/functions/update-score.ts b/src/functions/update-score.ts new file mode 100644 index 0000000..653500c --- /dev/null +++ b/src/functions/update-score.ts @@ -0,0 +1,16 @@ +import { User } from '../models/entities/user'; +import { api } from '../services/misskey'; +import { updateUser } from './users'; + +export const updateScore = async (user: User): Promise => { + const miUser = await api>(user.host, 'users/show', { username: user.username }, user.token); + if (miUser.error) { + throw miUser.error; + } + + await updateUser(user.username, user.host, { + prevNotesCount: miUser.notesCount, + prevFollowingCount: miUser.followingCount, + prevFollowersCount: miUser.followersCount, + }); +}; \ No newline at end of file diff --git a/src/models/entities/user.ts b/src/models/entities/user.ts index 2feea18..b2f1618 100644 --- a/src/models/entities/user.ts +++ b/src/models/entities/user.ts @@ -1,4 +1,5 @@ import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm'; +import { AlertMode, alertModes } from '../../types/AlertMode'; @Entity() @Index([ 'username', 'host' ], { unique: true }) @@ -44,4 +45,11 @@ export class User { default: 0, }) public prevFollowersCount: number; + + @Column({ + type: 'enum', + enum: alertModes, + default: 'note' + }) + public alertMode: AlertMode; } \ No newline at end of file diff --git a/src/server/die.ts b/src/server/die.ts index 219616d..4b26e22 100644 --- a/src/server/die.ts +++ b/src/server/die.ts @@ -1,6 +1,6 @@ import { Context } from 'koa'; -export const die = (ctx: Context, error: string, status = 400): Promise => { +export const die = (ctx: Context, error = '問題が発生しました。お手数ですが、最初からやり直してください。', status = 400): Promise => { ctx.status = status; return ctx.render('error', { error }); }; diff --git a/src/server/router.ts b/src/server/router.ts index 4cd99e2..c55f9df 100644 --- a/src/server/router.ts +++ b/src/server/router.ts @@ -6,8 +6,11 @@ import crypto from 'crypto'; import { die } from './die'; import { v4 as uuid } from 'uuid'; import { config } from '../config'; -import { upsertUser, getUser, getUserCount, updateUser, updateUsersMisshaiToken, getUserByMisshaiToken } from '../functions/users'; +import { upsertUser, getUser, getUserCount, updateUser, updateUsersMisshaiToken, getUserByMisshaiToken, deleteUser } from '../functions/users'; import { api } from '../services/misskey'; +import { getScores } from '../functions/get-scores'; +import { AlertMode, alertModes } from '../types/AlertMode'; +import { Users } from '../models'; export const router = new Router(); @@ -29,7 +32,7 @@ const login = async (ctx: Context, user: Record, host: string, const u = await getUser(user.username as string, host); if (!u) { - await die(ctx, '問題が発生しました。お手数ですが、最初からやり直してください。'); + await die(ctx); return; } @@ -58,7 +61,9 @@ router.get('/', async ctx => { }); } else { await ctx.render('mypage', { - user + user, + usersCount: await getUserCount(), + score: await getScores(user), }); } }); @@ -76,7 +81,7 @@ router.get('/login', async ctx => { host = meta.uri.replace(/^https?:\/\//, ''); const name = 'みす廃あらーと'; const description = 'ついついノートしすぎていませんか?'; - const permission = [ 'write:notes' ]; + const permission = [ 'write:notes', 'write:notifications' ]; if (meta.features.miauth) { // Use MiAuth @@ -106,6 +111,42 @@ router.get('/login', async ctx => { } }); +router.get('/logout', async ctx => { + const token = ctx.cookies.get('token'); + if (!token) { + await die(ctx, 'ログインしていません'); + return; + } + ctx.cookies.set('token', ''); + await ctx.render('welcome', { + usersCount: await getUserCount(), + welcomeMessage: 'ログアウトしました。', + }); +}); + +router.get('/optout', async ctx => { + const token = ctx.cookies.get('token'); + if (!token) { + await die(ctx, 'ログインしていません'); + return; + } + ctx.cookies.set('token', ''); + + const u = await getUserByMisshaiToken(token); + + if (!u) { + await die(ctx); + return; + } + + await deleteUser(u.username, u.host); + + await ctx.render('welcome', { + usersCount: await getUserCount(), + welcomeMessage: '連携を解除しました。', + }); +}); + router.get('/terms', async ctx => { await ctx.render('term'); }); @@ -127,7 +168,7 @@ router.get('/miauth', async ctx => { const host = sessionHostCache[session]; delete sessionHostCache[session]; if (!host) { - await die(ctx, '問題が発生しました。お手数ですが、最初からやり直してください。'); + await die(ctx); return; } @@ -135,7 +176,7 @@ router.get('/miauth', async ctx => { const { token, user } = (await axios.post(url)).data; if (!token || !user) { - await die(ctx, '問題が発生しました。お手数ですが、最初からやり直してください。'); + await die(ctx); return; } @@ -152,13 +193,13 @@ router.get('/legacy-auth', async ctx => { const host = sessionHostCache[token]; delete sessionHostCache[token]; if (!host) { - await die(ctx, '問題が発生しました。お手数ですが、最初からやり直してください。'); + await die(ctx); return; } const appSecret = tokenSecretCache[token]; delete tokenSecretCache[token]; if (!appSecret) { - await die(ctx, '問題が発生しました。お手数ですが、最初からやり直してください。'); + await die(ctx); return; } @@ -172,6 +213,32 @@ router.get('/legacy-auth', async ctx => { await login(ctx, user, host, i); }); +router.post('/update-settings', async ctx => { + const mode = ctx.request.body.alertMode as AlertMode; + // 一応型チェック + if (!alertModes.includes(mode)) { + await die(ctx, `${mode} is an invalid value`); + return; + } + + const token = ctx.cookies.get('token'); + if (!token) { + await die(ctx, 'ログインしていません'); + return; + } + + const u = await getUserByMisshaiToken(token); + + if (!u) { + await die(ctx); + return; + } + + await Users.update(u.id, { alertMode: mode }); + + ctx.redirect('/'); +}); + // Return 404 for other pages router.all('(.*)', async ctx => { diff --git a/src/services/worker.ts b/src/services/worker.ts index 91bae3d..f5c4b97 100644 --- a/src/services/worker.ts +++ b/src/services/worker.ts @@ -5,24 +5,38 @@ import { Users } from '../models'; import { api } from './misskey'; import { format } from '../functions/format'; import { deleteUser } from '../functions/users'; +import { updateScore } from '../functions/update-score'; +import { getScores } from '../functions/get-scores'; export default (): void => { - cron.schedule('0 0 0 * * *', async () => { + cron.schedule('50 45 0 * * *', async () => { const users = await Users.createQueryBuilder() .select() .getMany(); for (const user of users) { try { - const text = await format(user); - - const res = await api>(user.host, 'notes/create', { - text, - visibility: 'home' - }, user.token); - if (res.error) { - throw res.error; - } + await updateScore(user); + const text = format(await getScores(user)); + if (user.alertMode === 'note') { + const res = await api>(user.host, 'notes/create', { + text, + visibility: 'specified' + }, user.token); + if (res.error) { + throw res.error; + } + break; + } else if (user.alertMode === 'notification') { + const res = await api(user.host, 'notifications/create', { + header: 'みす廃あらーと', + icon: 'https://i.imgur.com/B991yTl.png', + body: text, + }, user.token); + if (res.error) { + throw res.error; + } + } } catch (e) { if (e.code === 'NO_SUCH_USER' || e.code === 'AUTHENTICATION_FAILED') { // ユーザーが削除されている場合、レコードからも消してとりやめ @@ -32,7 +46,8 @@ export default (): void => { console.error(e); } } finally { - await delay(3000); + if (user.alertMode === 'note') + await delay(3000); } } }); diff --git a/src/types/AlertMode.ts b/src/types/AlertMode.ts new file mode 100644 index 0000000..bde520a --- /dev/null +++ b/src/types/AlertMode.ts @@ -0,0 +1,7 @@ +export const alertModes = [ + 'note', + 'notification', + 'nothing' +] as const; + +export type AlertMode = typeof alertModes[number]; \ No newline at end of file diff --git a/src/types/Score.ts b/src/types/Score.ts new file mode 100644 index 0000000..32a36a2 --- /dev/null +++ b/src/types/Score.ts @@ -0,0 +1,9 @@ + +export type Score = { + notesCount: number; + followingCount: number; + followersCount: number; + notesDelta: string; + followingDelta: string; + followersDelta: string; +}; diff --git a/src/views/_components.pug b/src/views/_components.pug index e297d20..7daff7f 100644 --- a/src/views/_components.pug +++ b/src/views/_components.pug @@ -1,3 +1,25 @@ mixin exta() a(href=attributes.href target="_blank" rel="noopener noreferrer") - block \ No newline at end of file + block + +mixin serverInfo() + .xd-card + .header + h1.title + i.fas.fa-info-circle + | サービスの情報 + .body + dl + dt + i.fas.fa-users + | 登録者数 + dd !{usersCount} 人 + dt + | 総ノート数 + dd (coming soon) + dt + | 総フォロー数 + dd (coming soon) + dt + | 総フォロワー数 + dd (coming soon) \ No newline at end of file diff --git a/src/views/mypage.pug b/src/views/mypage.pug index 12eb744..c90f400 100644 --- a/src/views/mypage.pug +++ b/src/views/mypage.pug @@ -1,6 +1,63 @@ extends _base block content - section - h2 マイページ - p おかえりなさい、@!{ user.username }@!{ user.host } さん。 \ No newline at end of file + h2 マイページ + p おかえりなさい、@!{ user.username }@!{ user.host } さん。 + + section#scores + .xd-cards.center + +serverInfo() + .xd-card + .header + h1.title + i.fas.fa-chart-area + | あなたの廃人度は… + .body + table + thead + tr + th 内容 + th スコア + th 前日比 + tbody + tr + td ノート + td !{score.notesCount} + td !{score.notesDelta} + tr + td フォロー + td !{score.followingCount} + td !{score.followingDelta} + tr + td フォロワー + td !{score.followersCount} + td !{score.followersDelta} + + section.xd-card#settings + .header + h1.title + i.fas.fa-cog + | 設定 + .body + form(method="post", action="/update-settings") + p: label スコア通知方法: + select(name="alertMode") + option(value="note", selected=user.alertMode === 'note') 自動的にノートを投稿 (標準) + option(value="notification", selected=user.alertMode === 'notification') Misskey に通知 + option(value="nothing", selected=user.alertMode === 'nothing') 通知しない + p: label タイムゾーン:(coming soon) + button.primary(type="submit") 保存 + .body + div.mb-2: a.xd-button.danger#logout(href="/logout") ログアウト + div: a.xd-button.danger#optout(href="/optout") アカウント連携を解除する + +block script + script. + document.getElementById("optout").addEventListener("click", (e) => { + if (!confirm('連携を解除すると、統計情報などのデータが削除されてしまい、以後アラート機能をご利用いただけなくなります。この操作は変更できません。\n\nそれでもなお、連携を解除しますか?')) + e.preventDefault(); + }); + document.getElementById("logout").addEventListener("click", (e) => { + if (!confirm('ログアウトしますか?')) + e.preventDefault(); + }); \ No newline at end of file diff --git a/src/views/welcome.pug b/src/views/welcome.pug index 1f601aa..18016ee 100644 --- a/src/views/welcome.pug +++ b/src/views/welcome.pug @@ -20,17 +20,7 @@ block content section .xd-cards.center - .xd-card - .header - h1.title - i.fas.fa-info-circle - | 情報 - .body - dl - dt - i.fas.fa-users - | 登録者数 - dd !{usersCount} 人 + +serverInfo() .xd-card .header h1.title diff --git a/styles/_xeltica-design.scss b/styles/_xeltica-design.scss index 5133e92..4acdbd0 100644 --- a/styles/_xeltica-design.scss +++ b/styles/_xeltica-design.scss @@ -18,6 +18,9 @@ $card-header: $bg-pale-1; $card-footer: $bg-pale-1; $card-fg: $fg; $divider: rgba($fg, 0.25); +$table-bg-header: $md-grey-300; +$table-bg-odd: $md-grey-100; +$table-bg-even: white; $barSize: 64px; * { @@ -144,6 +147,18 @@ button, .xd-button { background: $primary-dark; } } + + &.danger { + background: $md-red-700; + color: $md-white; + border-color: $md-red-900; + &:hover { + background: $md-red-500; + } + &:active { + background: $md-red-800; + } + } } // ul, ol { @@ -161,6 +176,27 @@ a, .link { } } +table { + border: 1px solid $divider; + border-radius: 2px; + width: 100%; + + > thead { + background: $table-bg-header; + margin-bottom: 2px; + text-align: left; + } + > tbody > tr { + background: $table-bg-even; + &:nth-child(odd) { + background: $table-bg-odd; + } + } + th, td { + padding: 4px 8px; + } +} + code { border-radius: 2px; color: #0f0; @@ -233,6 +269,9 @@ code { } @media screen and (max-width: 640px) { flex-wrap: wrap; + > .xd-card { + margin: 8px 0; + } } &.wrap { diff --git a/yarn.lock b/yarn.lock index 0d7f19a..03107dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -158,6 +158,13 @@ dependencies: "@types/koa" "*" +"@types/koa-multer@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/koa-multer/-/koa-multer-1.0.0.tgz#f449f399ac3f80c05753452f000e8694ceec4249" + integrity sha512-1Fh/tu7nj6/QefLcTuHUUeeZ5J9zqOQbDfTLPG8sh9ni7eioDJ1jOYuP95k3/uo0ApSiUOZ5c2fEXTqiEnPZ+w== + dependencies: + "@types/koa" "*" + "@types/koa-router@^7.4.1": version "7.4.1" resolved "https://registry.yarnpkg.com/@types/koa-router/-/koa-router-7.4.1.tgz#3702a4cabe4558cc4eec70d5574acc04beecff7c" @@ -416,6 +423,11 @@ app-root-path@^3.0.0: resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad" integrity sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw== +append-field@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-0.1.0.tgz#6ddc58fa083c7bc545d3c5995b2830cc2366d44a" + integrity sha1-bdxY+gg8e8VF08WZWygwzCNm1Eo= + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -506,6 +518,11 @@ braces@~3.0.2: dependencies: fill-range "^7.0.1" +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + buffer-writer@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" @@ -519,6 +536,14 @@ buffer@^5.1.0: base64-js "^1.0.2" ieee754 "^1.1.4" +busboy@^0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM= + dependencies: + dicer "0.2.5" + readable-stream "1.1.x" + bytes@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" @@ -709,6 +734,16 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +concat-stream@^1.5.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + condense-newlines@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/condense-newlines/-/condense-newlines-0.2.1.tgz#3de985553139475d32502c83b02f60684d24c55f" @@ -919,6 +954,14 @@ destroy@^1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +dicer@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8= + dependencies: + readable-stream "1.1.x" + streamsearch "0.1.2" + diff@^3.1.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -1506,7 +1549,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1805,6 +1848,13 @@ koa-mount@^4.0.0: debug "^4.0.1" koa-compose "^4.1.0" +koa-multer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/koa-multer/-/koa-multer-1.0.2.tgz#d38f7ffd1db97b1aad33e7774732f000ebd67259" + integrity sha512-0kFzN4atVd+9oiG+4fYxQ9S2T3dPhKNvmhITIY606Qn9wLEmfhW0DhSpOzRYhddN//4rh/TCK95TMtflmFa5lA== + dependencies: + multer "1.3.0" + koa-router@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/koa-router/-/koa-router-9.1.0.tgz#47d1ce2109fd62b1d76eb42df90b635ff93b6831" @@ -2025,6 +2075,20 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +multer@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.3.0.tgz#092b2670f6846fa4914965efc8cf94c20fec6cd2" + integrity sha1-CSsmcPaEb6SRSWXvyM+Uwg/sbNI= + dependencies: + append-field "^0.1.0" + busboy "^0.2.11" + concat-stream "^1.5.0" + mkdirp "^0.5.1" + object-assign "^3.0.0" + on-finished "^2.3.0" + type-is "^1.6.4" + xtend "^4.0.0" + mz@^2.4.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -2131,6 +2195,11 @@ npm-run-all@^4.1.5: shell-quote "^1.6.1" string.prototype.padend "^3.0.0" +object-assign@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" + integrity sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I= + object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -2659,17 +2728,17 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -readable-stream@~1.0.31: - version "1.0.34" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" - integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= +readable-stream@1.1.x: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= dependencies: core-util-is "~1.0.0" inherits "~2.0.1" isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@~2.3.6: +readable-stream@^2.2.2, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -2682,6 +2751,16 @@ readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@~1.0.31: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + readdirp@~3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" @@ -2962,6 +3041,11 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= + string-width@^3.0.0, string-width@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" @@ -3205,7 +3289,7 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@^1.6.16: +type-is@^1.6.16, type-is@^1.6.4: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -3220,6 +3304,11 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + typeorm@0.2.25: version "0.2.25" resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.25.tgz#1a33513b375b78cc7740d2405202208b918d7dde"