From 283863c3903c1df212e7f35c633af8444e158f4b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 26 Oct 2018 00:21:00 +0300 Subject: [PATCH 001/101] Add frontend stub --- .editorconfig | 8 +- maubot/management/api/__init__.py | 0 maubot/management/frontend/.eslintrc.json | 70 + maubot/management/frontend/.gitignore | 5 + maubot/management/frontend/package.json | 25 + maubot/management/frontend/public/favicon.ico | Bin 0 -> 14846 bytes maubot/management/frontend/public/favicon.png | Bin 0 -> 13551 bytes maubot/management/frontend/public/index.html | 34 + .../management/frontend/public/manifest.json | 15 + .../management/frontend/src/MaubotManager.js | 33 + maubot/management/frontend/src/index.js | 21 + .../frontend/src/style/base/body.sass | 53 + .../frontend/src/style/base/vars.sass | 28 + .../management/frontend/src/style/index.sass | 17 + maubot/management/frontend/yarn.lock | 10103 ++++++++++++++++ 15 files changed, 10407 insertions(+), 5 deletions(-) create mode 100644 maubot/management/api/__init__.py create mode 100644 maubot/management/frontend/.eslintrc.json create mode 100644 maubot/management/frontend/.gitignore create mode 100644 maubot/management/frontend/package.json create mode 100644 maubot/management/frontend/public/favicon.ico create mode 100644 maubot/management/frontend/public/favicon.png create mode 100644 maubot/management/frontend/public/index.html create mode 100644 maubot/management/frontend/public/manifest.json create mode 100644 maubot/management/frontend/src/MaubotManager.js create mode 100644 maubot/management/frontend/src/index.js create mode 100644 maubot/management/frontend/src/style/base/body.sass create mode 100644 maubot/management/frontend/src/style/base/vars.sass create mode 100644 maubot/management/frontend/src/style/index.sass create mode 100644 maubot/management/frontend/yarn.lock diff --git a/.editorconfig b/.editorconfig index ea37f9c..d00d5a4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,15 +1,13 @@ root = true [*] -indent_style = tab +indent_style = space indent_size = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true - -[*.py] max_line_length = 99 -[*.{yaml,yml,py}] -indent_style = space +[*.json] +indent_size = 2 diff --git a/maubot/management/api/__init__.py b/maubot/management/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/maubot/management/frontend/.eslintrc.json b/maubot/management/frontend/.eslintrc.json new file mode 100644 index 0000000..4052abf --- /dev/null +++ b/maubot/management/frontend/.eslintrc.json @@ -0,0 +1,70 @@ +{ + "extends": "react-app", + "plugins": [ + "import" + ], + "rules": { + "indent": ["error", 4, { + "ignoredNodes": [ + "JSXAttribute", + "JSXSpreadAttribute" + ], + "FunctionDeclaration": {"parameters": "first"}, + "FunctionExpression": {"parameters": "first"}, + "CallExpression": {"arguments": "first"}, + "ArrayExpression": "first", + "ObjectExpression": "first", + "ImportDeclaration": "first" + }], + "react/jsx-indent-props": ["error", "first"], + "object-curly-newline": ["error", { + "consistent": true + }], + "object-curly-spacing": ["error", "always", { + "arraysInObjects": false, + "objectsInObjects": false + }], + "array-bracket-spacing": ["error", "never"], + "one-var": ["error", { + "initialized": "never", + "uninitialized": "always" + }], + "one-var-declaration-per-line": ["error", "initializations"], + "quotes": ["error", "double"], + "semi": ["error", "never"], + "comma-dangle": ["error", "always-multiline"], + "max-len": ["warn", 100], + "camelcase": ["error", { + "properties": "always" + }], + "space-before-function-paren": ["error", { + "anonymous": "never", + "named": "never", + "asyncArrow": "always" + }], + "func-style": ["warn", "declaration", {"allowArrowFunctions": true}], + "id-length": ["warn", {"max": 40, "exceptions": ["i", "j", "x", "y", "_"]}], + "arrow-body-style": ["error", "as-needed"], + "new-cap": ["warn", { + "newIsCap": true, + "capIsNew": true + }], + "no-empty": ["error", { + "allowEmptyCatch": true + }], + "eol-last": ["error", "always"], + "no-console": "off", + "import/no-nodejs-modules": "error", + "import/order": ["warn", { + "groups": [ + "builtin", + "external", + "internal", + "parent", + "sibling", + "index" + ], + "newlines-between": "never" + }] + } +} diff --git a/maubot/management/frontend/.gitignore b/maubot/management/frontend/.gitignore new file mode 100644 index 0000000..97c5f10 --- /dev/null +++ b/maubot/management/frontend/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/build +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/maubot/management/frontend/package.json b/maubot/management/frontend/package.json new file mode 100644 index 0000000..e02b7a0 --- /dev/null +++ b/maubot/management/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "maubot-manager", + "version": "0.1.0", + "private": true, + "dependencies": { + "node-sass": "^4.9.4", + "react": "^16.6.0", + "react-dom": "^16.6.0", + "react-scripts": "2.0.5" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "browserslist": [ + "last 5 firefox versions", + "last 3 and_ff versions", + "last 5 chrome versions", + "last 3 and_chr versions", + "last 2 safari versions", + "last 2 ios_saf versions" + ] +} diff --git a/maubot/management/frontend/public/favicon.ico b/maubot/management/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c74a967fe3a1a6d629541e1db50e062f9c7edbc3 GIT binary patch literal 14846 zcmeHN2Ut{B_W!9Uf?}l%7DUB{9RU>;X%0xh(Lq5<24TPi6dN$kP==25K4M1^EGSqI z1qB;+F&b;4l2{T;G}$D^B)i|Y$^YEBxwaz55m z*ck4KjS*fbjt#-u*iclZFGE$va+Ib;pdvFIRavXBKRX-;bHXW&!2X;F9N`ENnK$CJDMwRH8HIA3Jzk4*T&TnO6Lq+7@)$0hI)=-Q4QM)Z9JkNb;r6*Y+&Om? zcg`QfCls!pKaJDZ?%>q5PjLGBr)a!>7mYXW;S7bdHz~b&A7^jf$2l%EM*u zaI~K}-DaxwXxmxBxeJq0GjcQ5B(IL}63t0*n(Y}WQ$)*UvK3-?tNGc^zA`m@7MPDq zPn#4LwM-TszA93_e1iV_MR|6XZr&^3b8(&LF|=!MNs%h9Z!gpFL(B#b>e*4Z50w*) z^q|#6yK{$5I^9?~zF!X=HQJVSiPpW3AMp}{3UTv~>WduwpGI3CrTGEu)N8C$oNpn~SphV8j%C@(;1 zSs6-q?!t~ed$F@>KepE#N5#P!>^X82)fL-OTeTDQ2lwF6(R!5aYs8MqOW0Z0h`ls7 zR-L$j1IN$eV8dk`t-FPNCokYo<7L#JK7+b*S5SZX2F~o=h?9r+;nE?R^XhAG;Y2My zK7IgA2fjno>0`Kd?gTeyUA}f5mv7(4Cs!}v+VvaUoYZvpAwK@>OWb(yl$(=2d;S-E z{^C14x^opzzIuu0PafmZ*RSy8yC3oF`=9Xa)z5hS{a^6vhad3c>%Y;Q^h=wI@hW6jOSjF~uj`t;c|rV}QP9W!Rs$f2{RPO`OiahWsE%g4vZd!fhN zIj(bDTx>0^XX#puww&hT;yP#UToI87(=4rRt>2$wX>G$5HIcRTj1kjpB{N2I1Xm5& zW`=FQzHY8Wx8=n(6)q-y416UEW(=ApW=*%iq^p6cuf)&Q%v@weL>t@Dww5j~Gx~Sx z%;*w{y`G7{%GP$^w1GAQZA`j%)FQecqxb2o)kUwLp^1@kH|>sE9G&CW@2RELK})M6 zL5o47$AIWXbtJksBPFE0{l8%`CEu7!L`wRT!p;I0F=Z^Q63kBG-U&}$2>qzN-Rne!>~DN1vaP2u`}@l>`J8d zZA}>VrY%F+npN15wu<}+TIcd(ae(}Y>O2{03ZrPfi$!8e3bNP4BR3-%dFe?g%t^<_ z+*IV}<)EM-A9-unL$!WA64#a@bwfFFN_Hc+v=aGc`>?UJ92?7aW6Q3+Q0+Vb)vju6 z*;k8_{YSC0C zd-h@1-b!rSdl1_XHDLDTo44*H+-%v4gnA*2`11XuNV0 zXRqDn_V*XB-@&Cj&A9Z*=eT*P5x1{j!~N!Fur>43v#)90`~shS{s1>0K1K7xNBH#1 z7ifO|4L+y6{KH#U@Zj!kJpSSfJb6z0`Dah@;^j+zE&cX2x0XJqwe(xs+kgM^IbOZ| zJ6^q_HT9>T@zc*g+0a(U^jDG8_cw`vvY8L@BPKH4+I8=hDJt4{fvwZ4Gk&o$8tvfjHzU3sBb?{;7u#q z?_r?dxZ!=L@R(|CGi{oU&D5z=r&!xq+f1Hn-Cxi@R_G~Wm^*(_fHW{5Kq{36NCOtl z7l}n87op>F?rlqb= zN|-Uk)5t{ ztFzW4Dt9B41to}!uWH%96w{e!ez>37C-*^#!VlSzLCB0+iZwCeNKwj>7QGUAF|==q zU5pKJOHdl^kL^k+G9`DBC;k$tiLpqdP?(~iJ8d!Ud@cC=po~NG9NKcn#<4i>{E>^9_mBZz@Qnv?}_I{5m zCy(Gt<1ySgeHe|6XK?v)6Hb406DKd-!$~>=WoMl1T$Am8ZhdqKH*Va-^-r5|_wGGz z4|Id}KWy)F=kaqiKYD_@PhO(=8NdH|`uSbFc-)MKkH5gfC(rQZ^A~vb^eJA_9_aBm zuke)46P|znBVK>^4Y%+4iS|8z{qYC<{KMbh?RzxO+JA9c{9lZsq9RpcVGct!7Z>Cg zs#HXM!{(xb?BuwZ$jFF@2ybt0J@6v%ivbZLA|fMW<1%szm|Rizs(B(8XXjbYvk1;Y z=k^dX3elZ~LTAz31u;2sb9xvG+QPVPfy*-rPwipoQ8>?-ouK|Pxb<8aG)}N|*&oFy z({!7uy-i2Dxs7ylAK~UU!rgs@tGoMq?@@4fcXKDYtNSQdcd8xb>Nd(uu&mfN*lr>n zicFk1ew?MHrIm%1HEkEIr&%))!P3ggf&93>A84VBB~GDUn+>YA)I z47s^$)?}qpnwk=?pgKA6gak{(_6bUd&?p84Au2v4uRU^;qKVA?6;aU&sa0#h+Eqq9jDvVw$|RCuZd4cN=Z#!ladr4ui<5Bu)V!F zSP|?{yrM(D)gMTtp}`Izt@WIFZHjWSt&|m`ZHtTF_m|o_*xOT=qnN2}!1sy>!FEn^ zg}<#sRIWpDagbC(nk5oeOCp3QIUn;W)!C79@#vvJi&BVRCie4Fj{r5m1ZcRJgt{@g zlYMBE!goLqvkV9E2gSw74i5IgY#=x;V&X(7R+|$nCgI>Qri0B?$x6DVlc$J7xY6XS zkn>#ayAl;T$<%zfnTmUE7WvsrCDHNmYIR&g6t#uiw)Y4(HGhscRXW8!B{xZ-;94Ou zYDjH6FC=&9Eil*cE5%Vk6K163CMqP3T(#OF8Y|wYs8HKJ-Fmk0XE`{`G?^Hm8y{-R z>z7Grto@_p_Ecmx22bj(r^on*^Y$0yrwtkylAv(#<v_lm(Hj0ogVm3sfPGh?3 z>FKoUr#{CGGPMbD)TXPzVBa}JK3Q8Hsy$V%kV_`@=|&q%;%^zB;^MSHgZg&rW72n; zAG64&OQ>vemr#YawnVJmmy|PsHu{THX{JL=P1$>uUSp;?`c2mUFhaYtMAAh|OJ7Gv zy=x_YGxhiv7pD&zX+D?<=nT^7rQdh-z%Bzj(!n| z9z1{h{-zBwH|Go@7FMs}W$pc@e&bPYBh(eYo#?b)UmDMJ;vfFojB2WGkCyAJ=GW+F z_P-6?-_`%y{CZ5k`hMW;c~d_pr>l|v|Mhcbwer7Zzox$$7D{#LVlXk5d&~Y&f*u+O z^ytS$Jp_7c;-ELj$8A)5xVm!Xks7L-=K12r(>?4>xV}MJx^zU#{Z?Q$c;qk+BPpf7 z8U4*zrUfI04KrR!s(GX@rxl3>4$CRKTwS6>on0PYpkeK@V#PB}COzvT+#bvCV{1H}9Y{rVfO7cZ zq^}M|hGHp_qa%r&q}XQK$^Vrf4a)MoF+eE*PnCb;$Gh67lg$WXDG!D=CJ1 zC!LohM_GI@b|i&jSJDUEd+F_2G1!;2lKbX|`L;(??A>%M;uGSLuqFeUsfmcsU5E7C z0wfo0MjrXG2{~kM@(Co-Iep6da-?tE!TGYh4_n3iue*8wbyHOxwj4N$^87^XqI3GH z4Vm1xLCj~ZFUi2Rs>3Ly^Y{9y?WnFkgxcC8XlS5v@n2BB{Uh>EPhv;yDO4PzZ+FNy zJyg}q`KML%uDXVu!=Gv5&fwYkJLik;%*WZ?g*dyfgnZHsXsRy3xoYxDtGD9vk=60i_5qQ8^XSAx=uF=pIJ?Xi6MTGpeSQ7>=r8f39K(EHBKnAD&vuzT+u6lg z=;&xSeTwe9Sk*iinUcPBW1vK?Ow7p1%gf<@6y?$J4vW}XKu&6ck{Facb7UmFS(@#b zneN!~eH>q*OvvKSgt#cLHJ=)%mgL!}EqSC^n6Hxeah1xrDouSG4Xk|*$#F87R3pi! zQXj`25|wJ1Ru?;GAL^~IUkigWE+s2VZ9a)J!wDMwEHCvDIyn0HI?7dE9XfUzKFiU; z$;r`P7{KZ{DB}{784?pTJ=WBa<&k0sM=|lr#X?ouh>o4#m(yrSWm0FS786>z-^wZd z9Uc866$+}5+No68GZ)JIog5vVXdwTHxYX6bHBu=H@Ci^RCz5=Ti@(&tRT|)G|HhSg zt6`2Hb#e-jDW$W!&2W_FB$K>qp0Cu0s+7Lozb(;@>T6GzOBN}dY!ntD%v$Y8^1iMC zV*j>-+fJZeL8>-VsBJ6=%Ww=3N=bh8Ebm#f{z%VdGFPVnIslWq>KI!Hma10H3S`r2 zrlXIutMBg{puWAy5UIbE-Q@n=23uN_yn0fO@8v}AN!!`^I|cq$kWL?HA+k0mc^i{21U-&HneOjFY!rL`u4I{ z(tS=n^p^f3B~qo-S=&g!e}_vkIl$w`nY#ZIj)QSMD$N~x2Mkw9R@+Sl}d zk;5#o(%G%U;%{d{r?$qH)@|A^l&{VR)rWU-qAb#B zwx;<=dm&w@9?mkkj~=@_3GztSo&)GiSD&-LrF}MxVFm-8WD42r*5}W^hdtdV-R)%x zaaTu9TIOf3-B(~B5EvM9d z+;051_KhbDuyN4sZ_?9=W+8S+E2LKCE+nc5(9;Wu^mWoTG14c~1@v{5!Q1izD>I}1 zq~2(N?sO*~8at=1Lb=?zd+z{+w0Bp>={f_9m@tDGFW;{JO9j?e+TGdNp{I^Xe?zWqPTs)4?Ct&=TZ=>%tOdqrXxO)ZKY9nPqob{DV$#1a zeW1tpoTVE3-)x`BtH+1w(5FB9qJcWhwW^6&9e4k<+<*M|tJ^0r#?T5*7tvS;Sj6GC z?VrSnax7{-wLK)m^tM>{_WcuTY;7$fc{P|g1wNrxc}dAue4z5)ev4)xX43w%zk7Hr>o3)cS{tY8}ktOYZIS?z?FkfmH8?wj8jLo0Mn)#pW zKf8X`mXjt;vY2FH!BQ5eWMRo?S}2T4-EXsgFnQKKX=p`DVUB1H{h2^(CCjzW-@JTq zD8DnN-7dHBWf5V?T#ME%Gf(2-?Y z%SwuCcrTsiB4&^cdF-!;jen4_7o#H}rX>;sL*S^ei)drY&fM33H~R~uQj2E-7J6TH zC!Ddwmraw9bxQ)8%`g4E(j=LZ=@=0JS}}91*c^g|L24G>eXt-`z)jsE`SqCl#PwLj z;%Z>e`SEFHUfx;WT5V95@6FNIc%p~~mS%4wpdWl03J)=1K|=-lb%+Rnhz2kVfM#Z* z0}%cCSr35Y!c9QNM5z5RyU6pmI}hJC$jlhGZz;>KkHFrEB;nzdbK zW6~>eeGDTqQ;@Lh_jlGlVlzFjJdHFl#X>1S^;|DSLh@{?f6)$nW7K7Uoti_ zDn?B*WP|M%6zSQyr!KkTXIE#+ds5?Hj&2CapJOliU&D}lCEnV~x->uZEz8GVSwFv% zAKu&`7yFgf8X@d{VNNq=)G251NbUTI8&)fBsEu9;$n!6iujUMGVb9Q$s!ID~4vOM3 ztV_S0%`2YSuN!$CT={0I+vZKMc={AX?4$uAB6FQObR)d+Jj8=Z;v(PA`vg+CZf38{ zRP^~=siB!QqzK{vzJKvqz@HQRVt$DJ)qQmO!)14q4LZu}!)VRylrZJ`Jh^CjM?MzB zg|RC2Bam_Lw&gC2-mwZ=#>v6Z1=k6zrCT!U02!^$kNTlt@krR_20%fG1yInxXg6@b zb~lE)O?9Gt7sTBTut^JnheV@}?tPuwOucb$?w0M&SJzY+wflCs@mv0~n_IpQrgtEG zB!!HLbr@yAv$Qmpq}Fo)I?V!)3g8lAC26YWC_XG6{tK|PVmGiqqLosW_A#d;`G2Zx z6t?7ds@kjtQwut*@)@f1h7fp4IV=&M(#o&CzEJw3atM}^3?xBFFgH`o-h<(nuZr{;>|TS##P<-_;V<{EU(I!D7p zGWE@^O4TiVT*lU|KwxB4fQ#4!1ctyv8emuegg2|xg249z1i&wpdVr`GrdJ0<0KmRY zkGU7d7(fNg;B^2rV~}w*fC>Sz0CD)n|9|8EF9O?>t@mS?IxC}XpcN2uJ;htj;bmWR zh!L1FNQvZvcDjH*_=`1r z#%2G13)1UBRzz7Y5Xm^Wi8Bg*ANyXjQ^_;xBRhc}Su7=-nOKxq4_0WP7xhha=}<0< z5m)ltV)w?tKtkd|^7hDz+#8H@sR;1?RU%YJVL_zBdRzBuN&wu& zg^R$*Y$8_6HMnGw-HqT;Cm1cDU+H@2UOK!NKKIrir4~p9FZ@lnZH9RiFd{>z)lwK3 z8C}$n02F^E(;ofrxQHdhq67=}yz8bdqd_XBra0pjdGCXIW)4f>F3Ia~ad3WTCW&Em z7+e_$8rZde9KxUgd(`gi;Egadl)Su%937v&pPw{Vn6HjvK6QPseyk$!BMQD7j`=5y zu^0=?So3g~98&{e+@)bMV^`aC`UC)`CmpjLn>_^}B%DTFA z-@*2ds?c@;LF)zl+}1t;>vwX+X?i_P+t`+-H7jkS67tYihh8EW-s!9M z@4vrM3)X2O_vE6bd#`S(MPOkb1%EtduX@2%f>c5F>@I5HXpiKV<0k@zSKZF8($ovf zAY17eo$coOW~ny5wTpdsOc(-Cwaz*RFX?6%w<9`J@}gT#^H3qota83t9wiCL+VL=n zC#Vy0#l}~eACuC-XoTn6cOli-R>k2QOun{?a4B`(sqejO#Y~fSSWs_@$ zwoguK3xg5%Xl?I)?vOLdzc~-AMG(S}p15kXPE6?)8b=obs=(c1*&?xRQ@&b{h}I%;$% za>%=Tgd-%5EF3d%40z%0Z4A|x2WLkm%ATE0w(HqWJ@VX|{}fM`S!yqnTK|4p3A_vV zl9$YA)bHbcBS&LkV_)zm;kJHVLc`R|D4_|Z%=YR(4B>)tmYaQ%$XcSnXc=5gx5)S9 zH%87mU`iHeO73l7nqIP47FkL362PsfXo?#dVJ@{1{Tps5e_9yeeltB-4>h;gpRDm( zF+1|USUX?2)l-Mo8a65kydQVo+u;xhy44tXjw4G8=n|e@MCKqun~4*foZ5Y{XFOA9 zU5JA3g?xgUT;>z=xJiFrR%Kq#n8#8(CI!u0+ItmN3ttb*9~H;UdM+9{d2>lpO%R^? zG)0^2*$Lll|IWmfIecoHF}aidq)_4~a^<5w6e|rlE08UtA>N&pq}gqnv6Cs$*L1zc zn%o5k0e>a?!`)`rmP}hIss(hs&;#101v36~H(0U&2}yQEV~5vH5uR>}+h>FY>)=pU zL3-`(vlHtX2R%a^o}sq9k3rnR2^?HqJk|KYwF3MLOmD{GDt9EgnsPk9;vAeXM;_O@ z9+2-h`6YGAUhiE<%UN)wg3*4$-FjK@OZS_cEG=dnyw>a+Z}!72RFuhE0;9OY z01+6ZKJyl&Hy4LQ4;a>c98%6tw^MnqaBZt<{bPraKvtQ5X&Z1DZ7fevH|u}EO62Ms zt@xmF(LZhWTAU2=t87HNI; zSj6gxq{|TV`F7k$a;2`RGlYQMi1@-uHXaoV6HVa+=7F$2=H%3z6Gk&!JKg5gp90B5H{R7?B&CO=P)uA-shF zVYqhN`@MF=@dN&>_XShrvE%XKSxT}~C<11joQVtac%C+m-OKM^&0gzY46HWA zOr#2V`a61jgiVcv+BvmX{5vYQ3kA=6^K!B8LLWp4?g-}-Ec@DK=+R&6qQFAS>SCw0 z!+lz~@NWRK3Ab@yiMm5Dl6ZO1vSWlqgxT{c=~?2;?%iAa6OzkjHT?=TesZup-Z#jbR7EYV~LXvTRIb;5X_6xTT`SBn#gHi^ht zk+4~> zeYfLk5TpVYIQymN#Ao2V1>ZhDIZlvZ4F~KAjNLj*;v(4ZJr*@E6v2;^T$AL8`em)0 zP3BSYqf9wOm5DcTP?;gWO-&jGT-=mIQpY_i&R2zU1QX&)z-A!6i@(>e0Ya!gS^i$wDf5jNfqp;#g6 zkb&KKe!ZiHq+p(ap&gM7B(K1&3m$jPVfYQUlL1;fIwVaB43`&8jfo=o*t9|aZntvf zVJTFh*DG5B2CKy7MF=s0dh#_}NL2%jeS}U#{ zaktrVlp`2Zp6z#^^r1^PM%=__8UL@C@@Cio#($`z1YlJ+vg|+h!(UN|volqc{`)6y{>a<{PF`NhQn-Qk=dAsB z?t(bd!u9kvSo1!4P>Y*yN1i`PYwxJ3&m1K9iP}Bt9FPn;@fDSHhNMYi*N1+Rx}G== zw#3h}vkdw2E44Pk(}-z)8_vyvfxu)jFX!gl9;tv%}j02m-2JiMh@hw z#U0PzP;n6gA<@^Kk5jY^9M2d6yiRNO>`e} zA&O31(-GVA=+ANfDAr27`B;i|!}wDr{HBv7tx`?Ta~ve8@chUwT?Zr>2wJ_-wOcO4 z#@sdV#>eX-KhlIoFb7+H^4v})zfv(a8~GU28U?sr$^R35Q`&?gsJeB!x2Y859IAL`U@Rp(J(4@ly1+>x zr;as@iU$)=1#NlBaHK(LeXG87#$j-?t6OT@wMhkX60L+C!B4mdMxjN>1Wd4yEsI3c zUONGt^p6&Ub0&#XOhh{^G@fR|R#)!L-~Xclk6gC^|8i5+^D}|Y#0I2r?nOW z-xI?)?f6Bvh}p^3sZY2ybEYL*S=m0q?+C{9GdckB^R4Na`XL9W4gwc6NQ@!(@_5x* z3bH62DMQnZhZUo?KB+XzFBBLnk=2%LfhE1t_b&ZE-)rcSXJ^29)ok&{rYnnp=DIK? zB))5xDAKRAe-=tDZ;uUk5q#w2Wqp%=TD&NBN8sG+d)3(9WV+B#19oWZC)j&DMT<=) zt-i*!A1WM2KdqlUjoE3WxUSj>I`vV1%M(`amL}z47kwIcZ$qg3@xYV`vZNf-ap=IR ztim$p$|DYpW{4}_W>MQ;ejEvx@ zLTMV8X*-G-M4vh~E;9>`?{Oy}%szK~AP)9s|Ge98 z6;?w-O(_&MfHPN<+wGcaayRw#{I@JaXdfK@D0kbr{K&VWVfe}SS)kLhTlfPE$vu7k zrl)<(EQZtujCgdygMnlTstHb4w47r`a6rx>BQRnA=+&?vq`uDHamd?Hol>Woq;mfV zZKwGIz|KHZ-%${GF5A$5ll1p-seCwNio>?Kn|dUTBm}OtDcV;BV~oDawbN z8mtI!ynP=1?SAOUPr?78w9a4Aa#>YXa`XJ`4D0mSan-CKx7%yp^XQD*^Y3`w=Z~6= zd&Be2V^l~CB((+(U$)_jHdk!Ikn>YUFzGiGTh=%j-4Hl&VN)u|R$7>8xf(uWXQaVg z^!L1K6%L(BX&3Yi1U*y*F?1Y;h_kLQPEp&LR#;aL=1@7;b3$0n$30T{_nT1x_N(8KqmQ4( z*Ox5Czzi4GiB*3gYsO?Hd(x@m*;DX8*y&c_h@K-=Aybem5076?x-OQF#aLJ(p&5w1Hunvj{MNdg7K3wD%KSd_l^DZxD2}dRWTf=U~oyPTEEK{5Em3Hs(ww$ zQu@%gm&*=x0SD;D84bOk;3o0JQpFN&n1MRSm;Tw!5_dQ?D2Yozn?ug5FUx(p#j`=dq({6_Z;W{QE;m|#MDW^fL2^mi`+x*+3A zi8sjXM+BaoneXB^Y%?Tc4rSX8_47n<_b_`2es<8DJ#Q1CqZVcjx6~ZWv$j-21>S(a zY+e!@dVb`0ompq_nxwD-LO^&t7>lN%`ptS1kzflCl!;~mXk4GgQwV5f<`7V+rVh^1 zGGD@qJv-tF#;WLK;tu!eyMWH?G4(44-d%p1b~tQd?ZzFt&+>c*HU)RgWoT>E45!Wh&1bZYzWE;|uF5&)O&riVE%bhfe#XRj-L#J#+dvb2e31bmB;T#!87m* zVj4mCxfiJO9EVhgD+c0U>UOW^<{<_(Gg)x1eZiJ+b=Uy_CwKarP73bEid zn0r*92+rjSe2tUp`OTykjq9rzc3Y%sx7|+UzfG0V! z-K!(NriZCp?RP;L$>nwfdNH+P3N+yp;3tCL{Il5+8XKRG|A5u8&C`A^5Y&hk*nrg& zh*ew!$hiG1KfHfWa_6ha2*L^vs^y3I_3+pMtG&`0gA(H(>fJ=_g>};CJ09@;R%V#( z-&K|DhbfKT12GIZ(_gSUh28|HgKi)4K2VU7{+YR4k^n6yc%(h<_W}%0SfFz8iCzs{ z2tZGX0=5nM8tUhzI?A(tu`+VB=-yvc`kk@fYoE7A6LQeE%j{HqveJ2Z9}qX#=N3U% zq02Rq)dATXGXgvAOMG*2*dnbnEq`{vpRKz@ETN^JIyvGbrX?Kvf(;qZ=A+-E_n^s+ z?Aff%bs@y{@V~gN+l`;MT$xj1$7hpEGWiA5WzH@2zO>1Gkkvev!%&orD5cvD3I+YP zL=-}HFQ}OoH3WJI!HwU!CE=uLfNsvkA9;9C*YAFjc%)rd1Nq&~lq+7Qm~g(9GtXPg zPL=1$lsjiok$op<@m?f6pfL*2+=@JIynC&e%S(*Ce&bvI!LV83`h zVS3|n6CicNqFaCPI6y+5_^vGCs+%HI#W)O(LezCwRFZ$;^DuZD-XDm(JuT`B4u+f# z3kPF)D!7RJJ%}fpm1oJyN|BuV2&a+%#zWTMEGkY!r*t!p?Rx(%d$Kq-{=1IEOhxH& z#{g$+R&(DjIxUkufoa|KnsJT^A2{Ua1XX9N>pWE&8Bb)}CF~zSsh*JdNqveEvn~T4 zHX@swnN%e9F>D&y93r}d}sp8C4acYr~4-PJmpIowCDH#njFMCNvK5e%97 z#&lu8YUnwWVHy5+Gy>gk!&_wn5PN~d{G)DnRuwHT%>&|S4=QolpKYQim$eK(vu241 z&n}S(;0rJ|_bgrH?!SBtAkBUgr*~bh{dwhiX!$jaZMWzRm$#{)pg-50b#6Es*OWfL zSJs~#Ap%ZRtejbI-XTs(^gtkvY*y)GjJSl1z|Bmwhv#AWk9k~3&4lAqEyH9`P4|$- zDYH!bOse{{|4ZU&5BjIk$QeKU-CC!>{qV#~x@P~kkYtIr`yuDC+g=)pI0I}jdCET4fy4VP<%Hq-H9*hkqx^I6sMS6Ff-$ln{XP?N#>w7XkY{j{IDB4kUWyV0SL4iz?=l_w%4`}1s*En&S*OdOC3uJ3l>MYpu_GD8b z8m6~==gRC*PVKvBxkx+CIdmo6oGHM31_PmsG7{6jX!Uw3qCCHcOB&S}mkjgex_in0 zC75rXSrOAeBC4kW&Ojjl#M>z0eXl<31y@03AGpE!!BqI2=s%m`fdaEtS}~u6D;MkZ zr@?N0TQ`kN0!VtZzg>KMCq>0Mm0Hz&b@9(4&P-uJB5sM7i9|z;Cx@My1WlkoD3EVY zIR81sUCg>KegIR7f@Q4YYt>o7NlpXmov@*wiqc{A_cxNtrkO*bdgHTHsu<~Dxf*Y= znvmIO@yMAOoTTyjpHnKOVMiyrcMi;y>3r8@g)sG(ilmEwje11#DQYW9eqNBQKinw8 zx)Lva@)6PkA+`RS@PG08c#E9pOR5Ff9yU0?6yCmiFL!{FwfbC;&~Q-mJm;n~}_7FWaq|`k$MTW(a-`ZG<7jm=~RevRJc;Y>W&nW#K<}l;q?RHizfM z`Dn>PPaRmP@(Yhn+)LFG1*4f}1YVR>?W+d=>ivNBK13!x=B)THJ`>i?!~7k$dwmLp z$bIld-SAu2J!tSt{C3_yegJE+{NuDMX;Bj!ng1?As!SpnTV>X{Q0W&A95d$@T9RUv zU67@aSj%d3alRbA3E0`5UB5j9C3c+tc@`G_-_xQxryB19I~In}6>9q0CoZcA!f=W! zTS+!p(SXxAK%7(?1Vp_ow=g%=z#0qV5u~fU?R1oul%%)~Bp(o1iezCO2!?YW-ORx3 zV6`5m&8@%>#|d5__wqq>xig5r&T$8APOAzcUFP(+p<9{Ls$9*pe5T4xrKE3}%(rDH zbLzf42NnL_W&;3>z9m>c36|g~FnPl=8ZO2d$oYCQ(7^3oe+)!>RuSg0(Xw6 zoz5jymS^C23SdPGMlK0;mYn8w??YTSO`o^ z0~~lhv_>~$hv*T5B0T^tCBEPg71h_;SuBt5v(9gyqQ$r$*uHdf=g<4Lt zj1b0dUKq9=`R4@t*qEqpHHoIqg_BKly>G=enkX<|FdD19WwuHR^Hkb2RF{i$^J&rs zh`MU5J$w7MZoDgfZ}rtrX(Kpb$-*)ii6mA-#ovbqe;5|!*V;xd0v-P04ChJ8N~5ZS zpSk_ftpAnEwYc-)j?1mSeR{}|uxE?@CrTx+NHPEHd^(P>e}CAk4hx5lSwwT1o%{8L_^VqP{Dl5^%I0e{VRi)^z_{CGF5rh=@g6+skzpAN%-CV@$>whKSa1K_zV#WI~d6u-oKo^q|q?) zl5QNl&DmU>`08-a*YLPH967i7vM8xsJNrERehe#aTP7XuCnfaJw^W__!-sHAPA*14 zK2cM#!HatN=5r2mKxyFi?;fCTgt`T%axn)%j>u#%j;F?LRyD@-MV)le(bpBB8dodX znNM)nz*`QDfN^fNm&m>cH&;YtF75={)SUE7PQsz^6QBixXKoPH8L2pAF}Oou`)Tm- z;mL|XGb0uCh@n`Dak!skWqE$Qnj;o>QYK*Wc&A;G@E|pt)!bqENTHX+U28)hYw&$u z!G-VF{B{Fw4y0{4ixCNW2T?s-LvXqnK0>TI3o0`Hp~mj|R=ZgNQCR=1tet%#BT#>S z2KGWzWDb5ezOg`pKQxvr%X`T+^{;e7I1q^3zhv|A%p??3(p19!Yv0^VvTKO5Vhw)9 zxBQVDYpvF{yG7kn|2K|Y|Ci7~f{52qSdJ)IzYUT^69{}I%tA8z8jlp3HL39TBL6MK z3Jl-}9ndJYo6tn0Mw8f)xm)Dr`nqk;cT2MxsTscU`I_6D%cI^QA@Tf?0|Vy71IbBP zUCS0ieXz#_lV$!d zmrgww+0HQadex2`kN%W1(u8SChyP#@eTtBVh9DFf{9vQYf?`kcjCw@N3T{Q3b-Z$L zFg62RY1jl1{Lw6P#E06MKZlb6xna2h;lm(qKBm>DS^H2<4G)$f9~}^$+*pdoja+eS zP-s~QP_=W9`1tHwyPV?ntHFHc{VpHuWl5IeN%pgWJJz6YPxnRP6Tc!aEHxjUue9 z>N>2$Ag6H0Pf{qLX-7;H4iz0Vg;3~USH-Blxlz|iC0Z(}@F=#ke=)X~IR@+*)@4+` z4|!a2DA({DUJ+;cHerKD^6qQztzLe}lieNY(*Mvn(tZFBRayQ?3-uVAz0d+o8EW#b z6%L0RnJPAov9E7#hRa~g$hi|u@OzB_Rit^L2#@GJaTD1}H0f#iJepQ(H-5*!C*_xJ z*;OnpAYtBK=|s^<)W^KWY)?h^pw_v$Z`n|dHx$-h8X|#-%T*FXk}r_}D~<`fSKV*g z%$$B1Ove$np=Q<<#~u}|Ws*_zU$FfnS!@&Dm==0qw`C!Q9}#%)*==uhO6e&H3W4e0 z4<=b>U}i2Z8r~Cka%yp><(vYEf=p32#AYpj@k$m3#0toGj+r|*W(zmWIgZ73i1@B48*69T?!lOTO;yeYO-ot+L zzsA{*dJ}m&laa z-+K9+cc-O@jSBcGqYHSc`aI8R)_vK`!}LGNz6`M{*IcB0SMR*;z`LwB`-_cc(5W7F5oFSGJd1BFbeqR>}b`VsF&$N-WR!;Z$s?4U=l7jaM^XyI|J3_94+ z(&VQ-o+I%V;YZ&&MUzG?jSdfIeou~^K_&<}5KlFpzr71}0|HMk%ky6w_JpVQ#J=wF}r(z+2e9^JwvFfJE)REC7B9z6X@b1r%LHD(L z!V0hPm@(;2_k2tCu5{~_yuFs(1*n*1BAt&!75xg18#_l|2|%d9*avhg&r^{g0?5Y^ zzC7>8aP8SU8kl;UNS)O}M@fB-J)uSzqD-VZ5!-;Xp?qIsI7XP5$AJ)9dMyNrCclo3 z-P!F^hZ;^j7ZoSSywXU{&|6;F+1|&y^)TeV{$lHNljUKS7G=yCvhcL?h5Pe?Pw(_K z5Ki#|QV5Pu+CD_tdjH5VY+GPd3-$ARwz;83P+zncUAj2kf1xy1ufLi&(J)d+zUN3X zMN|J!yrJjg{O20cw~AG@PhoZs_4VC>i($J#jPfYi`>dvx;@;eM80GD&V6_uh)PXLcILAx* z@{@KzG33p-^Rw#8(xEmHTJ0QaBUMS`j;RuCE{->DQ%X2l)-;hbRP&zYX0-cxts*-c zxFJOXGE+R(q=yQ@=YV+1!aS9vWf2YC5HlFLW!}oLs%OZn#{!1%rc=B})30tWC%koD z*LmIlZs|@*IP(0p^8Qbe599)8sJ`+}cSyXZYu_67E^(7VQeXRG0Ul4+em-z-dGDrX zFu*zb`-Wzcn1#++sH}iWD)gTrT6P{ayEKVx!?RvUmDMOs_ABFnWPS9lsO>9s4qZO2 z?n~tlYEUYyEE-s$p}Z=U>t`4U)8eB_k&IBe^lysYTNf-$A%9;CuFO>{e(-s|pZ5yI zm@Fa_dB-nM-1<3rp1=cE_!nO(4^M^8YW(n*B*ja|pr;8#A+j^U=JaNJ0VeVv;YSh6 zXPxPsjn`I$gZ-g!uo-p}3VS#aCcg+0O^d0VV9xXwsb-8pJzc6;JC6tiGXZV<(K9)lYgJq0c>$g#u)XcoMC}`-% zU?0RBDA}V@7+Cm;Z9KfVfH>bEqocX^<3WAwZR@zdSAtAVzo09I{_4drx@+vmGyGU5 zhty@^t*|~mENaTy6r6g3eUAnII){>4*;?e~!F}F_+tdAyUy|`r_lD53WZ&vT$Zhvl z{@(P8#0}3`Rjc6Et4aDD@!bm7_wjg#7$@S}SJ9joXd-kEZz5Rz>HJctVnQs$J6;Am ze{rx2?>p<=r2+g4dm#o#`8f04yH}_fs7LSIaKpBlIqOCnPu;GjR}zz&s(+CQdI@#vBys84pzF=@z`VBtO9=)| z3{gbT(%F*?m&87|&F=4sxJP7`WZ0sJ0pR?G_0u{8P~Xb>^Auv=pYBaFgZKTcp~O^& z5HbuwP3<%k_&M#duc*w7taB@0WCCE>mb{uoE}*gB7+42$Hm+|lr@GC8XSj!;PYq;4 z-wsxTz9K)19tDX0*~cuaY0TDLMkD*j>wEQ9bR0mrau*KRKZ#ZUtstFs6!V6PuJ#BT z@3*f#IS@i!&;JUlNcmvzniI=lvSsQ?SN`6EaehRMth^oDRrOzg-yLM3mMfaJt@E{^ zJ*fCqRH^&?*yZ-={Sn0^rY{b9R^+<(Z7Ro2hb5tV-)(*y}HvoRVo{n#q=wy4zdzB8LLu z%k#XE|8?SxNPYYcOa+Z130P9n#wTl)#G^7w?I_mlnw;`jXhQY+p-#o3)#Tlb66=f!v{Ulc1iC$EK5_x=?*rG($$ zRLEL4=v&qN9dT61%T@kcxp=?8@?4yla1Wk9(J@E$DMRY>bKe)dJ=M85>MZqS<;veH zaK;(llY(GLD^gt>mDv~c^sI_T`Ifds%E+wwe7~J9p2ecm+q>h5rOdLwMX}i_X3roX zg#4zlrj8JrgB*-@6Q%caV@!o7ad@Ugbb(DCH&dAr1SI61Grt?r5mY-yFpYvPNfT{a z!oqwV0z9_~?NNlOQC{M=9OMj~g7;00l(a4a-%I&uWaLZmX%IF5V1*o2ctF4Fuk?7$ zJKqii-{(OY*sOopX2KM{|L|9l*2Ax?Mm8wk_BNPN8Qw=jn~CYjDUFfBgPaFI2cCWh z_lKzMP!tdn!xaZ=Z%WqU<;yBsKaq;xQ_h*^ec0y5I)~=TX9D(&!A3Z)G1`Ico9TwseTin~nEB^7c#~kK zKnd#7ISP>nTLK1>BG^g(F8zP=`E&l{Y=n+ivy{eNCy586^$%pem4UWT<8k8h`$QOw z_xL|#ze+UP=r99BIGz0W^2a?dCwn{BCrO__;^5|R7gtA3tEDdD7-z^rMtKw*-mOX- zKxXGx+pltip3l~8TWsXdx=MbdmAEd$uD8~i|4pIadft9h+ILo%L9GwNxB29=jx#90 zuZPG1XjzFBeJ%94=fTK#9EdO}Ly&kDreA+acY3WH+D1N@n?Vul*3nA&vM0JQ1Pkg)HXpgl9;r~(R3$k+UbP(itUmY{)6XI-l`=ZMTAwCkpG z!Hc!T7G5JE+!Oo(|IGvXPQ2d%`Sb)kk4yxyWars=nn|5Iwe0KJ20AWormnVMSCf+z z-vs%tZhq#N*z?D&XS4s3x8-X>BjS41jQ5#P%>9X_ndT7Rfbs z4dMy78V>O{p)z@H@Z9Jpfp{bde*Z%>*zhJVVTP5Ambw6Qan|#g>(U0MreT7V($5<- zpXhVR;Ui3>(Jha}mniV=*~W2h%+t3&+ZA7w+YxBusYQnen~Xt1)|BT_{pu_^M1w7f zwoID0b3Rd(#E35qLz|NCa!kH7;8grj9fl5Yg1>8_3a=#|f5}d5SjBjS38R#1q_z9| zyMZFhgf6Eq*+Q&QixBTR>h${3oD&6-X{SW;Swp<{SaRI%ri*LsO_1{790GVRYjfh+ zPHoiA$HkI7Za*dzG49^^B>wLeKa;@UX>I?0)8Uu7HK~S0sm!h=v}Nhfx&a%oj~(rs zO~2%!DR_aR@R30N*s|hvR1C;8%O^C;&X~CU)tL>5;VQ_D(k4<=QibH?KJgBD$qq~N zX%X. +--> + + + + + + + + + Maubot Manager + + + +
+ + diff --git a/maubot/management/frontend/public/manifest.json b/maubot/management/frontend/public/manifest.json new file mode 100644 index 0000000..a76f730 --- /dev/null +++ b/maubot/management/frontend/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Maubot", + "name": "Maubot Manager", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 48x48 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#50D367", + "background_color": "#FAFAFA" +} diff --git a/maubot/management/frontend/src/MaubotManager.js b/maubot/management/frontend/src/MaubotManager.js new file mode 100644 index 0000000..314f47a --- /dev/null +++ b/maubot/management/frontend/src/MaubotManager.js @@ -0,0 +1,33 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" + +class MaubotManager extends Component { + render() { + return ( +
+
+ +
+
+ +
+
+ ) + } +} + +export default MaubotManager diff --git a/maubot/management/frontend/src/index.js b/maubot/management/frontend/src/index.js new file mode 100644 index 0000000..294a680 --- /dev/null +++ b/maubot/management/frontend/src/index.js @@ -0,0 +1,21 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" +import ReactDOM from "react-dom" +import "./style/base" +import MaubotManager from "./MaubotManager" + +ReactDOM.render(, document.getElementById("root")) diff --git a/maubot/management/frontend/src/style/base/body.sass b/maubot/management/frontend/src/style/base/body.sass new file mode 100644 index 0000000..b45c068 --- /dev/null +++ b/maubot/management/frontend/src/style/base/body.sass @@ -0,0 +1,53 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +body + font-family: $font-stack + margin: 0 + padding: 0 + font-size: 16px + background-color: $background-color + +#root + position: fixed + top: 0 + bottom: 0 + right: 0 + left: 0 + +//.lindeb + > header + position: absolute + top: 0 + height: $header-height + left: 0 + right: 0 + + > main + position: absolute + top: $header-height + bottom: 0 + left: 0 + right: 0 + + text-align: center + + > .lindeb-content + text-align: left + display: inline-block + width: 100% + max-width: $max-width + box-sizing: border-box + padding: 0 1rem diff --git a/maubot/management/frontend/src/style/base/vars.sass b/maubot/management/frontend/src/style/base/vars.sass new file mode 100644 index 0000000..9dc77dd --- /dev/null +++ b/maubot/management/frontend/src/style/base/vars.sass @@ -0,0 +1,28 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +$main-color: darken(#50D367, 10%) +$dark-color: darken($main-color, 10%) +$light-color: lighten($main-color, 10%) +$alt-color: darken(#47B9D7, 10%) +$dark-alt-color: darken($alt-color, 10%) +$border-color: #CCC +$error-color: #D35067 +$text-color: #212121 +$background-color: #FAFAFA +$inverted-text-color: $background-color +$font-stack: sans-serif +$max-width: 42.5rem +$header-height: 3.5rem diff --git a/maubot/management/frontend/src/style/index.sass b/maubot/management/frontend/src/style/index.sass new file mode 100644 index 0000000..d3d2eaa --- /dev/null +++ b/maubot/management/frontend/src/style/index.sass @@ -0,0 +1,17 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +@import base/vars +@import base/body diff --git a/maubot/management/frontend/yarn.lock b/maubot/management/frontend/yarn.lock new file mode 100644 index 0000000..64d5190 --- /dev/null +++ b/maubot/management/frontend/yarn.lock @@ -0,0 +1,10103 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@7.0.0", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" + integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA== + dependencies: + "@babel/highlight" "^7.0.0" + +"@babel/code-frame@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz#2a02643368de80916162be70865c97774f3adbd9" + integrity sha512-cuAuTTIQ9RqcFRJ/Y8PvTh+paepNcaGxwQwjIDRWPXmzzyAeCO4KqS9ikMvq0MCbRk6GlYKwfzStrcP3/jSL8g== + dependencies: + "@babel/highlight" "7.0.0-beta.44" + +"@babel/core@7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.1.0.tgz#08958f1371179f62df6966d8a614003d11faeb04" + integrity sha512-9EWmD0cQAbcXSc+31RIoYgEHx3KQ2CCSMDBhnXrShWvo45TMw+3/55KVxlhkG53kw9tl87DqINgHDgFVhZJV/Q== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.0.0" + "@babel/helpers" "^7.1.0" + "@babel/parser" "^7.1.0" + "@babel/template" "^7.1.0" + "@babel/traverse" "^7.1.0" + "@babel/types" "^7.0.0" + convert-source-map "^1.1.0" + debug "^3.1.0" + json5 "^0.5.0" + lodash "^4.17.10" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/core@^7.0.1": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.1.2.tgz#f8d2a9ceb6832887329a7b60f9d035791400ba4e" + integrity sha512-IFeSSnjXdhDaoysIlev//UzHZbdEmm7D0EIH2qtse9xK7mXEZQpYjs2P00XlP1qYsYvid79p+Zgg6tz1mp6iVw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.1.2" + "@babel/helpers" "^7.1.2" + "@babel/parser" "^7.1.2" + "@babel/template" "^7.1.2" + "@babel/traverse" "^7.1.0" + "@babel/types" "^7.1.2" + convert-source-map "^1.1.0" + debug "^3.1.0" + json5 "^0.5.0" + lodash "^4.17.10" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.44.tgz#c7e67b9b5284afcf69b309b50d7d37f3e5033d42" + integrity sha512-5xVb7hlhjGcdkKpMXgicAVgx8syK5VJz193k0i/0sLP6DzE6lRrU1K3B/rFefgdo9LPGMAOOOAWW4jycj07ShQ== + dependencies: + "@babel/types" "7.0.0-beta.44" + jsesc "^2.5.1" + lodash "^4.2.0" + source-map "^0.5.0" + trim-right "^1.0.1" + +"@babel/generator@^7.0.0", "@babel/generator@^7.1.2", "@babel/generator@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.1.3.tgz#2103ec9c42d9bdad9190a6ad5ff2d456fd7b8673" + integrity sha512-ZoCZGcfIJFJuZBqxcY9OjC1KW2lWK64qrX1o4UYL3yshVhwKFYgzpWZ0vvtGMNJdTlvkw0W+HR1VnYN8q3QPFQ== + dependencies: + "@babel/types" "^7.1.3" + jsesc "^2.5.1" + lodash "^4.17.10" + source-map "^0.5.0" + trim-right "^1.0.1" + +"@babel/helper-annotate-as-pure@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32" + integrity sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz#6b69628dfe4087798e0c4ed98e3d4a6b2fbd2f5f" + integrity sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-builder-react-jsx@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.0.0.tgz#fa154cb53eb918cf2a9a7ce928e29eb649c5acdb" + integrity sha512-ebJ2JM6NAKW0fQEqN8hOLxK84RbRz9OkUhGS/Xd5u56ejMfVbayJ4+LykERZCOUM6faa6Fp3SZNX3fcT16MKHw== + dependencies: + "@babel/types" "^7.0.0" + esutils "^2.0.0" + +"@babel/helper-call-delegate@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.1.0.tgz#6a957f105f37755e8645343d3038a22e1449cc4a" + integrity sha512-YEtYZrw3GUK6emQHKthltKNZwszBcHK58Ygcis+gVUrF4/FmTVr5CCqQNSfmvg2y+YDEANyYoaLz/SHsnusCwQ== + dependencies: + "@babel/helper-hoist-variables" "^7.0.0" + "@babel/traverse" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-define-map@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz#3b74caec329b3c80c116290887c0dd9ae468c20c" + integrity sha512-yPPcW8dc3gZLN+U1mhYV91QU3n5uTbx7DUdf8NnPbjS0RMwBuHi9Xt2MUgppmNz7CJxTBWsGczTiEp1CSOTPRg== + dependencies: + "@babel/helper-function-name" "^7.1.0" + "@babel/types" "^7.0.0" + lodash "^4.17.10" + +"@babel/helper-explode-assignable-expression@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz#537fa13f6f1674df745b0c00ec8fe4e99681c8f6" + integrity sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA== + dependencies: + "@babel/traverse" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-function-name@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz#e18552aaae2231100a6e485e03854bc3532d44dd" + integrity sha512-MHRG2qZMKMFaBavX0LWpfZ2e+hLloT++N7rfM3DYOMUOGCD8cVjqZpwiL8a0bOX3IYcQev1ruciT0gdFFRTxzg== + dependencies: + "@babel/helper-get-function-arity" "7.0.0-beta.44" + "@babel/template" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + +"@babel/helper-function-name@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53" + integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw== + dependencies: + "@babel/helper-get-function-arity" "^7.0.0" + "@babel/template" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-get-function-arity@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz#d03ca6dd2b9f7b0b1e6b32c56c72836140db3a15" + integrity sha512-w0YjWVwrM2HwP6/H3sEgrSQdkCaxppqFeJtAnB23pRiJB5E/O9Yp7JAAeWBl+gGEgmBFinnTyOv2RN7rcSmMiw== + dependencies: + "@babel/types" "7.0.0-beta.44" + +"@babel/helper-get-function-arity@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3" + integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-hoist-variables@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0.tgz#46adc4c5e758645ae7a45deb92bab0918c23bb88" + integrity sha512-Ggv5sldXUeSKsuzLkddtyhyHe2YantsxWKNi7A+7LeD12ExRDWTRk29JCXpaHPAbMaIPZSil7n+lq78WY2VY7w== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-member-expression-to-functions@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz#8cd14b0a0df7ff00f009e7d7a436945f47c7a16f" + integrity sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-module-imports@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d" + integrity sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-module-transforms@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.1.0.tgz#470d4f9676d9fad50b324cdcce5fbabbc3da5787" + integrity sha512-0JZRd2yhawo79Rcm4w0LwSMILFmFXjugG3yqf+P/UsKsRS1mJCmMwwlHDlMg7Avr9LrvSpp4ZSULO9r8jpCzcw== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-simple-access" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.0.0" + "@babel/template" "^7.1.0" + "@babel/types" "^7.0.0" + lodash "^4.17.10" + +"@babel/helper-optimise-call-expression@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz#a2920c5702b073c15de51106200aa8cad20497d5" + integrity sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-plugin-utils@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250" + integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== + +"@babel/helper-regex@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.0.0.tgz#2c1718923b57f9bbe64705ffe5640ac64d9bdb27" + integrity sha512-TR0/N0NDCcUIUEbqV6dCO+LptmmSQFQ7q70lfcEB4URsjD0E1HzicrwUH+ap6BAQ2jhCX9Q4UqZy4wilujWlkg== + dependencies: + lodash "^4.17.10" + +"@babel/helper-remap-async-to-generator@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz#361d80821b6f38da75bd3f0785ece20a88c5fe7f" + integrity sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.0.0" + "@babel/helper-wrap-function" "^7.1.0" + "@babel/template" "^7.1.0" + "@babel/traverse" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-replace-supers@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.1.0.tgz#5fc31de522ec0ef0899dc9b3e7cf6a5dd655f362" + integrity sha512-BvcDWYZRWVuDeXTYZWxekQNO5D4kO55aArwZOTFXw6rlLQA8ZaDicJR1sO47h+HrnCiDFiww0fSPV0d713KBGQ== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.0.0" + "@babel/helper-optimise-call-expression" "^7.0.0" + "@babel/traverse" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-simple-access@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz#65eeb954c8c245beaa4e859da6188f39d71e585c" + integrity sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w== + dependencies: + "@babel/template" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-split-export-declaration@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz#c0b351735e0fbcb3822c8ad8db4e583b05ebd9dc" + integrity sha512-aQ7QowtkgKKzPGf0j6u77kBMdUFVBKNHw2p/3HX/POt5/oz8ec5cs0GwlgM8Hz7ui5EwJnzyfRmkNF1Nx1N7aA== + dependencies: + "@babel/types" "7.0.0-beta.44" + +"@babel/helper-split-export-declaration@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz#3aae285c0311c2ab095d997b8c9a94cad547d813" + integrity sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-wrap-function@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.1.0.tgz#8cf54e9190706067f016af8f75cb3df829cc8c66" + integrity sha512-R6HU3dete+rwsdAfrOzTlE9Mcpk4RjU3aX3gi9grtmugQY0u79X7eogUvfXA5sI81Mfq1cn6AgxihfN33STjJA== + dependencies: + "@babel/helper-function-name" "^7.1.0" + "@babel/template" "^7.1.0" + "@babel/traverse" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helpers@^7.1.0", "@babel/helpers@^7.1.2": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.1.2.tgz#ab752e8c35ef7d39987df4e8586c63b8846234b5" + integrity sha512-Myc3pUE8eswD73aWcartxB16K6CGmHDv9KxOmD2CeOs/FaEAQodr3VYGmlvOmog60vNQ2w8QbatuahepZwrHiA== + dependencies: + "@babel/template" "^7.1.2" + "@babel/traverse" "^7.1.0" + "@babel/types" "^7.1.2" + +"@babel/highlight@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.44.tgz#18c94ce543916a80553edcdcf681890b200747d5" + integrity sha512-Il19yJvy7vMFm8AVAh6OZzaFoAd0hbkeMZiX3P5HGD+z7dyI7RzndHB0dg6Urh/VAFfHtpOIzDUSxmY6coyZWQ== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +"@babel/highlight@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" + integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.1.2", "@babel/parser@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.1.3.tgz#2c92469bac2b7fbff810b67fca07bd138b48af77" + integrity sha512-gqmspPZOMW3MIRb9HlrnbZHXI1/KHTOroBwN1NcLL6pWxzqzEKGvRTq0W/PxS45OtQGbaFikSQpkS5zbnsQm2w== + +"@babel/plugin-proposal-async-generator-functions@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.1.0.tgz#41c1a702e10081456e23a7b74d891922dd1bb6ce" + integrity sha512-Fq803F3Jcxo20MXUSDdmZZXrPe6BWyGcWBPPNB/M7WaUYESKDeKMOGIxEzQOjGSmW/NWb6UaPZrtTB2ekhB/ew== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-remap-async-to-generator" "^7.1.0" + "@babel/plugin-syntax-async-generators" "^7.0.0" + +"@babel/plugin-proposal-class-properties@7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.1.0.tgz#9af01856b1241db60ec8838d84691aa0bd1e8df4" + integrity sha512-/PCJWN+CKt5v1xcGn4vnuu13QDoV+P7NcICP44BoonAJoPSGwVkgrXihFIQGiEjjPlUDBIw1cM7wYFLARS2/hw== + dependencies: + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-member-expression-to-functions" "^7.0.0" + "@babel/helper-optimise-call-expression" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-replace-supers" "^7.1.0" + "@babel/plugin-syntax-class-properties" "^7.0.0" + +"@babel/plugin-proposal-json-strings@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.0.0.tgz#3b4d7b5cf51e1f2e70f52351d28d44fc2970d01e" + integrity sha512-kfVdUkIAGJIVmHmtS/40i/fg/AGnw/rsZBCaapY5yjeO5RA9m165Xbw9KMOu2nqXP5dTFjEjHdfNdoVcHv133Q== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-json-strings" "^7.0.0" + +"@babel/plugin-proposal-object-rest-spread@7.0.0", "@babel/plugin-proposal-object-rest-spread@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.0.0.tgz#9a17b547f64d0676b6c9cecd4edf74a82ab85e7e" + integrity sha512-14fhfoPcNu7itSen7Py1iGN0gEm87hX/B+8nZPqkdmANyyYWYMY2pjA3r8WXbWVKMzfnSNS0xY8GVS0IjXi/iw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-object-rest-spread" "^7.0.0" + +"@babel/plugin-proposal-optional-catch-binding@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.0.0.tgz#b610d928fe551ff7117d42c8bb410eec312a6425" + integrity sha512-JPqAvLG1s13B/AuoBjdBYvn38RqW6n1TzrQO839/sIpqLpbnXKacsAgpZHzLD83Sm8SDXMkkrAvEnJ25+0yIpw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.0.0" + +"@babel/plugin-proposal-unicode-property-regex@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.0.0.tgz#498b39cd72536cd7c4b26177d030226eba08cd33" + integrity sha512-tM3icA6GhC3ch2SkmSxv7J/hCWKISzwycub6eGsDrFDgukD4dZ/I+x81XgW0YslS6mzNuQ1Cbzh5osjIMgepPQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-regex" "^7.0.0" + regexpu-core "^4.2.0" + +"@babel/plugin-syntax-async-generators@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.0.0.tgz#bf0891dcdbf59558359d0c626fdc9490e20bc13c" + integrity sha512-im7ged00ddGKAjcZgewXmp1vxSZQQywuQXe2B1A7kajjZmDeY/ekMPmWr9zJgveSaQH0k7BcGrojQhcK06l0zA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-class-properties@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.0.0.tgz#e051af5d300cbfbcec4a7476e37a803489881634" + integrity sha512-cR12g0Qzn4sgkjrbrzWy2GE7m9vMl/sFkqZ3gIpAQdrvPDnLM8180i+ANDFIXfjHo9aqp0ccJlQ0QNZcFUbf9w== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-dynamic-import@7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.0.0.tgz#6dfb7d8b6c3be14ce952962f658f3b7eb54c33ee" + integrity sha512-Gt9xNyRrCHCiyX/ZxDGOcBnlJl0I3IWicpZRC4CdC0P5a/I07Ya2OAMEBU+J7GmRFVmIetqEYRko6QYRuKOESw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-flow@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.0.0.tgz#70638aeaad9ee426bc532e51523cff8ff02f6f17" + integrity sha512-zGcuZWiWWDa5qTZ6iAnpG0fnX/GOu49pGR5PFvkQ9GmKNaSphXQnlNXh/LG20sqWtNrx/eB6krzfEzcwvUyeFA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-json-strings@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.0.0.tgz#0d259a68090e15b383ce3710e01d5b23f3770cbd" + integrity sha512-UlSfNydC+XLj4bw7ijpldc1uZ/HB84vw+U6BTuqMdIEmz/LDe63w/GHtpQMdXWdqQZFeAI9PjnHe/vDhwirhKA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-jsx@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.0.0.tgz#034d5e2b4e14ccaea2e4c137af7e4afb39375ffd" + integrity sha512-PdmL2AoPsCLWxhIr3kG2+F9v4WH06Q3z+NoGVpQgnUNGcagXHq5sB3OXxkSahKq9TLdNMN/AJzFYSOo8UKDMHg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-object-rest-spread@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.0.0.tgz#37d8fbcaf216bd658ea1aebbeb8b75e88ebc549b" + integrity sha512-5A0n4p6bIiVe5OvQPxBnesezsgFJdHhSs3uFSvaPdMqtsovajLZ+G2vZyvNe10EzJBWWo3AcHGKhAFUxqwp2dw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.0.0.tgz#886f72008b3a8b185977f7cb70713b45e51ee475" + integrity sha512-Wc+HVvwjcq5qBg1w5RG9o9RVzmCaAg/Vp0erHCKpAYV8La6I94o4GQAmFYNmkzoMO6gzoOSulpKeSSz6mPEoZw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-arrow-functions@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.0.0.tgz#a6c14875848c68a3b4b3163a486535ef25c7e749" + integrity sha512-2EZDBl1WIO/q4DIkIp4s86sdp4ZifL51MoIviLY/gG/mLSuOIEg7J8o6mhbxOTvUJkaN50n+8u41FVsr5KLy/w== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-async-to-generator@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.1.0.tgz#109e036496c51dd65857e16acab3bafdf3c57811" + integrity sha512-rNmcmoQ78IrvNCIt/R9U+cixUHeYAzgusTFgIAv+wQb9HJU4szhpDD6e5GCACmj/JP5KxuCwM96bX3L9v4ZN/g== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-remap-async-to-generator" "^7.1.0" + +"@babel/plugin-transform-block-scoped-functions@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.0.0.tgz#482b3f75103927e37288b3b67b65f848e2aa0d07" + integrity sha512-AOBiyUp7vYTqz2Jibe1UaAWL0Hl9JUXEgjFvvvcSc9MVDItv46ViXFw2F7SVt1B5k+KWjl44eeXOAk3UDEaJjQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-block-scoping@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.0.0.tgz#1745075edffd7cdaf69fab2fb6f9694424b7e9bc" + integrity sha512-GWEMCrmHQcYWISilUrk9GDqH4enf3UmhOEbNbNrlNAX1ssH3MsS1xLOS6rdjRVPgA7XXVPn87tRkdTEoA/dxEg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + lodash "^4.17.10" + +"@babel/plugin-transform-classes@7.1.0", "@babel/plugin-transform-classes@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.1.0.tgz#ab3f8a564361800cbc8ab1ca6f21108038432249" + integrity sha512-rNaqoD+4OCBZjM7VaskladgqnZ1LO6o2UxuWSDzljzW21pN1KXkB7BstAVweZdxQkHAujps5QMNOTWesBciKFg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.0.0" + "@babel/helper-define-map" "^7.1.0" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-optimise-call-expression" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-replace-supers" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.0.0" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.0.0.tgz#2fbb8900cd3e8258f2a2ede909b90e7556185e31" + integrity sha512-ubouZdChNAv4AAWAgU7QKbB93NU5sHwInEWfp+/OzJKA02E6Woh9RVoX4sZrbRwtybky/d7baTUqwFx+HgbvMA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-destructuring@7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.0.0.tgz#68e911e1935dda2f06b6ccbbf184ffb024e9d43a" + integrity sha512-Fr2GtF8YJSXGTyFPakPFB4ODaEKGU04bPsAllAIabwoXdFrPxL0LVXQX5dQWoxOjjgozarJcC9eWGsj0fD6Zsg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-destructuring@^7.0.0": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.1.3.tgz#e69ff50ca01fac6cb72863c544e516c2b193012f" + integrity sha512-Mb9M4DGIOspH1ExHOUnn2UUXFOyVTiX84fXCd+6B5iWrQg/QMeeRmSwpZ9lnjYLSXtZwiw80ytVMr3zue0ucYw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-dotall-regex@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.0.0.tgz#73a24da69bc3c370251f43a3d048198546115e58" + integrity sha512-00THs8eJxOJUFVx1w8i1MBF4XH4PsAjKjQ1eqN/uCH3YKwP21GCKfrn6YZFZswbOk9+0cw1zGQPHVc1KBlSxig== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-regex" "^7.0.0" + regexpu-core "^4.1.3" + +"@babel/plugin-transform-duplicate-keys@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.0.0.tgz#a0601e580991e7cace080e4cf919cfd58da74e86" + integrity sha512-w2vfPkMqRkdxx+C71ATLJG30PpwtTpW7DDdLqYt2acXU7YjztzeWW2Jk1T6hKqCLYCcEA5UQM/+xTAm+QCSnuQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-exponentiation-operator@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.1.0.tgz#9c34c2ee7fd77e02779cfa37e403a2e1003ccc73" + integrity sha512-uZt9kD1Pp/JubkukOGQml9tqAeI8NkE98oZnHZ2qHRElmeKCodbTZgOEUtujSCSLhHSBWbzNiFSDIMC4/RBTLQ== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-flow-strip-types@7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.0.0.tgz#c40ced34c2783985d90d9f9ac77a13e6fb396a01" + integrity sha512-WhXUNb4It5a19RsgKKbQPrjmy4yWOY1KynpEbNw7bnd1QTcrT/EIl3MJvnGgpgvrKyKbqX7nUNOJfkpLOnoDKA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-flow" "^7.0.0" + +"@babel/plugin-transform-for-of@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.0.0.tgz#f2ba4eadb83bd17dc3c7e9b30f4707365e1c3e39" + integrity sha512-TlxKecN20X2tt2UEr2LNE6aqA0oPeMT1Y3cgz8k4Dn1j5ObT8M3nl9aA37LLklx0PBZKETC9ZAf9n/6SujTuXA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-function-name@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.1.0.tgz#29c5550d5c46208e7f730516d41eeddd4affadbb" + integrity sha512-VxOa1TMlFMtqPW2IDYZQaHsFrq/dDoIjgN098NowhexhZcz3UGlvPgZXuE1jEvNygyWyxRacqDpCZt+par1FNg== + dependencies: + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-literals@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.0.0.tgz#2aec1d29cdd24c407359c930cdd89e914ee8ff86" + integrity sha512-1NTDBWkeNXgpUcyoVFxbr9hS57EpZYXpje92zv0SUzjdu3enaRwF/l3cmyRnXLtIdyJASyiS6PtybK+CgKf7jA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-modules-amd@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.1.0.tgz#f9e0a7072c12e296079b5a59f408ff5b97bf86a8" + integrity sha512-wt8P+xQ85rrnGNr2x1iV3DW32W8zrB6ctuBkYBbf5/ZzJY99Ob4MFgsZDFgczNU76iy9PWsy4EuxOliDjdKw6A== + dependencies: + "@babel/helper-module-transforms" "^7.1.0" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-modules-commonjs@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.1.0.tgz#0a9d86451cbbfb29bd15186306897c67f6f9a05c" + integrity sha512-wtNwtMjn1XGwM0AXPspQgvmE6msSJP15CX2RVfpTSTNPLhKhaOjaIfBaVfj4iUZ/VrFSodcFedwtPg/NxwQlPA== + dependencies: + "@babel/helper-module-transforms" "^7.1.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-simple-access" "^7.1.0" + +"@babel/plugin-transform-modules-systemjs@^7.0.0": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.1.3.tgz#2119a3e3db612fd74a19d88652efbfe9613a5db0" + integrity sha512-PvTxgjxQAq4pvVUZF3mD5gEtVDuId8NtWkJsZLEJZMZAW3TvgQl1pmydLLN1bM8huHFVVU43lf0uvjQj9FRkKw== + dependencies: + "@babel/helper-hoist-variables" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-modules-umd@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.1.0.tgz#a29a7d85d6f28c3561c33964442257cc6a21f2a8" + integrity sha512-enrRtn5TfRhMmbRwm7F8qOj0qEYByqUvTttPEGimcBH4CJHphjyK1Vg7sdU7JjeEmgSpM890IT/efS2nMHwYig== + dependencies: + "@babel/helper-module-transforms" "^7.1.0" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-new-target@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz#ae8fbd89517fa7892d20e6564e641e8770c3aa4a" + integrity sha512-yin069FYjah+LbqfGeTfzIBODex/e++Yfa0rH0fpfam9uTbuEeEOx5GLGr210ggOV77mVRNoeqSYqeuaqSzVSw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-object-super@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.1.0.tgz#b1ae194a054b826d8d4ba7ca91486d4ada0f91bb" + integrity sha512-/O02Je1CRTSk2SSJaq0xjwQ8hG4zhZGNjE8psTsSNPXyLRCODv7/PBozqT5AmQMzp7MI3ndvMhGdqp9c96tTEw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-replace-supers" "^7.1.0" + +"@babel/plugin-transform-parameters@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.1.0.tgz#44f492f9d618c9124026e62301c296bf606a7aed" + integrity sha512-vHV7oxkEJ8IHxTfRr3hNGzV446GAb+0hgbA7o/0Jd76s+YzccdWuTU296FOCOl/xweU4t/Ya4g41yWz80RFCRw== + dependencies: + "@babel/helper-call-delegate" "^7.1.0" + "@babel/helper-get-function-arity" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-react-constant-elements@7.0.0", "@babel/plugin-transform-react-constant-elements@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.0.0.tgz#ab413e33e9c46a766f5326014bcbf9e2b34ef7a4" + integrity sha512-z8yrW4KCVcqPYr0r9dHXe7fu3daLzn0r6TQEFoGbXahdrzEwT1d1ux+/EnFcqIHv9uPilUlnRnPIUf7GMO0ehg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-react-display-name@7.0.0", "@babel/plugin-transform-react-display-name@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.0.0.tgz#93759e6c023782e52c2da3b75eca60d4f10533ee" + integrity sha512-BX8xKuQTO0HzINxT6j/GiCwoJB0AOMs0HmLbEnAvcte8U8rSkNa/eSCAY+l1OA4JnCVq2jw2p6U8QQryy2fTPg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-react-jsx-self@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.0.0.tgz#a84bb70fea302d915ea81d9809e628266bb0bc11" + integrity sha512-pymy+AK12WO4safW1HmBpwagUQRl9cevNX+82AIAtU1pIdugqcH+nuYP03Ja6B+N4gliAaKWAegIBL/ymALPHA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-jsx" "^7.0.0" + +"@babel/plugin-transform-react-jsx-source@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.0.0.tgz#28e00584f9598c0dd279f6280eee213fa0121c3c" + integrity sha512-OSeEpFJEH5dw/TtxTg4nijl4nHBbhqbKL94Xo/Y17WKIf2qJWeIk/QeXACF19lG1vMezkxqruwnTjVizaW7u7w== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-jsx" "^7.0.0" + +"@babel/plugin-transform-react-jsx@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.0.0.tgz#524379e4eca5363cd10c4446ba163f093da75f3e" + integrity sha512-0TMP21hXsSUjIQJmu/r7RiVxeFrXRcMUigbKu0BLegJK9PkYodHstaszcig7zxXfaBji2LYUdtqIkHs+hgYkJQ== + dependencies: + "@babel/helper-builder-react-jsx" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-jsx" "^7.0.0" + +"@babel/plugin-transform-regenerator@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.0.0.tgz#5b41686b4ed40bef874d7ed6a84bdd849c13e0c1" + integrity sha512-sj2qzsEx8KDVv1QuJc/dEfilkg3RRPvPYx/VnKLtItVQRWt1Wqf5eVCOLZm29CiGFfYYsA3VPjfizTCV0S0Dlw== + dependencies: + regenerator-transform "^0.13.3" + +"@babel/plugin-transform-runtime@7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.1.0.tgz#9f76920d42551bb577e2dc594df229b5f7624b63" + integrity sha512-WFLMgzu5DLQEah0lKTJzYb14vd6UiES7PTnXcvrPZ1VrwFeJ+mTbvr65fFAsXYMt2bIoOoC0jk76zY1S7HZjUg== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + resolve "^1.8.1" + semver "^5.5.1" + +"@babel/plugin-transform-shorthand-properties@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.0.0.tgz#85f8af592dcc07647541a0350e8c95c7bf419d15" + integrity sha512-g/99LI4vm5iOf5r1Gdxq5Xmu91zvjhEG5+yZDJW268AZELAu4J1EiFLnkSG3yuUsZyOipVOVUKoGPYwfsTymhw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-spread@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.0.0.tgz#93583ce48dd8c85e53f3a46056c856e4af30b49b" + integrity sha512-L702YFy2EvirrR4shTj0g2xQp7aNwZoWNCkNu2mcoU0uyzMl0XRwDSwzB/xp6DSUFiBmEXuyAyEN16LsgVqGGQ== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-sticky-regex@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.0.0.tgz#30a9d64ac2ab46eec087b8530535becd90e73366" + integrity sha512-LFUToxiyS/WD+XEWpkx/XJBrUXKewSZpzX68s+yEOtIbdnsRjpryDw9U06gYc6klYEij/+KQVRnD3nz3AoKmjw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-regex" "^7.0.0" + +"@babel/plugin-transform-template-literals@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.0.0.tgz#084f1952efe5b153ddae69eb8945f882c7a97c65" + integrity sha512-vA6rkTCabRZu7Nbl9DfLZE1imj4tzdWcg5vtdQGvj+OH9itNNB6hxuRMHuIY8SGnEt1T9g5foqs9LnrHzsqEFg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-typeof-symbol@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.0.0.tgz#4dcf1e52e943e5267b7313bff347fdbe0f81cec9" + integrity sha512-1r1X5DO78WnaAIvs5uC48t41LLckxsYklJrZjNKcevyz83sF2l4RHbw29qrCPr/6ksFsdfRpT/ZgxNWHXRnffg== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + +"@babel/plugin-transform-unicode-regex@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.0.0.tgz#c6780e5b1863a76fe792d90eded9fcd5b51d68fc" + integrity sha512-uJBrJhBOEa3D033P95nPHu3nbFwFE9ZgXsfEitzoIXIwqAZWk7uXcg06yFKXz9FSxBH5ucgU/cYdX0IV8ldHKw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-regex" "^7.0.0" + regexpu-core "^4.1.3" + +"@babel/preset-env@7.1.0", "@babel/preset-env@^7.0.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.1.0.tgz#e67ea5b0441cfeab1d6f41e9b5c79798800e8d11" + integrity sha512-ZLVSynfAoDHB/34A17/JCZbyrzbQj59QC1Anyueb4Bwjh373nVPq5/HMph0z+tCmcDjXDe+DlKQq9ywQuvWrQg== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-async-generator-functions" "^7.1.0" + "@babel/plugin-proposal-json-strings" "^7.0.0" + "@babel/plugin-proposal-object-rest-spread" "^7.0.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.0.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.0.0" + "@babel/plugin-syntax-async-generators" "^7.0.0" + "@babel/plugin-syntax-object-rest-spread" "^7.0.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.0.0" + "@babel/plugin-transform-arrow-functions" "^7.0.0" + "@babel/plugin-transform-async-to-generator" "^7.1.0" + "@babel/plugin-transform-block-scoped-functions" "^7.0.0" + "@babel/plugin-transform-block-scoping" "^7.0.0" + "@babel/plugin-transform-classes" "^7.1.0" + "@babel/plugin-transform-computed-properties" "^7.0.0" + "@babel/plugin-transform-destructuring" "^7.0.0" + "@babel/plugin-transform-dotall-regex" "^7.0.0" + "@babel/plugin-transform-duplicate-keys" "^7.0.0" + "@babel/plugin-transform-exponentiation-operator" "^7.1.0" + "@babel/plugin-transform-for-of" "^7.0.0" + "@babel/plugin-transform-function-name" "^7.1.0" + "@babel/plugin-transform-literals" "^7.0.0" + "@babel/plugin-transform-modules-amd" "^7.1.0" + "@babel/plugin-transform-modules-commonjs" "^7.1.0" + "@babel/plugin-transform-modules-systemjs" "^7.0.0" + "@babel/plugin-transform-modules-umd" "^7.1.0" + "@babel/plugin-transform-new-target" "^7.0.0" + "@babel/plugin-transform-object-super" "^7.1.0" + "@babel/plugin-transform-parameters" "^7.1.0" + "@babel/plugin-transform-regenerator" "^7.0.0" + "@babel/plugin-transform-shorthand-properties" "^7.0.0" + "@babel/plugin-transform-spread" "^7.0.0" + "@babel/plugin-transform-sticky-regex" "^7.0.0" + "@babel/plugin-transform-template-literals" "^7.0.0" + "@babel/plugin-transform-typeof-symbol" "^7.0.0" + "@babel/plugin-transform-unicode-regex" "^7.0.0" + browserslist "^4.1.0" + invariant "^2.2.2" + js-levenshtein "^1.1.3" + semver "^5.3.0" + +"@babel/preset-react@7.0.0", "@babel/preset-react@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.0.0.tgz#e86b4b3d99433c7b3e9e91747e2653958bc6b3c0" + integrity sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-transform-react-display-name" "^7.0.0" + "@babel/plugin-transform-react-jsx" "^7.0.0" + "@babel/plugin-transform-react-jsx-self" "^7.0.0" + "@babel/plugin-transform-react-jsx-source" "^7.0.0" + +"@babel/runtime@7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0.tgz#adeb78fedfc855aa05bc041640f3f6f98e85424c" + integrity sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA== + dependencies: + regenerator-runtime "^0.12.0" + +"@babel/template@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f" + integrity sha512-w750Sloq0UNifLx1rUqwfbnC6uSUk0mfwwgGRfdLiaUzfAOiH0tHJE6ILQIUi3KYkjiCDTskoIsnfqZvWLBDng== + dependencies: + "@babel/code-frame" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" + lodash "^4.2.0" + +"@babel/template@^7.1.0", "@babel/template@^7.1.2": + version "7.1.2" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.1.2.tgz#090484a574fef5a2d2d7726a674eceda5c5b5644" + integrity sha512-SY1MmplssORfFiLDcOETrW7fCLl+PavlwMh92rrGcikQaRq4iWPVH0MpwPpY3etVMx6RnDjXtr6VZYr/IbP/Ag== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.1.2" + "@babel/types" "^7.1.2" + +"@babel/traverse@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.44.tgz#a970a2c45477ad18017e2e465a0606feee0d2966" + integrity sha512-UHuDz8ukQkJCDASKHf+oDt3FVUzFd+QYfuBIsiNu/4+/ix6pP/C+uQZJ6K1oEfbCMv/IKWbgDEh7fcsnIE5AtA== + dependencies: + "@babel/code-frame" "7.0.0-beta.44" + "@babel/generator" "7.0.0-beta.44" + "@babel/helper-function-name" "7.0.0-beta.44" + "@babel/helper-split-export-declaration" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" + debug "^3.1.0" + globals "^11.1.0" + invariant "^2.2.0" + lodash "^4.2.0" + +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0": + version "7.1.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.1.4.tgz#f4f83b93d649b4b2c91121a9087fa2fa949ec2b4" + integrity sha512-my9mdrAIGdDiSVBuMjpn/oXYpva0/EZwWL3sm3Wcy/AVWO2eXnsoZruOT9jOGNRXU8KbCIu5zsKnXcAJ6PcV6Q== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.1.3" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.0.0" + "@babel/parser" "^7.1.3" + "@babel/types" "^7.1.3" + debug "^3.1.0" + globals "^11.1.0" + lodash "^4.17.10" + +"@babel/types@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.44.tgz#6b1b164591f77dec0a0342aca995f2d046b3a757" + integrity sha512-5eTV4WRmqbaFM3v9gHAIljEQJU4Ssc6fxL61JN+Oe2ga/BwyjzjamwkCVVAQjHGuAX8i0BWo42dshL8eO5KfLQ== + dependencies: + esutils "^2.0.2" + lodash "^4.2.0" + to-fast-properties "^2.0.0" + +"@babel/types@^7.0.0", "@babel/types@^7.1.2", "@babel/types@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.1.3.tgz#3a767004567060c2f40fca49a304712c525ee37d" + integrity sha512-RpPOVfK+yatXyn8n4PB1NW6k9qjinrXrRR8ugBN8fD6hCy5RXI6PSbVqpOJBO9oSaY7Nom4ohj35feb0UR9hSA== + dependencies: + esutils "^2.0.2" + lodash "^4.17.10" + to-fast-properties "^2.0.0" + +"@csstools/convert-colors@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" + integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== + +"@svgr/core@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-2.4.1.tgz#03a407c28c4a1d84305ae95021e8eabfda8fa731" + integrity sha512-2i1cUbjpKt1KcIP05e10vkmu9Aedp32EFqVcSQ08onbB8lVxJqMPci3Hr54aI14S9cLg4JdcpO0D35HHUtT8oQ== + dependencies: + camelcase "^5.0.0" + cosmiconfig "^5.0.6" + h2x-core "^1.1.0" + h2x-plugin-jsx "^1.1.0" + merge-deep "^3.0.2" + prettier "^1.14.2" + svgo "^1.0.5" + +"@svgr/webpack@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-2.4.1.tgz#68bc581ecb4c09fadeb7936bd1afaceb9da960d2" + integrity sha512-sMHYq0zbMtSHcc9kVfkYI2zrl88u4mKGyQLgKt7r+ul5nITcncm/EPBhzEUrJY5izdlaU6EvyH8zOhZnfaSmOA== + dependencies: + "@babel/core" "^7.0.1" + "@babel/plugin-transform-react-constant-elements" "^7.0.0" + "@babel/preset-env" "^7.0.0" + "@babel/preset-react" "^7.0.0" + "@svgr/core" "^2.4.1" + loader-utils "^1.1.0" + +"@types/tapable@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.2.tgz#e13182e1b69871a422d7863e11a4a6f5b814a4bd" + integrity sha512-42zEJkBpNfMEAvWR5WlwtTH22oDzcMjFsL9gDGExwF8X8WvAiw7Vwop7hPw03QT8TKfec83LwbHj6SvpqM4ELQ== + +"@webassemblyjs/ast@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.6.tgz#3ef8c45b3e5e943a153a05281317474fef63e21e" + integrity sha512-8nkZS48EVsMUU0v6F1LCIOw4RYWLm2plMtbhFTjNgeXmsTNLuU3xTRtnljt9BFQB+iPbLRobkNrCWftWnNC7wQ== + dependencies: + "@webassemblyjs/helper-module-context" "1.7.6" + "@webassemblyjs/helper-wasm-bytecode" "1.7.6" + "@webassemblyjs/wast-parser" "1.7.6" + mamacro "^0.0.3" + +"@webassemblyjs/floating-point-hex-parser@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.6.tgz#7cb37d51a05c3fe09b464ae7e711d1ab3837801f" + integrity sha512-VBOZvaOyBSkPZdIt5VBMg3vPWxouuM13dPXGWI1cBh3oFLNcFJ8s9YA7S9l4mPI7+Q950QqOmqj06oa83hNWBA== + +"@webassemblyjs/helper-api-error@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.6.tgz#99b7e30e66f550a2638299a109dda84a622070ef" + integrity sha512-SCzhcQWHXfrfMSKcj8zHg1/kL9kb3aa5TN4plc/EREOs5Xop0ci5bdVBApbk2yfVi8aL+Ly4Qpp3/TRAUInjrg== + +"@webassemblyjs/helper-buffer@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.6.tgz#ba0648be12bbe560c25c997e175c2018df39ca3e" + integrity sha512-1/gW5NaGsEOZ02fjnFiU8/OEEXU1uVbv2um0pQ9YVL3IHSkyk6xOwokzyqqO1qDZQUAllb+V8irtClPWntbVqw== + +"@webassemblyjs/helper-code-frame@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.6.tgz#5a94d21b0057b69a7403fca0c253c3aaca95b1a5" + integrity sha512-+suMJOkSn9+vEvDvgyWyrJo5vJsWSDXZmJAjtoUq4zS4eqHyXImpktvHOZwXp1XQjO5H+YQwsBgqTQEc0J/5zg== + dependencies: + "@webassemblyjs/wast-printer" "1.7.6" + +"@webassemblyjs/helper-fsm@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.6.tgz#ae1741c6f6121213c7a0b587fb964fac492d3e49" + integrity sha512-HCS6KN3wgxUihGBW7WFzEC/o8Eyvk0d56uazusnxXthDPnkWiMv+kGi9xXswL2cvfYfeK5yiM17z2K5BVlwypw== + +"@webassemblyjs/helper-module-context@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.6.tgz#116d19a51a6cebc8900ad53ca34ff8269c668c23" + integrity sha512-e8/6GbY7OjLM+6OsN7f2krC2qYVNaSr0B0oe4lWdmq5sL++8dYDD1TFbD1TdAdWMRTYNr/Qq7ovXWzia2EbSjw== + dependencies: + mamacro "^0.0.3" + +"@webassemblyjs/helper-wasm-bytecode@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.6.tgz#98e515eaee611aa6834eb5f6a7f8f5b29fefb6f1" + integrity sha512-PzYFCb7RjjSdAOljyvLWVqd6adAOabJW+8yRT+NWhXuf1nNZWH+igFZCUK9k7Cx7CsBbzIfXjJc7u56zZgFj9Q== + +"@webassemblyjs/helper-wasm-section@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.6.tgz#783835867bdd686df7a95377ab64f51a275e8333" + integrity sha512-3GS628ppDPSuwcYlQ7cDCGr4W2n9c4hLzvnRKeuz+lGsJSmc/ADVoYpm1ts2vlB1tGHkjtQMni+yu8mHoMlKlA== + dependencies: + "@webassemblyjs/ast" "1.7.6" + "@webassemblyjs/helper-buffer" "1.7.6" + "@webassemblyjs/helper-wasm-bytecode" "1.7.6" + "@webassemblyjs/wasm-gen" "1.7.6" + +"@webassemblyjs/ieee754@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.7.6.tgz#c34fc058f2f831fae0632a8bb9803cf2d3462eb1" + integrity sha512-V4cIp0ruyw+hawUHwQLn6o2mFEw4t50tk530oKsYXQhEzKR+xNGDxs/SFFuyTO7X3NzEu4usA3w5jzhl2RYyzQ== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.7.6.tgz#197f75376a29f6ed6ace15898a310d871d92f03b" + integrity sha512-ojdlG8WpM394lBow4ncTGJoIVZ4aAtNOWHhfAM7m7zprmkVcKK+2kK5YJ9Bmj6/ketTtOn7wGSHCtMt+LzqgYQ== + dependencies: + "@xtuc/long" "4.2.1" + +"@webassemblyjs/utf8@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.7.6.tgz#eb62c66f906af2be70de0302e29055d25188797d" + integrity sha512-oId+tLxQ+AeDC34ELRYNSqJRaScB0TClUU6KQfpB8rNT6oelYlz8axsPhf6yPTg7PBJ/Z5WcXmUYiHEWgbbHJw== + +"@webassemblyjs/wasm-edit@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.6.tgz#fa41929160cd7d676d4c28ecef420eed5b3733c5" + integrity sha512-pTNjLO3o41v/Vz9VFLl+I3YLImpCSpodFW77pNoH4agn5I6GgSxXHXtvWDTvYJFty0jSeXZWLEmbaSIRUDlekg== + dependencies: + "@webassemblyjs/ast" "1.7.6" + "@webassemblyjs/helper-buffer" "1.7.6" + "@webassemblyjs/helper-wasm-bytecode" "1.7.6" + "@webassemblyjs/helper-wasm-section" "1.7.6" + "@webassemblyjs/wasm-gen" "1.7.6" + "@webassemblyjs/wasm-opt" "1.7.6" + "@webassemblyjs/wasm-parser" "1.7.6" + "@webassemblyjs/wast-printer" "1.7.6" + +"@webassemblyjs/wasm-gen@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.6.tgz#695ac38861ab3d72bf763c8c75e5f087ffabc322" + integrity sha512-mQvFJVumtmRKEUXMohwn8nSrtjJJl6oXwF3FotC5t6e2hlKMh8sIaW03Sck2MDzw9xPogZD7tdP5kjPlbH9EcQ== + dependencies: + "@webassemblyjs/ast" "1.7.6" + "@webassemblyjs/helper-wasm-bytecode" "1.7.6" + "@webassemblyjs/ieee754" "1.7.6" + "@webassemblyjs/leb128" "1.7.6" + "@webassemblyjs/utf8" "1.7.6" + +"@webassemblyjs/wasm-opt@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.6.tgz#fbafa78e27e1a75ab759a4b658ff3d50b4636c21" + integrity sha512-go44K90fSIsDwRgtHhX14VtbdDPdK2sZQtZqUcMRvTojdozj5tLI0VVJAzLCfz51NOkFXezPeVTAYFqrZ6rI8Q== + dependencies: + "@webassemblyjs/ast" "1.7.6" + "@webassemblyjs/helper-buffer" "1.7.6" + "@webassemblyjs/wasm-gen" "1.7.6" + "@webassemblyjs/wasm-parser" "1.7.6" + +"@webassemblyjs/wasm-parser@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.6.tgz#84eafeeff405ad6f4c4b5777d6a28ae54eed51fe" + integrity sha512-t1T6TfwNY85pDA/HWPA8kB9xA4sp9ajlRg5W7EKikqrynTyFo+/qDzIpvdkOkOGjlS6d4n4SX59SPuIayR22Yg== + dependencies: + "@webassemblyjs/ast" "1.7.6" + "@webassemblyjs/helper-api-error" "1.7.6" + "@webassemblyjs/helper-wasm-bytecode" "1.7.6" + "@webassemblyjs/ieee754" "1.7.6" + "@webassemblyjs/leb128" "1.7.6" + "@webassemblyjs/utf8" "1.7.6" + +"@webassemblyjs/wast-parser@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.7.6.tgz#ca4d20b1516e017c91981773bd7e819d6bd9c6a7" + integrity sha512-1MaWTErN0ziOsNUlLdvwS+NS1QWuI/kgJaAGAMHX8+fMJFgOJDmN/xsG4h/A1Gtf/tz5VyXQciaqHZqp2q0vfg== + dependencies: + "@webassemblyjs/ast" "1.7.6" + "@webassemblyjs/floating-point-hex-parser" "1.7.6" + "@webassemblyjs/helper-api-error" "1.7.6" + "@webassemblyjs/helper-code-frame" "1.7.6" + "@webassemblyjs/helper-fsm" "1.7.6" + "@xtuc/long" "4.2.1" + mamacro "^0.0.3" + +"@webassemblyjs/wast-printer@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.7.6.tgz#a6002c526ac5fa230fe2c6d2f1bdbf4aead43a5e" + integrity sha512-vHdHSK1tOetvDcl1IV1OdDeGNe/NDDQ+KzuZHMtqTVP1xO/tZ/IKNpj5BaGk1OYFdsDWQqb31PIwdEyPntOWRQ== + dependencies: + "@webassemblyjs/ast" "1.7.6" + "@webassemblyjs/wast-parser" "1.7.6" + "@xtuc/long" "4.2.1" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.1.tgz#5c85d662f76fa1d34575766c5dcd6615abcd30d8" + integrity sha512-FZdkNBDqBRHKQ2MEbSC17xnPFOhZxeJ2YGSfr2BKf3sujG49Qe3bB+rGCwQfIaA7WHnGeGkSijX4FuBCdrzW/g== + +abab@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" + integrity sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +accepts@~1.3.4, accepts@~1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" + integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I= + dependencies: + mime-types "~2.1.18" + negotiator "0.6.1" + +acorn-dynamic-import@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz#901ceee4c7faaef7e07ad2a47e890675da50a278" + integrity sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg== + dependencies: + acorn "^5.0.0" + +acorn-globals@^4.1.0, acorn-globals@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.0.tgz#e3b6f8da3c1552a95ae627571f7dd6923bb54103" + integrity sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw== + dependencies: + acorn "^6.0.1" + acorn-walk "^6.0.1" + +acorn-jsx@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-4.1.1.tgz#e8e41e48ea2fe0c896740610ab6a4ffd8add225e" + integrity sha512-JY+iV6r+cO21KtntVvFkD+iqjtdpRUpGqKWgfkCdZq1R+kbreEl8EcdcJR4SmiIgsIQT33s6QzheQ9a275Q8xw== + dependencies: + acorn "^5.0.3" + +acorn-walk@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.0.tgz#c957f4a1460da46af4a0388ce28b4c99355b0cbc" + integrity sha512-ugTb7Lq7u4GfWSqqpwE0bGyoBZNMTok/zDBXxfEG0QM50jNlGhIWjRC1pPN7bvV1anhF+bs+/gNcRw+o55Evbg== + +acorn@^5.0.0, acorn@^5.0.3, acorn@^5.5.3, acorn@^5.6.0, acorn@^5.6.2: + version "5.7.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" + integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== + +acorn@^6.0.1, acorn@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.2.tgz#6a459041c320ab17592c6317abbfdf4bbaa98ca4" + integrity sha512-GXmKIvbrN3TV7aVqAzVFaMW8F8wzVX7voEBRO3bDA64+EX37YSayggRJP5Xig6HYHBkWKpFg9W5gg6orklubhg== + +address@1.0.3, address@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" + integrity sha512-z55ocwKBRLryBs394Sm3ushTtBeg6VAeuku7utSoSnsJKvKcnXFIyC6vh27n3rXyxSgkJBBCAvyOn7gSUcTYjg== + +ajv-errors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.0.tgz#ecf021fa108fd17dfb5e6b383f2dd233e31ffc59" + integrity sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk= + +ajv-keywords@^3.0.0, ajv-keywords@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" + integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo= + +ajv@^5.3.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +ajv@^6.0.1, ajv@^6.1.0, ajv@^6.5.3: + version "6.5.4" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.4.tgz#247d5274110db653706b550fcc2b797ca28cfc59" + integrity sha512-4Wyjt8+t6YszqaXnLDfMmG/8AlO5Zbcsy3ATHncCzjW/NoPzAId8AK6749Ybjmdt+kUY1gP60fCu46oDxPv/mg== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +alphanum-sort@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= + +ansi-colors@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.1.0.tgz#dcfaacc90ef9187de413ec3ef8d5eb981a98808f" + integrity sha512-hTv1qPdi+sVEk3jYsdjox5nQI0C9HTbjKShbCdYLKb1LOfNbb7wsF4d7OEKIZoxIHx02tSp3m94jcPW2EfMjmA== + +ansi-escapes@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" + integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw== + +ansi-html@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" + integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +append-transform@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991" + integrity sha1-126/jKlNJ24keja61EpLdKthGZE= + dependencies: + default-require-extensions "^1.0.0" + +aproba@^1.0.3, aproba@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +aria-query@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc" + integrity sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w= + dependencies: + ast-types-flow "0.0.7" + commander "^2.11.0" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + integrity sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8= + dependencies: + arr-flatten "^1.0.1" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.0.1, arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" + integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= + +array-filter@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" + integrity sha1-fajPLiZijtcygDWB/SH2fKzS7uw= + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +array-flatten@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296" + integrity sha1-Qmu52oQJDBg42BLIFQryCoMx4pY= + +array-includes@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" + integrity sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.7.0" + +array-map@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" + integrity sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI= + +array-reduce@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" + integrity sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys= + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +arrify@^1.0.0, arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + +asap@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +asn1.js@^4.0.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" + integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assert@^1.1.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + integrity sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE= + dependencies: + util "0.10.3" + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +ast-types-flow@0.0.7, ast-types-flow@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" + integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + integrity sha1-GdOGodntxufByF04iu28xW0zYC0= + +async-foreach@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" + integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI= + +async-limiter@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" + integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== + +async@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= + +async@^2.1.4, async@^2.5.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" + integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== + dependencies: + lodash "^4.17.10" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +atob@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +autoprefixer@^9.1.5: + version "9.2.0" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.2.0.tgz#e46f893882b19a160370e7bcd3ec6bbeaace4d10" + integrity sha512-OuxUyTvzRe9EvKyouPqfr8QUkQ0pH400NOFzI1LFINO8zwgJr7ZTybLql03P//LjR0iWile2lCoy2vRTRSFpMw== + dependencies: + browserslist "^4.2.1" + caniuse-lite "^1.0.30000890" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^7.0.5" + postcss-value-parser "^3.3.1" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + +axobject-query@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.1.tgz#05dfa705ada8ad9db993fa6896f22d395b0b0a07" + integrity sha1-Bd+nBa2orZ25k/polvItOVsLCgc= + dependencies: + ast-types-flow "0.0.7" + +babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-core@7.0.0-bridge.0: + version "7.0.0-bridge.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" + integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg== + +babel-core@^6.0.0, babel-core@^6.26.0: + version "6.26.3" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" + integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== + dependencies: + babel-code-frame "^6.26.0" + babel-generator "^6.26.0" + babel-helpers "^6.24.1" + babel-messages "^6.23.0" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + convert-source-map "^1.5.1" + debug "^2.6.9" + json5 "^0.5.1" + lodash "^4.17.4" + minimatch "^3.0.4" + path-is-absolute "^1.0.1" + private "^0.1.8" + slash "^1.0.0" + source-map "^0.5.7" + +babel-eslint@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-9.0.0.tgz#7d9445f81ed9f60aff38115f838970df9f2b6220" + integrity sha512-itv1MwE3TMbY0QtNfeL7wzak1mV47Uy+n6HtSOO4Xd7rvmO+tsGQSgyOEEgo6Y2vHZKZphaoelNeSVj4vkLA1g== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.0.0" + "@babel/traverse" "^7.0.0" + "@babel/types" "^7.0.0" + eslint-scope "3.7.1" + eslint-visitor-keys "^1.0.0" + +babel-eslint@^8.2.3: + version "8.2.6" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.6.tgz#6270d0c73205628067c0f7ae1693a9e797acefd9" + integrity sha512-aCdHjhzcILdP8c9lej7hvXKvQieyRt20SF102SIGyY4cUIiw6UaAtK4j2o3dXX74jEmy0TJ0CEhv4fTIM3SzcA== + dependencies: + "@babel/code-frame" "7.0.0-beta.44" + "@babel/traverse" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" + eslint-scope "3.7.1" + eslint-visitor-keys "^1.0.0" + +babel-extract-comments@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/babel-extract-comments/-/babel-extract-comments-1.0.0.tgz#0a2aedf81417ed391b85e18b4614e693a0351a21" + integrity sha512-qWWzi4TlddohA91bFwgt6zO/J0X+io7Qp184Fw0m2JYRSTZnJbFR8+07KmzudHCZgOiKRCrjhylwv9Xd8gfhVQ== + dependencies: + babylon "^6.18.0" + +babel-generator@^6.18.0, babel-generator@^6.26.0: + version "6.26.1" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" + integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.17.4" + source-map "^0.5.7" + trim-right "^1.0.1" + +babel-helpers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" + integrity sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI= + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-jest@23.6.0, babel-jest@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-23.6.0.tgz#a644232366557a2240a0c083da6b25786185a2f1" + integrity sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew== + dependencies: + babel-plugin-istanbul "^4.1.6" + babel-preset-jest "^23.2.0" + +babel-loader@8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.4.tgz#7bbf20cbe4560629e2e41534147692d3fecbdce6" + integrity sha512-fhBhNkUToJcW9nV46v8w87AJOwAJDz84c1CL57n3Stj73FANM/b9TbCUK4YhdOwEyZ+OxhYpdeZDNzSI29Firw== + dependencies: + find-cache-dir "^1.0.0" + loader-utils "^1.0.2" + mkdirp "^0.5.1" + util.promisify "^1.0.0" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-dynamic-import-node@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.2.0.tgz#c0adfb07d95f4a4495e9aaac6ec386c4d7c2524e" + integrity sha512-fP899ELUnTaBcIzmrW7nniyqqdYWrWuJUyPWHxFa/c7r7hS6KC8FscNfLlBNIoPSc55kYMGEEKjPjJGCLbE1qA== + dependencies: + object.assign "^4.1.0" + +babel-plugin-istanbul@^4.1.6: + version "4.1.6" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45" + integrity sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ== + dependencies: + babel-plugin-syntax-object-rest-spread "^6.13.0" + find-up "^2.1.0" + istanbul-lib-instrument "^1.10.1" + test-exclude "^4.2.1" + +babel-plugin-jest-hoist@^23.2.0: + version "23.2.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-23.2.0.tgz#e61fae05a1ca8801aadee57a6d66b8cefaf44167" + integrity sha1-5h+uBaHKiAGq3uV6bWa4zvr0QWc= + +babel-plugin-macros@2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.4.2.tgz#21b1a2e82e2130403c5ff785cba6548e9b644b28" + integrity sha512-NBVpEWN4OQ/bHnu1fyDaAaTPAjnhXCEPqr1RwqxrU7b6tZ2hypp+zX4hlNfmVGfClD5c3Sl6Hfj5TJNF5VG5aA== + dependencies: + cosmiconfig "^5.0.5" + resolve "^1.8.1" + +babel-plugin-named-asset-import@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.2.2.tgz#af1290f77e073411ef1a12f17fc458f1111122eb" + integrity sha512-NtESBqk8LZuNhBd1BMLxDOh0JPytMs88LwAZFmHg1ZyuGrIAO40dw7p624w+flj0uuhfKTNY8tYKsUEAZGRRFA== + +babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" + integrity sha1-/WU28rzhODb/o6VFjEkDpZe7O/U= + +babel-plugin-transform-object-rest-spread@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" + integrity sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY= + dependencies: + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-runtime "^6.26.0" + +babel-plugin-transform-react-remove-prop-types@0.4.18: + version "0.4.18" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.18.tgz#85ff79d66047b34288c6f7cc986b8854ab384f8c" + integrity sha512-azed2nHo8vmOy7EY26KH+om5oOcWRs0r1U8wOmhwta+SBMMnmJ4H6yaBZRCcHBtMeWp9AVhvBTL/lpR1kEx+Xw== + +babel-preset-jest@^23.2.0: + version "23.2.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-23.2.0.tgz#8ec7a03a138f001a1a8fb1e8113652bf1a55da46" + integrity sha1-jsegOhOPABoaj7HoETZSvxpV2kY= + dependencies: + babel-plugin-jest-hoist "^23.2.0" + babel-plugin-syntax-object-rest-spread "^6.13.0" + +babel-preset-react-app@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/babel-preset-react-app/-/babel-preset-react-app-5.0.4.tgz#e64a875071af1637a712b68f429551988ec5ebe4" + integrity sha512-NQ344N1BXY4ur8c7iRqj1JQvcI6tiLbNB8W2z0Vanmc6xld+EG9CYk9cfIFTPgk5RCkughimt+0uw2nd2CyevQ== + dependencies: + "@babel/core" "7.1.0" + "@babel/plugin-proposal-class-properties" "7.1.0" + "@babel/plugin-proposal-object-rest-spread" "7.0.0" + "@babel/plugin-syntax-dynamic-import" "7.0.0" + "@babel/plugin-transform-classes" "7.1.0" + "@babel/plugin-transform-destructuring" "7.0.0" + "@babel/plugin-transform-flow-strip-types" "7.0.0" + "@babel/plugin-transform-react-constant-elements" "7.0.0" + "@babel/plugin-transform-react-display-name" "7.0.0" + "@babel/plugin-transform-runtime" "7.1.0" + "@babel/preset-env" "7.1.0" + "@babel/preset-react" "7.0.0" + "@babel/runtime" "7.0.0" + babel-loader "8.0.4" + babel-plugin-dynamic-import-node "2.2.0" + babel-plugin-macros "2.4.2" + babel-plugin-transform-react-remove-prop-types "0.4.18" + +babel-register@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" + integrity sha1-btAhFz4vy0htestFxgCahW9kcHE= + dependencies: + babel-core "^6.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" + home-or-tmp "^2.0.0" + lodash "^4.17.4" + mkdirp "^0.5.1" + source-map-support "^0.4.15" + +babel-runtime@^6.22.0, babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + +babel-traverse@^6.0.0, babel-traverse@^6.18.0, babel-traverse@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + +babel-types@^6.0.0, babel-types@^6.18.0, babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babylon@7.0.0-beta.44: + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d" + integrity sha512-5Hlm13BJVAioCHpImtFqNOF2H3ieTOHd0fmFGMxOJ9jgeFqeAwsv3u5P5cR7CSeFrkgHsT19DgFJkHV0/Mcd8g== + +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base64-js@^1.0.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" + integrity sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw== + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +bfj@6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/bfj/-/bfj-6.1.1.tgz#05a3b7784fbd72cfa3c22e56002ef99336516c48" + integrity sha512-+GUNvzHR4nRyGybQc2WpNJL4MJazMuvf92ueIyA0bIkPRwhhQu3IfZQ2PSoVPpCBJfmoSdOxu5rnotfFLlvYRQ== + dependencies: + bluebird "^3.5.1" + check-types "^7.3.0" + hoopy "^0.1.2" + tryer "^1.0.0" + +big.js@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" + integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== + +binary-extensions@^1.0.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14" + integrity sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg== + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= + dependencies: + inherits "~2.0.0" + +bluebird@^3.5.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.2.tgz#1be0908e054a751754549c270489c1505d4ab15a" + integrity sha512-dhHTWMI7kMx5whMQntl7Vr9C6BvV10lFXDAasnqnrMYhXVCzzk6IO9Fo2L75jXHT07WrOngL1WDXOp+yYS91Yg== + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== + +body-parser@1.18.3: + version "1.18.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" + integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ= + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "~1.6.3" + iconv-lite "0.4.23" + on-finished "~2.3.0" + qs "6.5.2" + raw-body "2.3.3" + type-is "~1.6.16" + +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + +boolbase@^1.0.0, boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + integrity sha1-uneWLhLf+WnWt2cR6RS3N4V79qc= + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +braces@^2.3.0, braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= + +browser-process-hrtime@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4" + integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw== + +browser-resolve@^1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" + integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== + dependencies: + resolve "1.1.7" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" + integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" + integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + integrity sha1-qk62jl17ZYuqa/alfmMMvXqT0pg= + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== + dependencies: + pako "~1.0.5" + +browserslist@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.1.1.tgz#328eb4ff1215b12df6589e9ab82f8adaa4fc8cd6" + integrity sha512-VBorw+tgpOtZ1BYhrVSVTzTt/3+vSE3eFUh0N2GCFK1HffceOaf32YS/bs6WiFhjDAblAFrx85jMy3BG9fBK2Q== + dependencies: + caniuse-lite "^1.0.30000884" + electron-to-chromium "^1.3.62" + node-releases "^1.0.0-alpha.11" + +browserslist@^4.0.0, browserslist@^4.1.0, browserslist@^4.1.1, browserslist@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.2.1.tgz#257a24c879d1cd4016348eee5c25de683260b21d" + integrity sha512-1oO0c7Zhejwd+LXihS89WqtKionSbz298rJZKJgfrHIZhrV8AC15gw553VcB0lcEugja7IhWD7iAlrsamfYVPA== + dependencies: + caniuse-lite "^1.0.30000890" + electron-to-chromium "^1.3.79" + node-releases "^1.0.0-alpha.14" + +bser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" + integrity sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk= + dependencies: + node-int64 "^0.4.0" + +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-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= + +buffer@^4.3.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +cacache@^10.0.4: + version "10.0.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" + integrity sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA== + dependencies: + bluebird "^3.5.1" + chownr "^1.0.1" + glob "^7.1.2" + graceful-fs "^4.1.11" + lru-cache "^4.1.1" + mississippi "^2.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.2" + ssri "^5.2.4" + unique-filename "^1.1.0" + y18n "^4.0.0" + +cacache@^11.0.2: + version "11.2.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.2.0.tgz#617bdc0b02844af56310e411c0878941d5739965" + integrity sha512-IFWl6lfK6wSeYCHUXh+N1lY72UDrpyrYQJNIVQf48paDuWbv5RbAtJYf/4gUQFObTCHZwdZ5sI8Iw7nqwP6nlQ== + dependencies: + bluebird "^3.5.1" + chownr "^1.0.1" + figgy-pudding "^3.1.0" + glob "^7.1.2" + graceful-fs "^4.1.11" + lru-cache "^4.1.3" + mississippi "^3.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.2" + ssri "^6.0.0" + unique-filename "^1.1.0" + y18n "^4.0.0" + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +caller-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" + integrity sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8= + dependencies: + callsites "^0.2.0" + +callsites@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" + integrity sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo= + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= + +camel-case@3.0.x: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" + integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M= + dependencies: + no-case "^2.2.0" + upper-case "^1.1.1" + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= + +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= + +camelcase@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" + integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000884, caniuse-lite@^1.0.30000887, caniuse-lite@^1.0.30000890: + version "1.0.30000890" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000890.tgz#86a18ffcc65d79ec6a437e985761b8bf1c4efeaf" + integrity sha512-4NI3s4Y6ROm+SgZN5sLUG4k7nVWQnedis3c/RWkynV5G6cHSY7+a8fwFyn2yoBDE3E6VswhTNNwR3PvzGqlTkg== + +capture-exit@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f" + integrity sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28= + dependencies: + rsvp "^3.3.3" + +case-sensitive-paths-webpack-plugin@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.1.2.tgz#c899b52175763689224571dad778742e133f0192" + integrity sha512-oEZgAFfEvKtjSRCu6VgYkuGxwrWXMnQzyBmlLPP7r6PWQVtHxP5Z5N6XsuJvtoVax78am/r7lr46bwo3IVEBOg== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@2.4.1, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" + integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +check-types@^7.3.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.4.0.tgz#0378ec1b9616ec71f774931a3c6516fad8c152f4" + integrity sha512-YbulWHdfP99UfZ73NcUDlNJhEIDgm9Doq9GhpyXbF+7Aegi3CVV7qqMCKTTqJxlvEvnQBp9IA+dxsGN6xK/nSg== + +chokidar@^2.0.0, chokidar@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26" + integrity sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.0" + braces "^2.3.0" + glob-parent "^3.1.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + lodash.debounce "^4.0.8" + normalize-path "^2.1.1" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + upath "^1.0.5" + optionalDependencies: + fsevents "^1.2.2" + +chownr@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" + integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== + +chrome-trace-event@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz#45a91bd2c20c9411f0963b5aaeb9a1b95e09cc48" + integrity sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A== + dependencies: + tslib "^1.9.0" + +ci-info@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" + integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +circular-json@^0.3.1: + version "0.3.3" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +clean-css@4.2.x: + version "4.2.1" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17" + integrity sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g== + dependencies: + source-map "~0.6.0" + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= + dependencies: + restore-cursor "^2.0.0" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +cliui@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" + integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + +clone-deep@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.2.4.tgz#4e73dd09e9fb971cc38670c5dced9c1896481cc6" + integrity sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY= + dependencies: + for-own "^0.1.3" + is-plain-object "^2.0.1" + kind-of "^3.0.2" + lazy-cache "^1.0.3" + shallow-clone "^0.1.2" + +clone-deep@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713" + integrity sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ== + dependencies: + for-own "^1.0.0" + is-plain-object "^2.0.4" + kind-of "^6.0.0" + shallow-clone "^1.0.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +coa@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.1.tgz#f3f8b0b15073e35d70263fb1042cb2c023db38af" + integrity sha512-5wfTTO8E2/ja4jFSxePXlG5nRu5bBtL/r1HCIpJW/lzT6yDtKl0u0Z4o/Vpz32IpKmBn7HerheEZQgA9N2DarQ== + dependencies: + q "^1.1.2" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0, color-convert@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" + integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.0.tgz#d8e9fb096732875774c84bf922815df0308d0ffc" + integrity sha512-CwyopLkuRYO5ei2EpzpIh6LqJMt6Mt+jZhO5VI5f/wJLZriXQE32/SSqzmrh+QB+AZT81Cj8yv+7zwToW8ahZg== + dependencies: + color-convert "^1.9.1" + color-string "^1.5.2" + +colors@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM= + +combined-stream@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" + integrity sha1-cj599ugBrFYTETp+RFqbactjKBg= + dependencies: + delayed-stream "~1.0.0" + +combined-stream@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" + integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== + dependencies: + delayed-stream "~1.0.0" + +commander@2.17.x, commander@~2.17.1: + version "2.17.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" + integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== + +commander@^2.11.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== + +commander@~2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" + integrity sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA== + +common-tags@^1.4.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" + integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + +component-emitter@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= + +compressible@~2.0.14: + version "2.0.15" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.15.tgz#857a9ab0a7e5a07d8d837ed43fe2defff64fe212" + integrity sha512-4aE67DL33dSW9gw4CI2H/yTxqHLNcxp0yS6jB+4h+wr3e43+1z7vm0HU9qXOH8j+qjKuL8+UtkOxYQSMq60Ylw== + dependencies: + mime-db ">= 1.36.0 < 2" + +compression@^1.5.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.3.tgz#27e0e176aaf260f7f2c2813c3e440adb9f1993db" + integrity sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.14" + debug "2.6.9" + on-headers "~1.0.1" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "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" + +confusing-browser-globals@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.4.tgz#7d3ae232117abdf158f968c1ebeeab42ea980991" + integrity sha512-oS1wYNQBHNxM7xnwetiG8X4QjEbWwSzQjcBTtsz0HTNLCTBsepo12z557p9I9F9gYCHaVGQ3rcJD/tQgprGlRA== + +connect-history-api-fallback@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a" + integrity sha1-sGhzk0vF40T+9hGhlqb6rgruAVo= + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + integrity sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA= + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= + +contains-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" + integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" + integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== + dependencies: + safe-buffer "~5.1.1" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= + +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +core-js@2.5.7, core-js@^2.4.0, core-js@^2.5.0: + version "2.5.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" + integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw== + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cosmiconfig@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-4.0.0.tgz#760391549580bbd2df1e562bc177b13c290972dc" + integrity sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ== + dependencies: + is-directory "^0.3.1" + js-yaml "^3.9.0" + parse-json "^4.0.0" + require-from-string "^2.0.1" + +cosmiconfig@^5.0.0, cosmiconfig@^5.0.5, cosmiconfig@^5.0.6: + version "5.0.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.6.tgz#dca6cf680a0bd03589aff684700858c81abeeb39" + integrity sha512-6DWfizHriCrFWURP1/qyhsiFvYdlJzbCzmtFWh744+KyWsJo5+kPzUZZaMRSSItoYc0pxFX7gEO7ZC1/gN/7AQ== + dependencies: + is-directory "^0.3.1" + js-yaml "^3.9.0" + parse-json "^4.0.0" + +create-ecdh@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" + integrity sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw== + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" + integrity sha1-ElYDfsufDF9549bvE14wdwGEuYI= + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +css-color-names@0.0.4, css-color-names@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= + +css-declaration-sorter@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22" + integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA== + dependencies: + postcss "^7.0.1" + timsort "^0.3.0" + +css-loader@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.0.tgz#9f46aaa5ca41dbe31860e3b62b8e23c42916bf56" + integrity sha512-tMXlTYf3mIMt3b0dDCOQFJiVvxbocJ5Ho577WiGPYPZcqVEO218L2iU22pDXzkTZCLDE+9AmGSUkWxeh/nZReA== + dependencies: + babel-code-frame "^6.26.0" + css-selector-tokenizer "^0.7.0" + icss-utils "^2.1.0" + loader-utils "^1.0.2" + lodash.camelcase "^4.3.0" + postcss "^6.0.23" + postcss-modules-extract-imports "^1.2.0" + postcss-modules-local-by-default "^1.2.0" + postcss-modules-scope "^1.1.0" + postcss-modules-values "^1.3.0" + postcss-value-parser "^3.3.0" + source-list-map "^2.0.0" + +css-select-base-adapter@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.0.tgz#0102b3d14630df86c3eb9fa9f5456270106cf990" + integrity sha1-AQKz0UYw34bD65+p9UVicBBs+ZA= + +css-select@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-select@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.0.0.tgz#7aa2921392114831f68db175c0b6a555df74bbd5" + integrity sha512-MGhoq1S9EyPgZIGnts8Yz5WwUOyHmPMdlqeifsYs/xFX7AAm3hY0RJe1dqVlXtYPI66Nsk39R/sa5/ree6L2qg== + dependencies: + boolbase "^1.0.0" + css-what "2.1" + domutils "^1.7.0" + nth-check "^1.0.1" + +css-selector-tokenizer@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86" + integrity sha1-5piEdK6MlTR3v15+/s/OzNnPTIY= + dependencies: + cssesc "^0.1.0" + fastparse "^1.1.1" + regexpu-core "^1.0.0" + +css-tree@1.0.0-alpha.28: + version "1.0.0-alpha.28" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.28.tgz#8e8968190d886c9477bc8d61e96f61af3f7ffa7f" + integrity sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w== + dependencies: + mdn-data "~1.1.0" + source-map "^0.5.3" + +css-tree@1.0.0-alpha.29: + version "1.0.0-alpha.29" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39" + integrity sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg== + dependencies: + mdn-data "~1.1.0" + source-map "^0.5.3" + +css-unit-converter@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996" + integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY= + +css-url-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/css-url-regex/-/css-url-regex-1.1.0.tgz#83834230cc9f74c457de59eebd1543feeb83b7ec" + integrity sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w= + +css-what@2.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd" + integrity sha1-lGfQMsOM+u+58teVASUwYvh/ob0= + +cssdb@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-3.2.1.tgz#65e7dc90be476ce5b6e567b19f3bd73a8c66bcb5" + integrity sha512-I0IS8zvxED8sQtFZnV7M+AkhWqTgp1HIyfMQJBbjdn4GgurBt7NCZaDgrWiAN2kNJN34mhF1p50aZIMQu290mA== + +cssesc@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" + integrity sha1-yBSQPkViM3GgR3tAEJqq++6t27Q= + +cssesc@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-1.0.1.tgz#ef7bd8d0229ed6a3a7051ff7771265fe7330e0a8" + integrity sha512-S2hzrpWvE6G/rW7i7IxJfWBYn27QWfOIncUW++8Rbo1VB5zsJDSVPcnI+Q8z7rhxT6/yZeLOCja4cZnghJrNGA== + +cssnano-preset-default@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.2.tgz#1de3f27e73b7f0fbf87c1d7fd7a63ae980ac3774" + integrity sha512-zO9PeP84l1E4kbrdyF7NSLtA/JrJY1paX5FHy5+w/ziIXO2kDqDMfJ/mosXkaHHSa3RPiIY3eB6aEgwx3IiGqA== + dependencies: + css-declaration-sorter "^4.0.1" + cssnano-util-raw-cache "^4.0.1" + postcss "^7.0.0" + postcss-calc "^6.0.2" + postcss-colormin "^4.0.2" + postcss-convert-values "^4.0.1" + postcss-discard-comments "^4.0.1" + postcss-discard-duplicates "^4.0.2" + postcss-discard-empty "^4.0.1" + postcss-discard-overridden "^4.0.1" + postcss-merge-longhand "^4.0.6" + postcss-merge-rules "^4.0.2" + postcss-minify-font-values "^4.0.2" + postcss-minify-gradients "^4.0.1" + postcss-minify-params "^4.0.1" + postcss-minify-selectors "^4.0.1" + postcss-normalize-charset "^4.0.1" + postcss-normalize-display-values "^4.0.1" + postcss-normalize-positions "^4.0.1" + postcss-normalize-repeat-style "^4.0.1" + postcss-normalize-string "^4.0.1" + postcss-normalize-timing-functions "^4.0.1" + postcss-normalize-unicode "^4.0.1" + postcss-normalize-url "^4.0.1" + postcss-normalize-whitespace "^4.0.1" + postcss-ordered-values "^4.1.1" + postcss-reduce-initial "^4.0.2" + postcss-reduce-transforms "^4.0.1" + postcss-svgo "^4.0.1" + postcss-unique-selectors "^4.0.1" + +cssnano-util-get-arguments@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f" + integrity sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8= + +cssnano-util-get-match@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d" + integrity sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0= + +cssnano-util-raw-cache@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282" + integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA== + dependencies: + postcss "^7.0.0" + +cssnano-util-same-parent@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" + integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== + +cssnano@^4.1.0: + version "4.1.4" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.4.tgz#55b71e3d8f5451dd3edc7955673415c98795788f" + integrity sha512-wP0wbOM9oqsek14CiNRYrK9N3w3jgadtGZKHXysgC/OMVpy0KZgWVPdNqODSZbz7txO9Gekr9taOfcCgL0pOOw== + dependencies: + cosmiconfig "^5.0.0" + cssnano-preset-default "^4.0.2" + is-resolvable "^1.0.0" + postcss "^7.0.0" + +csso@^3.5.0: + version "3.5.1" + resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b" + integrity sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg== + dependencies: + css-tree "1.0.0-alpha.29" + +cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0", cssom@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.4.tgz#8cd52e8a3acfd68d3aed38ee0a640177d2f9d797" + integrity sha512-+7prCSORpXNeR4/fUP3rL+TzqtiFfhMvTd7uEqMdgPvLPt4+uzFUeufx5RHjGTACCargg/DiEt/moMQmvnfkog== + +cssstyle@^1.0.0, cssstyle@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.1.1.tgz#18b038a9c44d65f7a8e428a653b9f6fe42faf5fb" + integrity sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog== + dependencies: + cssom "0.3.x" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= + dependencies: + array-find-index "^1.0.1" + +cyclist@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" + integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= + +damerau-levenshtein@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514" + integrity sha1-AxkcQyy27qFou3fzpV/9zLiXhRQ= + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +data-urls@^1.0.0, data-urls@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.0.1.tgz#d416ac3896918f29ca84d81085bc3705834da579" + integrity sha512-0HdcMZzK6ubMUnsMmQmG0AcLQPvbvb47R0+7CCZQCYgcd8OUWG91CG7sM6GoXgjz+WLl4ArFzHtBMy/QqSF4eg== + dependencies: + abab "^2.0.0" + whatwg-mimetype "^2.1.0" + whatwg-url "^7.0.0" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= + +debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decamelize@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-2.0.0.tgz#656d7bbc8094c4c788ea53c5840908c9c7d063c7" + integrity sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg== + dependencies: + xregexp "4.0.0" + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +deep-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +default-gateway@^2.6.0: + version "2.7.2" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-2.7.2.tgz#b7ef339e5e024b045467af403d50348db4642d0f" + integrity sha512-lAc4i9QJR0YHSDFdzeBQKfZ1SRDG3hsJNEkrpcZa8QhBfidLAilT60BDEIVUUGqosFp425KOgB3uYqcnQrWafQ== + dependencies: + execa "^0.10.0" + ip-regex "^2.1.0" + +default-require-extensions@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" + integrity sha1-836hXT4T/9m0N9M+GnW1+5eHTLg= + dependencies: + strip-bom "^2.0.0" + +define-properties@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +del@^2.0.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + integrity sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag= + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +del@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" + integrity sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU= + dependencies: + globby "^6.1.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + p-map "^1.1.1" + pify "^3.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + integrity sha1-wHTS4qpqipoH29YfmhXCzYPsjsw= + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= + dependencies: + repeating "^2.0.0" + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + +detect-newline@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" + integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= + +detect-node@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" + integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== + +detect-port-alt@1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" + integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== + dependencies: + address "^1.0.1" + debug "^2.6.0" + +diff@^3.2.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +diffie-hellman@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= + +dns-packet@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" + integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= + dependencies: + buffer-indexof "^1.0.0" + +doctrine@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +dom-converter@~0.2: + version "0.2.0" + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + +dom-serializer@0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" + integrity sha1-BzxpdUbOB4DOI75KKOKT5AvDDII= + dependencies: + domelementtype "~1.1.1" + entities "~1.1.1" + +domain-browser@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== + +domelementtype@1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" + integrity sha1-sXrtguirWeUt2cGbF1bg/BhyBMI= + +domelementtype@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" + integrity sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs= + +domexception@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" + integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== + dependencies: + webidl-conversions "^4.0.2" + +domhandler@2.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.1.0.tgz#d2646f5e57f6c3bab11cf6cb05d3c0acf7412594" + integrity sha1-0mRvXlf2w7qxHPbLBdPArPdBJZQ= + dependencies: + domelementtype "1" + +domutils@1.1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485" + integrity sha1-vdw94Jm5ou+sxRxiPyj0FuzFdIU= + dependencies: + domelementtype "1" + +domutils@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +dot-prop@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" + integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ== + dependencies: + is-obj "^1.0.0" + +dotenv-expand@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-4.2.0.tgz#def1f1ca5d6059d24a766e587942c21106ce1275" + integrity sha1-3vHxyl1gWdJKdm5YeULCEQbOEnU= + +dotenv@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.0.0.tgz#24e37c041741c5f4b25324958ebbc34bca965935" + integrity sha512-FlWbnhgjtwD+uNLUGHbMykMOYQaTivdHEmYwAKFjn6GKe/CqY0fNae93ZHTd20snh9ZLr8mTzIL9m0APQ1pjQg== + +duplexer@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= + +duplexify@^3.4.2, duplexify@^3.6.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.6.1.tgz#b1a7a29c4abfd639585efaecce80d666b1e34125" + integrity sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +electron-to-chromium@^1.3.62, electron-to-chromium@^1.3.79: + version "1.3.79" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.79.tgz#774718f06284a4bf8f578ac67e74508fe659f13a" + integrity sha512-LQdY3j4PxuUl6xfxiFruTSlCniTrTrzAd8/HfsLEMi0PUpaQ0Iy+Pr4N4VllDYjs0Hyu2lkTbvzqlG+PX9NsNw== + +elliptic@^6.0.0: + version "6.4.1" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.1.tgz#c2d0b7776911b86722c632c3c06c60f2f819939a" + integrity sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ== + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +emoji-regex@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2" + integrity sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ== + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" + integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + tapable "^1.0.0" + +entities@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" + integrity sha1-blwtClYhtdra7O+AuQ7ftc13cvA= + +errno@^0.1.3, errno@~0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" + integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== + dependencies: + prr "~1.0.1" + +error-ex@^1.2.0, error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" + integrity sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA== + dependencies: + es-to-primitive "^1.1.1" + function-bind "^1.1.1" + has "^1.0.1" + is-callable "^1.1.3" + is-regex "^1.0.4" + +es-to-primitive@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" + integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escodegen@^1.11.0, escodegen@^1.9.1: + version "1.11.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589" + integrity sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw== + dependencies: + esprima "^3.1.3" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +eslint-config-react-app@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-3.0.4.tgz#83f394d765e7d5af623d773e64609e9c9f2cbeb5" + integrity sha512-Fos37wkB5QNc8Qh7HUq/ChMUi1njLE78Y2U0RD3MOtYRtPNrMyYw1pLufb8GwNSphMSgRgOkIdakc3d0yXbWyQ== + dependencies: + confusing-browser-globals "^1.0.4" + +eslint-import-resolver-node@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" + integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q== + dependencies: + debug "^2.6.9" + resolve "^1.5.0" + +eslint-loader@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/eslint-loader/-/eslint-loader-2.1.1.tgz#2a9251523652430bfdd643efdb0afc1a2a89546a" + integrity sha512-1GrJFfSevQdYpoDzx8mEE2TDWsb/zmFuY09l6hURg1AeFIKQOvZ+vH0UPjzmd1CZIbfTV5HUkMeBmFiDBkgIsQ== + dependencies: + loader-fs-cache "^1.0.0" + loader-utils "^1.0.2" + object-assign "^4.0.1" + object-hash "^1.1.4" + rimraf "^2.6.1" + +eslint-module-utils@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz#b270362cd88b1a48ad308976ce7fa54e98411746" + integrity sha1-snA2LNiLGkitMIl2zn+lTphBF0Y= + dependencies: + debug "^2.6.8" + pkg-dir "^1.0.0" + +eslint-plugin-flowtype@2.50.1: + version "2.50.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.50.1.tgz#36d4c961ac8b9e9e1dc091d3fba0537dad34ae8a" + integrity sha512-9kRxF9hfM/O6WGZcZPszOVPd2W0TLHBtceulLTsGfwMPtiCCLnCW0ssRiOOiXyqrCA20pm1iXdXm7gQeN306zQ== + dependencies: + lodash "^4.17.10" + +eslint-plugin-import@2.14.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz#6b17626d2e3e6ad52cfce8807a845d15e22111a8" + integrity sha512-FpuRtniD/AY6sXByma2Wr0TXvXJ4nA/2/04VPlfpmUDPOpOY264x+ILiwnrk/k4RINgDAyFZByxqPUbSQ5YE7g== + dependencies: + contains-path "^0.1.0" + debug "^2.6.8" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.1" + eslint-module-utils "^2.2.0" + has "^1.0.1" + lodash "^4.17.4" + minimatch "^3.0.3" + read-pkg-up "^2.0.0" + resolve "^1.6.0" + +eslint-plugin-jsx-a11y@6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.1.2.tgz#69bca4890b36dcf0fe16dd2129d2d88b98f33f88" + integrity sha512-7gSSmwb3A+fQwtw0arguwMdOdzmKUgnUcbSNlo+GjKLAQFuC2EZxWqG9XHRI8VscBJD5a8raz3RuxQNFW+XJbw== + dependencies: + aria-query "^3.0.0" + array-includes "^3.0.3" + ast-types-flow "^0.0.7" + axobject-query "^2.0.1" + damerau-levenshtein "^1.0.4" + emoji-regex "^6.5.1" + has "^1.0.3" + jsx-ast-utils "^2.0.1" + +eslint-plugin-react@7.11.1: + version "7.11.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.11.1.tgz#c01a7af6f17519457d6116aa94fc6d2ccad5443c" + integrity sha512-cVVyMadRyW7qsIUh3FHp3u6QHNhOgVrLQYdQEB1bPWBsgbNCHdFAeNMquBMCcZJu59eNthX053L70l7gRt4SCw== + dependencies: + array-includes "^3.0.3" + doctrine "^2.1.0" + has "^1.0.3" + jsx-ast-utils "^2.0.1" + prop-types "^15.6.2" + +eslint-scope@3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" + integrity sha1-PWPD7f2gLgbgGkUq2IyqzHzctug= + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-scope@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" + integrity sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-utils@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512" + integrity sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q== + +eslint-visitor-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" + integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ== + +eslint@5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.6.0.tgz#b6f7806041af01f71b3f1895cbb20971ea4b6223" + integrity sha512-/eVYs9VVVboX286mBK7bbKnO1yamUy2UCRjiY6MryhQL2PaaXCExsCQ2aO83OeYRhU2eCU/FMFP+tVMoOrzNrA== + dependencies: + "@babel/code-frame" "^7.0.0" + ajv "^6.5.3" + chalk "^2.1.0" + cross-spawn "^6.0.5" + debug "^3.1.0" + doctrine "^2.1.0" + eslint-scope "^4.0.0" + eslint-utils "^1.3.1" + eslint-visitor-keys "^1.0.0" + espree "^4.0.0" + esquery "^1.0.1" + esutils "^2.0.2" + file-entry-cache "^2.0.0" + functional-red-black-tree "^1.0.1" + glob "^7.1.2" + globals "^11.7.0" + ignore "^4.0.6" + imurmurhash "^0.1.4" + inquirer "^6.1.0" + is-resolvable "^1.1.0" + js-yaml "^3.12.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.5" + minimatch "^3.0.4" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + path-is-inside "^1.0.2" + pluralize "^7.0.0" + progress "^2.0.0" + regexpp "^2.0.0" + require-uncached "^1.0.3" + semver "^5.5.1" + strip-ansi "^4.0.0" + strip-json-comments "^2.0.1" + table "^4.0.3" + text-table "^0.2.0" + +espree@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-4.0.0.tgz#253998f20a0f82db5d866385799d912a83a36634" + integrity sha512-kapdTCt1bjmspxStVKX6huolXVV5ZfyZguY1lcfhVVZstce3bqxH9mcLzNn3/mlgW6wQ732+0fuG9v7h0ZQoKg== + dependencies: + acorn "^5.6.0" + acorn-jsx "^4.1.1" + +esprima@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" + integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" + integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA== + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== + dependencies: + estraverse "^4.1.0" + +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= + +esutils@^2.0.0, esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +eventemitter3@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" + integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== + +events@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= + +eventsource@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232" + integrity sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI= + dependencies: + original ">=0.0.5" + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +exec-sh@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" + integrity sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw== + dependencies: + merge "^1.2.0" + +execa@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" + integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== + dependencies: + cross-spawn "^6.0.0" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + integrity sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s= + dependencies: + is-posix-bracket "^0.1.0" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + integrity sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc= + dependencies: + fill-range "^2.1.0" + +expand-tilde@^2.0.0, expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" + integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= + dependencies: + homedir-polyfill "^1.0.1" + +expect@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-23.6.0.tgz#1e0c8d3ba9a581c87bd71fb9bc8862d443425f98" + integrity sha512-dgSoOHgmtn/aDGRVFWclQyPDKl2CQRq0hmIEoUAuQs/2rn2NcvCWcSCovm6BLeuB/7EZuLGu2QfnR+qRt5OM4w== + dependencies: + ansi-styles "^3.2.0" + jest-diff "^23.6.0" + jest-get-type "^22.1.0" + jest-matcher-utils "^23.6.0" + jest-message-util "^23.4.0" + jest-regex-util "^23.3.0" + +express@^4.16.2: + version "4.16.4" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" + integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg== + dependencies: + accepts "~1.3.5" + array-flatten "1.1.1" + body-parser "1.18.3" + content-disposition "0.5.2" + content-type "~1.0.4" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.1.1" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + proxy-addr "~2.0.4" + qs "6.5.2" + range-parser "~1.2.0" + safe-buffer "5.1.2" + send "0.16.2" + serve-static "1.13.2" + setprototypeof "1.1.0" + statuses "~1.4.0" + type-is "~1.6.16" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +external-editor@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" + integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + integrity sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE= + dependencies: + is-extglob "^1.0.0" + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= + +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fastparse@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" + integrity sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg= + +faye-websocket@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ= + dependencies: + websocket-driver ">=0.5.1" + +faye-websocket@~0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38" + integrity sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg= + dependencies: + websocket-driver ">=0.5.1" + +fb-watchman@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" + integrity sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg= + dependencies: + bser "^2.0.0" + +figgy-pudding@^3.1.0, figgy-pudding@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" + integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" + integrity sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E= + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + +file-loader@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-2.0.0.tgz#39749c82f020b9e85901dcff98e8004e6401cfde" + integrity sha512-YCsBfd1ZGCyonOKLxPiKPdu+8ld9HAaMEvJewzz+b2eTF7uL5Zm/HdBF6FjCrpCMRq25Mi0U1gl4pwn2TlH7hQ== + dependencies: + loader-utils "^1.0.2" + schema-utils "^1.0.0" + +filename-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" + integrity sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY= + +fileset@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0" + integrity sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA= + dependencies: + glob "^7.0.3" + minimatch "^3.0.3" + +filesize@3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317" + integrity sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg== + +fill-range@^2.1.0: + version "2.2.4" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565" + integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q== + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^3.0.0" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +finalhandler@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" + integrity sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.2" + statuses "~1.4.0" + unpipe "~1.0.0" + +find-cache-dir@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9" + integrity sha1-yN765XyKUqinhPnjHFfHQumToLk= + dependencies: + commondir "^1.0.1" + mkdirp "^0.5.1" + pkg-dir "^1.0.0" + +find-cache-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" + integrity sha1-kojj6ePMN0hxfTnq3hfPcfww7m8= + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^2.0.0" + +find-cache-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d" + integrity sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA== + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^3.0.0" + +find-up@3.0.0, find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= + dependencies: + locate-path "^2.0.0" + +flat-cache@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481" + integrity sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE= + dependencies: + circular-json "^0.3.1" + del "^2.0.2" + graceful-fs "^4.1.2" + write "^0.2.1" + +flatten@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" + integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I= + +flush-write-stream@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd" + integrity sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw== + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.4" + +follow-redirects@^1.0.0: + version "1.5.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.9.tgz#c9ed9d748b814a39535716e531b9196a845d89c6" + integrity sha512-Bh65EZI/RU8nx0wbYF9shkFZlqLP+6WT/5FnA3cE/djNSuKNHJEinGGZgu/cQEkeeb2GdFOgenAmn8qaqYke2w== + dependencies: + debug "=3.1.0" + +for-in@^0.1.3: + version "0.1.8" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" + integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE= + +for-in@^1.0.1, for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +for-own@^0.1.3, for-own@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4= + dependencies: + for-in "^1.0.1" + +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" + integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= + dependencies: + for-in "^1.0.1" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + integrity sha1-SXBJi+YEwgwAXU9cI67NIda0kJk= + dependencies: + asynckit "^0.4.0" + combined-stream "1.0.6" + mime-types "^2.1.12" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-extra@7.0.0, fs-extra@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.0.tgz#8cc3f47ce07ef7b3593a11b9fb245f7e34c041d6" + integrity sha512-EglNDLRpmaTWiD/qraZn6HREAEAHJcJOmxNEYwq6xeMKnVMAy3GUcFB+wXt2C6k4CNvB/mP1y/U3dzvKKj5OtQ== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" + integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-minipass@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" + integrity sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ== + dependencies: + minipass "^2.2.1" + +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@1.2.4, fsevents@^1.2.2, fsevents@^1.2.3: + version "1.2.4" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" + integrity sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg== + dependencies: + nan "^2.9.2" + node-pre-gyp "^0.10.0" + +fstream@^1.0.0, fstream@^1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + integrity sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE= + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.1.0, function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gaze@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" + integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g== + dependencies: + globule "^1.0.0" + +get-caller-file@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + +get-own-enumerable-property-symbols@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-2.0.1.tgz#5c4ad87f2834c4b9b4e84549dc1e0650fb38c24b" + integrity sha512-TtY/sbOemiMKPRUDDanGCSgBYe7Mf0vbRsWnBZ+9yghpZ1MvcpSpuZFjHdEeY/LZjZy0vdLjS77L6HosisFiug== + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + integrity sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q= + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + integrity sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg= + dependencies: + is-glob "^2.0.0" + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@1.0.0, global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" + integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" + integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" + +globals@^11.1.0, globals@^11.7.0: + version "11.8.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.8.0.tgz#c1ef45ee9bed6badf0663c5cb90e8d1adec1321d" + integrity sha512-io6LkyPVuzCHBSQV9fmOwxZkUk6nIaGmxheLDgmuFv89j0fm2aqDbIXKAGfzCMHqz3HLF2Zf8WSG6VqMh2qFmA== + +globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + integrity sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0= + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globule@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.1.tgz#5dffb1b191f22d20797a9369b49eab4e9839696d" + integrity sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ== + dependencies: + glob "~7.1.1" + lodash "~4.17.10" + minimatch "~3.0.2" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= + +gzip-size@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.0.0.tgz#a55ecd99222f4c48fd8c01c625ce3b349d0a0e80" + integrity sha512-5iI7omclyqrnWw4XbXAmGhPsABkSIDQonv2K0h61lybgofWa6iZyvrI3r2zsJH4P8Nb64fFVzlvfhs0g7BBxAA== + dependencies: + duplexer "^0.1.1" + pify "^3.0.0" + +h2x-core@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/h2x-core/-/h2x-core-1.1.1.tgz#7fb31ab28e30ebf11818e3c7d183487ecf489f9f" + integrity sha512-LdXe4Irs731knLtHgLyFrnJCumfiqXXQwKN1IMUhi37li29PLfLbMDvfK7Rk4wmgHLKP+sIITT1mcJV4QsC3nw== + dependencies: + h2x-generate "^1.1.0" + h2x-parse "^1.1.1" + h2x-traverse "^1.1.0" + +h2x-generate@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/h2x-generate/-/h2x-generate-1.1.0.tgz#c2c98c60070e1eed231e482d5826c3c5dab2a9ba" + integrity sha512-L7Hym0yb20QIjvqeULUPOeh/cyvScdOAyJ6oRlh5dF0+w92hf3OiTk1q15KBijde7jGEe+0R4aOmtW8gkPNIzg== + dependencies: + h2x-traverse "^1.1.0" + +h2x-parse@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/h2x-parse/-/h2x-parse-1.1.1.tgz#875712cd3be75cf736c610d279b8653b24f58385" + integrity sha512-WRSmPF+tIWuUXVEZaYRhcZx/JGEJx8LjZpDDtrvMr5m/GTR0NerydCik5dRzcKXPWCtfXxuJRLR4v2P4HB2B1A== + dependencies: + h2x-types "^1.1.0" + jsdom ">=11.0.0" + +h2x-plugin-jsx@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/h2x-plugin-jsx/-/h2x-plugin-jsx-1.2.0.tgz#211fa02e5c4e0a07307b0005629923910e631c01" + integrity sha512-a7Vb3BHhJJq0dPDNdqguEyQirENkVsFtvM2YkiaT5h/fmGhmM1nDy3BLeJeSKi2tL2g9v4ykm2Z+GG9QrhDgPA== + dependencies: + h2x-types "^1.1.0" + +h2x-traverse@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/h2x-traverse/-/h2x-traverse-1.1.0.tgz#194b36c593f4e20a754dee47fa6b2288647b2271" + integrity sha512-1ND8ZbISLSUgpLHYJRvhvElITvs0g44L7RxjeXViz5XP6rooa+FtXTFLByl2Yg01zj2txubifHIuU4pgvj8l+A== + dependencies: + h2x-types "^1.1.0" + +h2x-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/h2x-types/-/h2x-types-1.1.0.tgz#ec0d5e3674e2207269f32976ac9c82aaff4818e6" + integrity sha512-QdH5qfLcdF209UsCdM0ZNZ9Dwm2PHvMfeLZtivBrjX3Y/df4US2pwsUC4HBfWhye/mx/t6puODeC7Oacb/Ol8g== + +handle-thing@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" + integrity sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ= + +handlebars@^4.0.3: + version "4.0.12" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.12.tgz#2c15c8a96d46da5e266700518ba8cb8d919d5bc5" + integrity sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA== + dependencies: + async "^2.5.0" + optimist "^0.6.1" + source-map "^0.6.1" + optionalDependencies: + uglify-js "^3.1.4" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29" + integrity sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA== + dependencies: + ajv "^5.3.0" + har-schema "^2.0.0" + +harmony-reflect@^1.4.6: + version "1.6.1" + resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.1.tgz#c108d4f2bb451efef7a37861fdbdae72c9bdefa9" + integrity sha512-WJTeyp0JzGtHcuMsi7rw2VwtkvLa+JyfEKJCFyfcS0+CDkjQ5lHPu7zEhFZP+PDSRrEgXa5Ah0l1MbgbE41XjA== + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" + integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.0, has@^1.0.1, has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg= + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.5" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.5.tgz#e38ab4b85dfb1e0c40fe9265c0e9b54854c23812" + integrity sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +he@1.1.x: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= + +hex-color-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" + integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoek@4.x.x: + version "4.2.1" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" + integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA== + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg= + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +homedir-polyfill@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" + integrity sha1-TCu8inWJmP7r9e1oWA921GdotLw= + dependencies: + parse-passwd "^1.0.0" + +hoopy@^0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" + integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== + +hosted-git-info@^2.1.4: + version "2.7.1" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" + integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w== + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +hsl-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" + integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4= + +hsla-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" + integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= + +html-comment-regex@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" + integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== + +html-encoding-sniffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" + integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== + dependencies: + whatwg-encoding "^1.0.1" + +html-entities@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" + integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= + +html-minifier@^3.2.3: + version "3.5.20" + resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.20.tgz#7b19fd3caa0cb79f7cde5ee5c3abdf8ecaa6bb14" + integrity sha512-ZmgNLaTp54+HFKkONyLFEfs5dd/ZOtlquKaTnqIWFmx3Av5zG6ZPcV2d0o9XM2fXOTxxIf6eDcwzFFotke/5zA== + dependencies: + camel-case "3.0.x" + clean-css "4.2.x" + commander "2.17.x" + he "1.1.x" + param-case "2.1.x" + relateurl "0.2.x" + uglify-js "3.4.x" + +html-webpack-plugin@4.0.0-alpha.2: + version "4.0.0-alpha.2" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.0.0-alpha.2.tgz#7745967e389a57a098e26963f328ebe4c19b598d" + integrity sha512-tyvhjVpuGqD7QYHi1l1drMQTg5i+qRxpQEGbdnYFREgOKy7aFDf/ocQ/V1fuEDlQx7jV2zMap3Hj2nE9i5eGXw== + dependencies: + "@types/tapable" "1.0.2" + html-minifier "^3.2.3" + loader-utils "^1.1.0" + lodash "^4.17.10" + pretty-error "^2.0.2" + tapable "^1.0.0" + util.promisify "1.0.0" + +htmlparser2@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.3.0.tgz#cc70d05a59f6542e43f0e685c982e14c924a9efe" + integrity sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4= + dependencies: + domelementtype "1" + domhandler "2.1" + domutils "1.1" + readable-stream "1.0" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= + +http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.4.0: + version "0.4.13" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.13.tgz#3bd6d6fde6e3172c9334c3b33b6c193d80fe1137" + integrity sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc= + +http-proxy-middleware@~0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz#0987e6bb5a5606e5a69168d8f967a87f15dd8aab" + integrity sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q== + dependencies: + http-proxy "^1.16.2" + is-glob "^4.0.0" + lodash "^4.17.5" + micromatch "^3.1.9" + +http-proxy@^1.16.2: + version "1.17.0" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" + integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g== + dependencies: + eventemitter3 "^3.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= + +iconv-lite@0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= + +icss-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962" + integrity sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI= + dependencies: + postcss "^6.0.1" + +identity-obj-proxy@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14" + integrity sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ= + dependencies: + harmony-reflect "^1.4.6" + +ieee754@^1.1.4: + version "1.1.12" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" + integrity sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA== + +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= + +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== + dependencies: + minimatch "^3.0.4" + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +import-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" + integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk= + dependencies: + import-from "^2.1.0" + +import-from@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" + integrity sha1-M1238qev/VOqpHHUuAId7ja387E= + dependencies: + resolve-from "^3.0.0" + +import-local@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" + integrity sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ== + dependencies: + pkg-dir "^2.0.0" + resolve-cwd "^2.0.0" + +import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +in-publish@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" + integrity sha1-4g/146KvwmkDILbcVSaCqcf631E= + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= + dependencies: + repeating "^2.0.0" + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= + +ini@^1.3.4, ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +inquirer@6.2.0, inquirer@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.0.tgz#51adcd776f661369dc1e894859c2560a224abdd8" + integrity sha512-QIEQG4YyQ2UYZGDC4srMZ7BjHOmNk1lR2JQj5UknBapklm6WHA+VVH7N+sUdX3A7NeCfGF8o4X1S3Ao7nAcIeg== + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^3.0.0" + figures "^2.0.0" + lodash "^4.17.10" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^6.1.0" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + +internal-ip@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-3.0.1.tgz#df5c99876e1d2eb2ea2d74f520e3f669a00ece27" + integrity sha512-NXXgESC2nNVtU+pqmC9e6R8B1GpKxzsAQhffvh5AL79qKnodd+L7tnEQmTiUAVngqLalPbSqRA7XGIEL5nCd0Q== + dependencies: + default-gateway "^2.6.0" + ipaddr.js "^1.5.2" + +invariant@^2.2.0, invariant@^2.2.2, invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= + +invert-kv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" + integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== + +ip-regex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" + integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= + +ip@^1.1.0, ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + +ipaddr.js@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" + integrity sha1-6qM9bd16zo9/b+DJygRA5wZzix4= + +ipaddr.js@^1.5.2: + version "1.8.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.1.tgz#fa4b79fa47fd3def5e3b159825161c0a519c9427" + integrity sha1-+kt5+kf9Pe9eOxWYJRYcClGclCc= + +is-absolute-url@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.0.2, is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74= + dependencies: + builtin-modules "^1.0.0" + +is-callable@^1.1.3, is-callable@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" + integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== + +is-ci@^1.0.10: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" + integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== + dependencies: + ci-info "^1.5.0" + +is-color-stop@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" + integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U= + dependencies: + css-color-names "^0.0.4" + hex-color-regex "^1.1.0" + hsl-regex "^1.0.0" + hsla-regex "^1.0.0" + rgb-regex "^1.0.1" + rgba-regex "^1.0.0" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= + +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + integrity sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE= + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + integrity sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ= + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + integrity sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA= + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-generator-fn@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-1.0.0.tgz#969d49e1bb3329f6bb7f09089be26578b2ddd46a" + integrity sha1-lp1J4bszKfa7fwkIm+JleLLd1Go= + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + integrity sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM= + dependencies: + is-extglob "^1.0.0" + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" + integrity sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A= + dependencies: + is-extglob "^2.1.1" + +is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8= + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-number@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" + integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== + +is-obj@^1.0.0, is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0= + +is-path-in-cwd@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" + integrity sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ== + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= + dependencies: + path-is-inside "^1.0.1" + +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + integrity sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q= + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU= + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= + +is-regex@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= + dependencies: + has "^1.0.1" + +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + +is-resolvable@^1.0.0, is-resolvable@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" + integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== + +is-root@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.0.0.tgz#838d1e82318144e5a6f77819d90207645acc7019" + integrity sha512-F/pJIk8QD6OX5DNhRB7hWamLsUilmkDGho48KbgZ6xg/lmAZXHxzXQ91jzB3yRSw5kdQGGGc4yz8HYhTYIMWPg== + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-svg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" + integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ== + dependencies: + html-comment-regex "^1.1.0" + +is-symbol@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" + integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== + dependencies: + has-symbols "^1.0.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= + +is-windows@^1.0.1, is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isemail@3.x.x: + version "3.1.3" + resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.1.3.tgz#64f37fc113579ea12523165c3ebe3a71a56ce571" + integrity sha512-5xbsG5wYADIcB+mfLsd+nst1V/D+I7EU7LEZPo2GOIMu4JzfcRs5yQoypP4avA7QtUqgxYLKBYNv4IdzBmbhdw== + dependencies: + punycode "2.x.x" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +istanbul-api@^1.3.1: + version "1.3.7" + resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.3.7.tgz#a86c770d2b03e11e3f778cd7aedd82d2722092aa" + integrity sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA== + dependencies: + async "^2.1.4" + fileset "^2.0.2" + istanbul-lib-coverage "^1.2.1" + istanbul-lib-hook "^1.2.2" + istanbul-lib-instrument "^1.10.2" + istanbul-lib-report "^1.1.5" + istanbul-lib-source-maps "^1.2.6" + istanbul-reports "^1.5.1" + js-yaml "^3.7.0" + mkdirp "^0.5.1" + once "^1.4.0" + +istanbul-lib-coverage@^1.2.0, istanbul-lib-coverage@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz#ccf7edcd0a0bb9b8f729feeb0930470f9af664f0" + integrity sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ== + +istanbul-lib-hook@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz#bc6bf07f12a641fbf1c85391d0daa8f0aea6bf86" + integrity sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw== + dependencies: + append-transform "^0.4.0" + +istanbul-lib-instrument@^1.10.1, istanbul-lib-instrument@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz#1f55ed10ac3c47f2bdddd5307935126754d0a9ca" + integrity sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A== + dependencies: + babel-generator "^6.18.0" + babel-template "^6.16.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + babylon "^6.18.0" + istanbul-lib-coverage "^1.2.1" + semver "^5.3.0" + +istanbul-lib-report@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz#f2a657fc6282f96170aaf281eb30a458f7f4170c" + integrity sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw== + dependencies: + istanbul-lib-coverage "^1.2.1" + mkdirp "^0.5.1" + path-parse "^1.0.5" + supports-color "^3.1.2" + +istanbul-lib-source-maps@^1.2.4, istanbul-lib-source-maps@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz#37b9ff661580f8fca11232752ee42e08c6675d8f" + integrity sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg== + dependencies: + debug "^3.1.0" + istanbul-lib-coverage "^1.2.1" + mkdirp "^0.5.1" + rimraf "^2.6.1" + source-map "^0.5.3" + +istanbul-reports@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.5.1.tgz#97e4dbf3b515e8c484caea15d6524eebd3ff4e1a" + integrity sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw== + dependencies: + handlebars "^4.0.3" + +jest-changed-files@^23.4.2: + version "23.4.2" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-23.4.2.tgz#1eed688370cd5eebafe4ae93d34bb3b64968fe83" + integrity sha512-EyNhTAUWEfwnK0Is/09LxoqNDOn7mU7S3EHskG52djOFS/z+IT0jT3h3Ql61+dklcG7bJJitIWEMB4Sp1piHmA== + dependencies: + throat "^4.0.0" + +jest-cli@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-23.6.0.tgz#61ab917744338f443ef2baa282ddffdd658a5da4" + integrity sha512-hgeD1zRUp1E1zsiyOXjEn4LzRLWdJBV//ukAHGlx6s5mfCNJTbhbHjgxnDUXA8fsKWN/HqFFF6X5XcCwC/IvYQ== + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.2" + graceful-fs "^4.1.11" + import-local "^1.0.0" + is-ci "^1.0.10" + istanbul-api "^1.3.1" + istanbul-lib-coverage "^1.2.0" + istanbul-lib-instrument "^1.10.1" + istanbul-lib-source-maps "^1.2.4" + jest-changed-files "^23.4.2" + jest-config "^23.6.0" + jest-environment-jsdom "^23.4.0" + jest-get-type "^22.1.0" + jest-haste-map "^23.6.0" + jest-message-util "^23.4.0" + jest-regex-util "^23.3.0" + jest-resolve-dependencies "^23.6.0" + jest-runner "^23.6.0" + jest-runtime "^23.6.0" + jest-snapshot "^23.6.0" + jest-util "^23.4.0" + jest-validate "^23.6.0" + jest-watcher "^23.4.0" + jest-worker "^23.2.0" + micromatch "^2.3.11" + node-notifier "^5.2.1" + prompts "^0.1.9" + realpath-native "^1.0.0" + rimraf "^2.5.4" + slash "^1.0.0" + string-length "^2.0.0" + strip-ansi "^4.0.0" + which "^1.2.12" + yargs "^11.0.0" + +jest-config@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-23.6.0.tgz#f82546a90ade2d8c7026fbf6ac5207fc22f8eb1d" + integrity sha512-i8V7z9BeDXab1+VNo78WM0AtWpBRXJLnkT+lyT+Slx/cbP5sZJ0+NDuLcmBE5hXAoK0aUp7vI+MOxR+R4d8SRQ== + dependencies: + babel-core "^6.0.0" + babel-jest "^23.6.0" + chalk "^2.0.1" + glob "^7.1.1" + jest-environment-jsdom "^23.4.0" + jest-environment-node "^23.4.0" + jest-get-type "^22.1.0" + jest-jasmine2 "^23.6.0" + jest-regex-util "^23.3.0" + jest-resolve "^23.6.0" + jest-util "^23.4.0" + jest-validate "^23.6.0" + micromatch "^2.3.11" + pretty-format "^23.6.0" + +jest-diff@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-23.6.0.tgz#1500f3f16e850bb3d71233408089be099f610c7d" + integrity sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g== + dependencies: + chalk "^2.0.1" + diff "^3.2.0" + jest-get-type "^22.1.0" + pretty-format "^23.6.0" + +jest-docblock@^23.2.0: + version "23.2.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-23.2.0.tgz#f085e1f18548d99fdd69b20207e6fd55d91383a7" + integrity sha1-8IXh8YVI2Z/dabICB+b9VdkTg6c= + dependencies: + detect-newline "^2.1.0" + +jest-each@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-23.6.0.tgz#ba0c3a82a8054387016139c733a05242d3d71575" + integrity sha512-x7V6M/WGJo6/kLoissORuvLIeAoyo2YqLOoCDkohgJ4XOXSqOtyvr8FbInlAWS77ojBsZrafbozWoKVRdtxFCg== + dependencies: + chalk "^2.0.1" + pretty-format "^23.6.0" + +jest-environment-jsdom@^23.4.0: + version "23.4.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-23.4.0.tgz#056a7952b3fea513ac62a140a2c368c79d9e6023" + integrity sha1-BWp5UrP+pROsYqFAosNox52eYCM= + dependencies: + jest-mock "^23.2.0" + jest-util "^23.4.0" + jsdom "^11.5.1" + +jest-environment-node@^23.4.0: + version "23.4.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-23.4.0.tgz#57e80ed0841dea303167cce8cd79521debafde10" + integrity sha1-V+gO0IQd6jAxZ8zozXlSHeuv3hA= + dependencies: + jest-mock "^23.2.0" + jest-util "^23.4.0" + +jest-get-type@^22.1.0: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4" + integrity sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w== + +jest-haste-map@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-23.6.0.tgz#2e3eb997814ca696d62afdb3f2529f5bbc935e16" + integrity sha512-uyNhMyl6dr6HaXGHp8VF7cK6KpC6G9z9LiMNsst+rJIZ8l7wY0tk8qwjPmEghczojZ2/ZhtEdIabZ0OQRJSGGg== + dependencies: + fb-watchman "^2.0.0" + graceful-fs "^4.1.11" + invariant "^2.2.4" + jest-docblock "^23.2.0" + jest-serializer "^23.0.1" + jest-worker "^23.2.0" + micromatch "^2.3.11" + sane "^2.0.0" + +jest-jasmine2@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-23.6.0.tgz#840e937f848a6c8638df24360ab869cc718592e0" + integrity sha512-pe2Ytgs1nyCs8IvsEJRiRTPC0eVYd8L/dXJGU08GFuBwZ4sYH/lmFDdOL3ZmvJR8QKqV9MFuwlsAi/EWkFUbsQ== + dependencies: + babel-traverse "^6.0.0" + chalk "^2.0.1" + co "^4.6.0" + expect "^23.6.0" + is-generator-fn "^1.0.0" + jest-diff "^23.6.0" + jest-each "^23.6.0" + jest-matcher-utils "^23.6.0" + jest-message-util "^23.4.0" + jest-snapshot "^23.6.0" + jest-util "^23.4.0" + pretty-format "^23.6.0" + +jest-leak-detector@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-23.6.0.tgz#e4230fd42cf381a1a1971237ad56897de7e171de" + integrity sha512-f/8zA04rsl1Nzj10HIyEsXvYlMpMPcy0QkQilVZDFOaPbv2ur71X5u2+C4ZQJGyV/xvVXtCCZ3wQ99IgQxftCg== + dependencies: + pretty-format "^23.6.0" + +jest-matcher-utils@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz#726bcea0c5294261a7417afb6da3186b4b8cac80" + integrity sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog== + dependencies: + chalk "^2.0.1" + jest-get-type "^22.1.0" + pretty-format "^23.6.0" + +jest-message-util@^23.4.0: + version "23.4.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-23.4.0.tgz#17610c50942349508d01a3d1e0bda2c079086a9f" + integrity sha1-F2EMUJQjSVCNAaPR4L2iwHkIap8= + dependencies: + "@babel/code-frame" "^7.0.0-beta.35" + chalk "^2.0.1" + micromatch "^2.3.11" + slash "^1.0.0" + stack-utils "^1.0.1" + +jest-mock@^23.2.0: + version "23.2.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-23.2.0.tgz#ad1c60f29e8719d47c26e1138098b6d18b261134" + integrity sha1-rRxg8p6HGdR8JuETgJi20YsmETQ= + +jest-pnp-resolver@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.0.1.tgz#f397cd71dbcd4a1947b2e435f6da8e9a347308fa" + integrity sha512-kzhvJQp+9k0a/hpvIIzOJgOwfOqmnohdrAMZW2EscH3kxR2VWD7EcPa10cio8EK9V7PcD75bhG1pFnO70zGwSQ== + +jest-regex-util@^23.3.0: + version "23.3.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-23.3.0.tgz#5f86729547c2785c4002ceaa8f849fe8ca471bc5" + integrity sha1-X4ZylUfCeFxAAs6qj4Sf6MpHG8U= + +jest-resolve-dependencies@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-23.6.0.tgz#b4526af24c8540d9a3fab102c15081cf509b723d" + integrity sha512-EkQWkFWjGKwRtRyIwRwI6rtPAEyPWlUC2MpzHissYnzJeHcyCn1Hc8j7Nn1xUVrS5C6W5+ZL37XTem4D4pLZdA== + dependencies: + jest-regex-util "^23.3.0" + jest-snapshot "^23.6.0" + +jest-resolve@23.6.0, jest-resolve@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-23.6.0.tgz#cf1d1a24ce7ee7b23d661c33ba2150f3aebfa0ae" + integrity sha512-XyoRxNtO7YGpQDmtQCmZjum1MljDqUCob7XlZ6jy9gsMugHdN2hY4+Acz9Qvjz2mSsOnPSH7skBmDYCHXVZqkA== + dependencies: + browser-resolve "^1.11.3" + chalk "^2.0.1" + realpath-native "^1.0.0" + +jest-runner@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-23.6.0.tgz#3894bd219ffc3f3cb94dc48a4170a2e6f23a5a38" + integrity sha512-kw0+uj710dzSJKU6ygri851CObtCD9cN8aNkg8jWJf4ewFyEa6kwmiH/r/M1Ec5IL/6VFa0wnAk6w+gzUtjJzA== + dependencies: + exit "^0.1.2" + graceful-fs "^4.1.11" + jest-config "^23.6.0" + jest-docblock "^23.2.0" + jest-haste-map "^23.6.0" + jest-jasmine2 "^23.6.0" + jest-leak-detector "^23.6.0" + jest-message-util "^23.4.0" + jest-runtime "^23.6.0" + jest-util "^23.4.0" + jest-worker "^23.2.0" + source-map-support "^0.5.6" + throat "^4.0.0" + +jest-runtime@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-23.6.0.tgz#059e58c8ab445917cd0e0d84ac2ba68de8f23082" + integrity sha512-ycnLTNPT2Gv+TRhnAYAQ0B3SryEXhhRj1kA6hBPSeZaNQkJ7GbZsxOLUkwg6YmvWGdX3BB3PYKFLDQCAE1zNOw== + dependencies: + babel-core "^6.0.0" + babel-plugin-istanbul "^4.1.6" + chalk "^2.0.1" + convert-source-map "^1.4.0" + exit "^0.1.2" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.1.11" + jest-config "^23.6.0" + jest-haste-map "^23.6.0" + jest-message-util "^23.4.0" + jest-regex-util "^23.3.0" + jest-resolve "^23.6.0" + jest-snapshot "^23.6.0" + jest-util "^23.4.0" + jest-validate "^23.6.0" + micromatch "^2.3.11" + realpath-native "^1.0.0" + slash "^1.0.0" + strip-bom "3.0.0" + write-file-atomic "^2.1.0" + yargs "^11.0.0" + +jest-serializer@^23.0.1: + version "23.0.1" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-23.0.1.tgz#a3776aeb311e90fe83fab9e533e85102bd164165" + integrity sha1-o3dq6zEekP6D+rnlM+hRAr0WQWU= + +jest-snapshot@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-23.6.0.tgz#f9c2625d1b18acda01ec2d2b826c0ce58a5aa17a" + integrity sha512-tM7/Bprftun6Cvj2Awh/ikS7zV3pVwjRYU2qNYS51VZHgaAMBs5l4o/69AiDHhQrj5+LA2Lq4VIvK7zYk/bswg== + dependencies: + babel-types "^6.0.0" + chalk "^2.0.1" + jest-diff "^23.6.0" + jest-matcher-utils "^23.6.0" + jest-message-util "^23.4.0" + jest-resolve "^23.6.0" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + pretty-format "^23.6.0" + semver "^5.5.0" + +jest-util@^23.4.0: + version "23.4.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-23.4.0.tgz#4d063cb927baf0a23831ff61bec2cbbf49793561" + integrity sha1-TQY8uSe68KI4Mf9hvsLLv0l5NWE= + dependencies: + callsites "^2.0.0" + chalk "^2.0.1" + graceful-fs "^4.1.11" + is-ci "^1.0.10" + jest-message-util "^23.4.0" + mkdirp "^0.5.1" + slash "^1.0.0" + source-map "^0.6.0" + +jest-validate@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-23.6.0.tgz#36761f99d1ed33fcd425b4e4c5595d62b6597474" + integrity sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A== + dependencies: + chalk "^2.0.1" + jest-get-type "^22.1.0" + leven "^2.1.0" + pretty-format "^23.6.0" + +jest-watcher@^23.4.0: + version "23.4.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-23.4.0.tgz#d2e28ce74f8dad6c6afc922b92cabef6ed05c91c" + integrity sha1-0uKM50+NrWxq/JIrksq+9u0FyRw= + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.1" + string-length "^2.0.0" + +jest-worker@^23.2.0: + version "23.2.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-23.2.0.tgz#faf706a8da36fae60eb26957257fa7b5d8ea02b9" + integrity sha1-+vcGqNo2+uYOsmlXJX+ntdjqArk= + dependencies: + merge-stream "^1.0.1" + +jest@23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-23.6.0.tgz#ad5835e923ebf6e19e7a1d7529a432edfee7813d" + integrity sha512-lWzcd+HSiqeuxyhG+EnZds6iO3Y3ZEnMrfZq/OTGvF/C+Z4fPMCdhWTGSAiO2Oym9rbEXfwddHhh6jqrTF3+Lw== + dependencies: + import-local "^1.0.0" + jest-cli "^23.6.0" + +joi@^11.1.1: + version "11.4.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-11.4.0.tgz#f674897537b625e9ac3d0b7e1604c828ad913ccb" + integrity sha512-O7Uw+w/zEWgbL6OcHbyACKSj0PkQeUgmehdoXVSxt92QFCq4+1390Rwh5moI2K/OgC7D8RHRZqHZxT2husMJHA== + dependencies: + hoek "4.x.x" + isemail "3.x.x" + topo "2.x.x" + +js-base64@^2.1.8: + version "2.4.9" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.9.tgz#748911fb04f48a60c4771b375cac45a80df11c03" + integrity sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ== + +js-levenshtein@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.4.tgz#3a56e3cbf589ca0081eb22cd9ba0b1290a16d26e" + integrity sha512-PxfGzSs0ztShKrUYPIn5r0MtyAhYcCwmndozzpz8YObbPnD1jFxzlBGbRnX2mIu6Z13xN6+PTu05TQFnZFlzow== + +js-tokens@^3.0.0, js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" + integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +jsdom@>=11.0.0: + version "12.2.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-12.2.0.tgz#7cf3f5b5eafd47f8f09ca52315d367ff6e95de23" + integrity sha512-QPOggIJ8fquWPLaYYMoh+zqUmdphDtu1ju0QGTitZT1Yd8I5qenPpXM1etzUegu3MjVp8XPzgZxdn8Yj7e40ig== + dependencies: + abab "^2.0.0" + acorn "^6.0.2" + acorn-globals "^4.3.0" + array-equal "^1.0.0" + cssom "^0.3.4" + cssstyle "^1.1.1" + data-urls "^1.0.1" + domexception "^1.0.1" + escodegen "^1.11.0" + html-encoding-sniffer "^1.0.2" + nwsapi "^2.0.9" + parse5 "5.1.0" + pn "^1.1.0" + request "^2.88.0" + request-promise-native "^1.0.5" + saxes "^3.1.3" + symbol-tree "^3.2.2" + tough-cookie "^2.4.3" + w3c-hr-time "^1.0.1" + webidl-conversions "^4.0.2" + whatwg-encoding "^1.0.5" + whatwg-mimetype "^2.2.0" + whatwg-url "^7.0.0" + ws "^6.1.0" + xml-name-validator "^3.0.0" + +jsdom@^11.5.1: + version "11.12.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" + integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== + dependencies: + abab "^2.0.0" + acorn "^5.5.3" + acorn-globals "^4.1.0" + array-equal "^1.0.0" + cssom ">= 0.3.2 < 0.4.0" + cssstyle "^1.0.0" + data-urls "^1.0.0" + domexception "^1.0.1" + escodegen "^1.9.1" + html-encoding-sniffer "^1.0.2" + left-pad "^1.3.0" + nwsapi "^2.0.7" + parse5 "4.0.0" + pn "^1.1.0" + request "^2.87.0" + request-promise-native "^1.0.5" + sax "^1.2.4" + symbol-tree "^3.2.2" + tough-cookie "^2.3.4" + w3c-hr-time "^1.0.1" + webidl-conversions "^4.0.2" + whatwg-encoding "^1.0.3" + whatwg-mimetype "^2.1.0" + whatwg-url "^6.4.1" + ws "^5.2.0" + xml-name-validator "^3.0.0" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= + +jsesc@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe" + integrity sha1-5CGiqOINawgZ3yiQj3glJrlt0f4= + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= + +json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8= + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json3@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" + integrity sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE= + +json5@^0.5.0, json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jsx-ast-utils@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f" + integrity sha1-6AGxs5mF4g//yHtA43SAgOLcrH8= + dependencies: + array-includes "^3.0.3" + +killable@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" + integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== + +kind-of@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" + integrity sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU= + dependencies: + is-buffer "^1.0.2" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== + +kleur@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300" + integrity sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ== + +last-call-webpack-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555" + integrity sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w== + dependencies: + lodash "^4.17.5" + webpack-sources "^1.1.0" + +lazy-cache@^0.2.3: + version "0.2.7" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65" + integrity sha1-f+3fLctu23fRHvHRF6tf/fCrG2U= + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= + dependencies: + invert-kv "^1.0.0" + +lcid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" + integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== + dependencies: + invert-kv "^2.0.0" + +left-pad@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" + integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== + +leven@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" + integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA= + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +loader-fs-cache@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/loader-fs-cache/-/loader-fs-cache-1.0.1.tgz#56e0bf08bd9708b26a765b68509840c8dec9fdbc" + integrity sha1-VuC/CL2XCLJqdltoUJhAyN7J/bw= + dependencies: + find-cache-dir "^0.1.1" + mkdirp "0.5.1" + +loader-runner@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.1.tgz#026f12fe7c3115992896ac02ba022ba92971b979" + integrity sha512-By6ZFY7ETWOc9RFaAIb23IjJVcM4dvJC/N57nmdz9RSkMXvAXGI7SyVlAw3v8vjtDRlqThgVDVmTnr9fqMlxkw== + +loader-utils@1.1.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0= + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +lodash._reinterpolate@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= + +lodash.assign@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= + +lodash.clonedeep@^4.3.2: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + +lodash.mergewith@^4.6.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" + integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ== + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + +lodash.tail@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" + integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ= + +lodash.template@^4.2.4, lodash.template@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" + integrity sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A= + dependencies: + lodash._reinterpolate "~3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316" + integrity sha1-K01OlbpEDZFf8IvImeRVNmZxMxY= + dependencies: + lodash._reinterpolate "~3.0.0" + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + +"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@~4.17.10: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + +loglevel@^1.4.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" + integrity sha1-4PyVEztu8nbNyIh82vJKpvFW+Po= + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lower-case@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" + integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= + +lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" + integrity sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= + dependencies: + tmpl "1.0.x" + +mamacro@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4" + integrity sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA== + +map-age-cleaner@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.2.tgz#098fb15538fd3dbe461f12745b0ca8568d4e3f74" + integrity sha512-UN1dNocxQq44IhJyMI4TU8phc2m9BddacHRPRjKGLYaF0jqd3xLz0jS0skpAU9WgYyoR4gHtUpzytNBS385FWQ== + dependencies: + p-defer "^1.0.0" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +math-random@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac" + integrity sha1-izqsWIuKZuSXXjzepn97sylgH6w= + +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +mdn-data@~1.1.0: + version "1.1.4" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01" + integrity sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + integrity sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y= + dependencies: + mimic-fn "^1.0.0" + +mem@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.0.0.tgz#6437690d9471678f6cc83659c00cbafcd6b0cdaf" + integrity sha512-WQxG/5xYc3tMbYLXoXPm81ET2WDULiU5FxbuIoNbJqLOOI8zehXFdZuiUEgfdrU2mVB1pxBZUGlYORSrpuJreA== + dependencies: + map-age-cleaner "^0.1.1" + mimic-fn "^1.0.0" + p-is-promise "^1.1.0" + +memory-fs@^0.4.0, memory-fs@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +meow@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge-deep@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/merge-deep/-/merge-deep-3.0.2.tgz#f39fa100a4f1bd34ff29f7d2bf4508fbb8d83ad2" + integrity sha512-T7qC8kg4Zoti1cFd8Cr0M+qaZfOwjlPDEdZIIPPB2JZctjaPM4fX+i7HOId69tAti2fvO6X5ldfYUONDODsrkA== + dependencies: + arr-union "^3.1.0" + clone-deep "^0.2.4" + kind-of "^3.0.2" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +merge-stream@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1" + integrity sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE= + dependencies: + readable-stream "^2.0.1" + +merge@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" + integrity sha1-dTHjnUlJwoGma4xabgJl6LBYlNo= + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +micromatch@^2.3.11: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + integrity sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU= + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8, micromatch@^3.1.9: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +"mime-db@>= 1.36.0 < 2", mime-db@~1.36.0: + version "1.36.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" + integrity sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw== + +mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.19: + version "2.1.20" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" + integrity sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A== + dependencies: + mime-db "~1.36.0" + +mime@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== + +mime@^2.0.3, mime@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369" + integrity sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg== + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== + +mini-css-extract-plugin@0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.4.3.tgz#98d60fcc5d228c3e36a9bd15a1d6816d6580beb8" + integrity sha512-Mxs0nxzF1kxPv4TRi2NimewgXlJqh0rGE30vviCU2WHrpbta6wklnUV9dr9FUtoAHmB3p3LeXEC+ZjgHvB0Dzg== + dependencies: + loader-utils "^1.1.0" + schema-utils "^1.0.0" + webpack-sources "^1.1.0" + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= + +minimatch@3.0.4, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= + +minipass@^2.2.1, minipass@^2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.4.tgz#4768d7605ed6194d6d576169b9e12ef71e9d9957" + integrity sha512-mlouk1OHlaUE8Odt1drMtG1bAJA4ZA6B/ehysgV0LUIrDHdKgo1KorZq3pK0b/7Z7LJIQ12MNM6aC+Tn6lUZ5w== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.1.tgz#6734acc045a46e61d596a43bb9d9cd326e19cc42" + integrity sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg== + dependencies: + minipass "^2.2.1" + +mississippi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f" + integrity sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw== + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^2.0.1" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +mississippi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" + integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^3.0.0" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +mixin-deep@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" + integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mixin-object@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" + integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4= + dependencies: + for-in "^0.1.3" + is-extendable "^0.1.1" + +mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I= + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= + +multicast-dns@^6.0.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" + integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== + dependencies: + dns-packet "^1.3.1" + thunky "^1.0.2" + +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= + +nan@^2.10.0, nan@^2.9.2: + version "2.11.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766" + integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +needle@^2.2.1: + version "2.2.4" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e" + integrity sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA== + dependencies: + debug "^2.1.2" + iconv-lite "^0.4.4" + sax "^1.2.4" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk= + +neo-async@^2.5.0: + version "2.5.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.2.tgz#489105ce7bc54e709d736b195f82135048c50fcc" + integrity sha512-vdqTKI9GBIYcAEbFAcpKPErKINfPF5zIuz3/niBfq8WUZjpT2tytLlFVrBgWdOtqI4uaA/Rb6No0hux39XXDuw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +no-case@^2.2.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" + integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== + dependencies: + lower-case "^1.1.1" + +node-forge@0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df" + integrity sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ== + +node-gyp@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" + integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== + dependencies: + fstream "^1.0.0" + glob "^7.0.3" + graceful-fs "^4.1.2" + mkdirp "^0.5.0" + nopt "2 || 3" + npmlog "0 || 1 || 2 || 3 || 4" + osenv "0" + request "^2.87.0" + rimraf "2" + semver "~5.3.0" + tar "^2.0.0" + which "1" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + +node-libs-browser@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df" + integrity sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg== + dependencies: + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^1.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.0" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.10.3" + vm-browserify "0.0.4" + +node-notifier@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.2.1.tgz#fa313dd08f5517db0e2502e5758d664ac69f9dea" + integrity sha512-MIBs+AAd6dJ2SklbbE8RUDRlIVhU8MaNLh1A9SUZDUHPiZkWLFde6UNwG41yQHZEToHgJMXqyVZ9UcS/ReOVTg== + dependencies: + growly "^1.3.0" + semver "^5.4.1" + shellwords "^0.1.1" + which "^1.3.0" + +node-pre-gyp@^0.10.0: + version "0.10.3" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" + integrity sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A== + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + +node-releases@^1.0.0-alpha.11, node-releases@^1.0.0-alpha.14: + version "1.0.0-alpha.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.0.0-alpha.14.tgz#da9e2780add4bbb59ad890af9e2018a1d9c0034b" + integrity sha512-G8nnF9cP9QPP/jUmYWw/uUUhumHmkm+X/EarCugYFjYm2uXRMFeOD6CVT3RLdoyCvDUNy51nirGfUItKWs/S1g== + dependencies: + semver "^5.3.0" + +node-sass@^4.9.4: + version "4.9.4" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.9.4.tgz#349bd7f1c89422ffe7e1e4b60f2055a69fbc5512" + integrity sha512-MXyurANsUoE4/6KmfMkwGcBzAnJQ5xJBGW7Ei6ea8KnUKuzHr/SguVBIi3uaUAHtZCPUYkvlJ3Ef5T5VAwVpaA== + dependencies: + async-foreach "^0.1.3" + chalk "^1.1.1" + cross-spawn "^3.0.0" + gaze "^1.0.0" + get-stdin "^4.0.1" + glob "^7.0.3" + in-publish "^2.0.0" + lodash.assign "^4.2.0" + lodash.clonedeep "^4.3.2" + lodash.mergewith "^4.6.0" + meow "^3.7.0" + mkdirp "^0.5.1" + nan "^2.10.0" + node-gyp "^3.8.0" + npmlog "^4.0.0" + request "^2.88.0" + sass-graph "^2.2.4" + stdout-stream "^1.4.0" + "true-case-path" "^1.0.2" + +"nopt@2 || 3": + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= + dependencies: + abbrev "1" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + integrity sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw== + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.0.1, normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= + +normalize-url@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" + integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== + +npm-bundled@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979" + integrity sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g== + +npm-packlist@^1.1.6: + version "1.1.12" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.12.tgz#22bde2ebc12e72ca482abd67afc51eb49377243a" + integrity sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g== + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +nth-check@^1.0.1, nth-check@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.1.tgz#9929acdf628fc2c41098deab82ac580cf149aae4" + integrity sha1-mSms32KPwsQQmN6rgqxYDPFJquQ= + dependencies: + boolbase "~1.0.0" + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +nwsapi@^2.0.7, nwsapi@^2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.9.tgz#77ac0cdfdcad52b6a1151a84e73254edc33ed016" + integrity sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@4.1.1, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-hash@^1.1.4: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.0.tgz#76d9ba6ff113cf8efc0d996102851fe6723963e2" + integrity sha512-05KzQ70lSeGSrZJQXE5wNDiTkBJDlUT/myi6RX9dVIvz7a7Qh4oH93BQdiPMn27nldYvVQCKMUaM83AfizZlsQ== + +object-keys@^1.0.11, object-keys@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" + integrity sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.getownpropertydescriptors@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" + integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.1" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + integrity sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo= + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +object.values@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.0.4.tgz#e524da09b4f66ff05df457546ec72ac99f13069a" + integrity sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.6.1" + function-bind "^1.1.0" + has "^1.0.1" + +obuf@^1.0.0, obuf@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" + integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c= + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= + dependencies: + mimic-fn "^1.0.0" + +opn@5.4.0, opn@^5.1.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.4.0.tgz#cb545e7aab78562beb11aa3bfabc7042e1761035" + integrity sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw== + dependencies: + is-wsl "^1.1.0" + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +optimize-css-assets-webpack-plugin@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.1.tgz#9eb500711d35165b45e7fd60ba2df40cb3eb9159" + integrity sha512-Rqm6sSjWtx9FchdP0uzTQDc7GXDKnwVEGoSxjezPkzMewx7gEWE9IMUYKmigTRC4U3RaNSwYVnUDLuIdtTpm0A== + dependencies: + cssnano "^4.1.0" + last-call-webpack-plugin "^3.0.0" + +optionator@^0.8.1, optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +original@>=0.0.5: + version "1.0.2" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" + integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== + dependencies: + url-parse "^1.4.3" + +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= + dependencies: + lcid "^1.0.0" + +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + integrity sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA== + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + +os-locale@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.0.1.tgz#3b014fbf01d87f60a1e5348d80fe870dc82c4620" + integrity sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw== + dependencies: + execa "^0.10.0" + lcid "^2.0.0" + mem "^4.0.0" + +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +osenv@0, osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-is-promise@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" + integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-limit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.0.0.tgz#e624ed54ee8c460a778b3c9f3670496ff8a57aec" + integrity sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A== + dependencies: + p-try "^2.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= + dependencies: + p-limit "^1.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-map@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" + integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + +p-try@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" + integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== + +pako@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" + integrity sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg== + +parallel-transform@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" + integrity sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY= + dependencies: + cyclist "~0.2.2" + inherits "^2.0.3" + readable-stream "^2.1.5" + +param-case@2.1.x: + version "2.1.1" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" + integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc= + dependencies: + no-case "^2.2.0" + +parse-asn1@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8" + integrity sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw== + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + integrity sha1-ssN2z7EfNVE7rdFz7wu246OIORw= + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= + dependencies: + error-ex "^1.2.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= + +parse5@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" + integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== + +parse5@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2" + integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== + +parseurl@~1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" + integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M= + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +path-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + integrity sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo= + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-is-inside@^1.0.1, path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-parse@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= + dependencies: + pify "^2.0.0" + +pbkdf2@^3.0.3: + version "3.0.17" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" + integrity sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA== + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +pkg-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" + integrity sha1-ektQio1bstYp1EcFb/TpyTFM89Q= + dependencies: + find-up "^1.0.0" + +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= + dependencies: + find-up "^2.1.0" + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +pkg-up@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" + integrity sha1-yBmscoBZpGHKscOImivjxJoATX8= + dependencies: + find-up "^2.1.0" + +pluralize@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" + integrity sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow== + +pn@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" + integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== + +pnp-webpack-plugin@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.1.0.tgz#947a96d1db94bb5a1fc014d83b581e428699ac8c" + integrity sha512-CPCdcFxx7fEcDMWTDjXe2Wypt4JuMt4q5Q2UrpTcyBBkLiCIyPEh/mCGmUWIcNkKGyXwQ9Y2wVhlKm6ketiBNQ== + +portfinder@^1.0.9: + version "1.0.17" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.17.tgz#a8a1691143e46c4735edefcf4fbcccedad26456a" + integrity sha512-syFcRIRzVI1BoEFOCaAiizwDolh1S1YXSodsVhncbhjzjZQulhczNRbqnUl9N31Q4dKGOXsNDqxC2BWBgSMqeQ== + dependencies: + async "^1.5.2" + debug "^2.2.0" + mkdirp "0.5.x" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +postcss-attribute-case-insensitive@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.0.tgz#807b6a797ad8bf1c821b2d51cf641e9dd3837624" + integrity sha512-K/zqdg0/UgUgC8qR0lDuxYzmowPpnvrrNC5YuoqzhHMubR9AuhsPlpVu3jjkLHgDAzR+ohD/m7//iGnN9WxbzQ== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + +postcss-calc@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-6.0.2.tgz#4d9a43e27dbbf27d095fecb021ac6896e2318337" + integrity sha512-fiznXjEN5T42Qm7qqMCVJXS3roaj9r4xsSi+meaBVe7CJBl8t/QLOXu02Z2E6oWAMWIvCuF6JrvzFekmVEbOKA== + dependencies: + css-unit-converter "^1.1.1" + postcss "^7.0.2" + postcss-selector-parser "^2.2.2" + reduce-css-calc "^2.0.0" + +postcss-color-functional-notation@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz#5efd37a88fbabeb00a2966d1e53d98ced93f74e0" + integrity sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-color-hex-alpha@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.2.tgz#e9b1886bb038daed33f6394168c210b40bb4fdb6" + integrity sha512-8bIOzQMGdZVifoBQUJdw+yIY00omBd2EwkJXepQo9cjp1UOHHHoeRDeSzTP6vakEpaRc6GAIOfvcQR7jBYaG5Q== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-color-mod-function@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz#816ba145ac11cc3cb6baa905a75a49f903e4d31d" + integrity sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ== + dependencies: + "@csstools/convert-colors" "^1.4.0" + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-color-rebeccapurple@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz#c7a89be872bb74e45b1e3022bfe5748823e6de77" + integrity sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-colormin@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.2.tgz#93cd1fa11280008696887db1a528048b18e7ed99" + integrity sha512-1QJc2coIehnVFsz0otges8kQLsryi4lo19WD+U5xCWvXd0uw/Z+KKYnbiNDCnO9GP+PvErPHCG0jNvWTngk9Rw== + dependencies: + browserslist "^4.0.0" + color "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-convert-values@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f" + integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-custom-media@^7.0.4: + version "7.0.6" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.6.tgz#c6b00ff7fee6ddc7dfdd946d66ed65e7743db09b" + integrity sha512-Txk5Ve0XWw105N6490xoq1aAHkLWIBGowBxHrLqbfsPfv/nzPvP2txyPMwn6OW87eeTO7H0a7P/BVJ4WMPEgJA== + dependencies: + postcss "^7.0.5" + +postcss-custom-properties@^8.0.5: + version "8.0.8" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-8.0.8.tgz#1812e2553805e1affce93164dd1709ef6b69c53e" + integrity sha512-G3U8uSxj0B4jPJ1QBF5WYeW716n5HV/wcH2lOTV1V+EI+F0T0/ZOhl32MLLTMD79bN2mE77IOoclbCoLl4QtPA== + dependencies: + postcss "^7.0.5" + postcss-values-parser "^2.0.0" + +postcss-custom-selectors@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz#64858c6eb2ecff2fb41d0b28c9dd7b3db4de7fba" + integrity sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + +postcss-dir-pseudo-class@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz#6e3a4177d0edb3abcc85fdb6fbb1c26dabaeaba2" + integrity sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + +postcss-discard-comments@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.1.tgz#30697735b0c476852a7a11050eb84387a67ef55d" + integrity sha512-Ay+rZu1Sz6g8IdzRjUgG2NafSNpp2MSMOQUb+9kkzzzP+kh07fP0yNbhtFejURnyVXSX3FYy2nVNW1QTnNjgBQ== + dependencies: + postcss "^7.0.0" + +postcss-discard-duplicates@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb" + integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ== + dependencies: + postcss "^7.0.0" + +postcss-discard-empty@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765" + integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w== + dependencies: + postcss "^7.0.0" + +postcss-discard-overridden@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57" + integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg== + dependencies: + postcss "^7.0.0" + +postcss-env-function@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-2.0.2.tgz#0f3e3d3c57f094a92c2baf4b6241f0b0da5365d7" + integrity sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-flexbugs-fixes@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.1.0.tgz#e094a9df1783e2200b7b19f875dcad3b3aff8b20" + integrity sha512-jr1LHxQvStNNAHlgco6PzY308zvLklh7SJVYuWUwyUQncofaAlD2l+P/gxKHOdqWKe7xJSkVLFF/2Tp+JqMSZA== + dependencies: + postcss "^7.0.0" + +postcss-focus-visible@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz#477d107113ade6024b14128317ade2bd1e17046e" + integrity sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g== + dependencies: + postcss "^7.0.2" + +postcss-focus-within@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz#763b8788596cee9b874c999201cdde80659ef680" + integrity sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w== + dependencies: + postcss "^7.0.2" + +postcss-font-variant@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-4.0.0.tgz#71dd3c6c10a0d846c5eda07803439617bbbabacc" + integrity sha512-M8BFYKOvCrI2aITzDad7kWuXXTm0YhGdP9Q8HanmN4EF1Hmcgs1KK5rSHylt/lUJe8yLxiSwWAHdScoEiIxztg== + dependencies: + postcss "^7.0.2" + +postcss-gap-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz#431c192ab3ed96a3c3d09f2ff615960f902c1715" + integrity sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg== + dependencies: + postcss "^7.0.2" + +postcss-image-set-function@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz#28920a2f29945bed4c3198d7df6496d410d3f288" + integrity sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-initial@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.0.tgz#1772512faf11421b791fb2ca6879df5f68aa0517" + integrity sha512-WzrqZ5nG9R9fUtrA+we92R4jhVvEB32IIRTzfIG/PLL8UV4CvbF1ugTEHEFX6vWxl41Xt5RTCJPEZkuWzrOM+Q== + dependencies: + lodash.template "^4.2.4" + postcss "^7.0.2" + +postcss-lab-function@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz#bb51a6856cd12289ab4ae20db1e3821ef13d7d2e" + integrity sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg== + dependencies: + "@csstools/convert-colors" "^1.4.0" + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-load-config@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.0.0.tgz#f1312ddbf5912cd747177083c5ef7a19d62ee484" + integrity sha512-V5JBLzw406BB8UIfsAWSK2KSwIJ5yoEIVFb4gVkXci0QdKgA24jLmHZ/ghe/GgX0lJ0/D1uUK1ejhzEY94MChQ== + dependencies: + cosmiconfig "^4.0.0" + import-cwd "^2.0.0" + +postcss-loader@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d" + integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA== + dependencies: + loader-utils "^1.1.0" + postcss "^7.0.0" + postcss-load-config "^2.0.0" + schema-utils "^1.0.0" + +postcss-logical@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-3.0.0.tgz#2495d0f8b82e9f262725f75f9401b34e7b45d5b5" + integrity sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA== + dependencies: + postcss "^7.0.2" + +postcss-media-minmax@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5" + integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw== + dependencies: + postcss "^7.0.2" + +postcss-merge-longhand@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.6.tgz#2b938fa3529c3d1657e53dc7ff0fd604dbc85ff1" + integrity sha512-JavnI+V4IHWsaUAfOoKeMEiJQGXTraEy1nHM0ILlE6NIQPEZrJDAnPh3lNGZ5HAk2mSSrwp66JoGhvjp6SqShA== + dependencies: + css-color-names "0.0.4" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + stylehacks "^4.0.0" + +postcss-merge-rules@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.2.tgz#2be44401bf19856f27f32b8b12c0df5af1b88e74" + integrity sha512-UiuXwCCJtQy9tAIxsnurfF0mrNHKc4NnNx6NxqmzNNjXpQwLSukUxELHTRF0Rg1pAmcoKLih8PwvZbiordchag== + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + cssnano-util-same-parent "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + vendors "^1.0.0" + +postcss-minify-font-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6" + integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-gradients@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.1.tgz#6da95c6e92a809f956bb76bf0c04494953e1a7dd" + integrity sha512-pySEW3E6Ly5mHm18rekbWiAjVi/Wj8KKt2vwSfVFAWdW6wOIekgqxKxLU7vJfb107o3FDNPkaYFCxGAJBFyogA== + dependencies: + cssnano-util-get-arguments "^4.0.0" + is-color-stop "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-params@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.1.tgz#5b2e2d0264dd645ef5d68f8fec0d4c38c1cf93d2" + integrity sha512-h4W0FEMEzBLxpxIVelRtMheskOKKp52ND6rJv+nBS33G1twu2tCyurYj/YtgU76+UDCvWeNs0hs8HFAWE2OUFg== + dependencies: + alphanum-sort "^1.0.0" + browserslist "^4.0.0" + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + uniqs "^2.0.0" + +postcss-minify-selectors@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.1.tgz#a891c197977cc37abf60b3ea06b84248b1c1e9cd" + integrity sha512-8+plQkomve3G+CodLCgbhAKrb5lekAnLYuL1d7Nz+/7RANpBEVdgBkPNwljfSKvZ9xkkZTZITd04KP+zeJTJqg== + dependencies: + alphanum-sort "^1.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +postcss-modules-extract-imports@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz#66140ecece38ef06bf0d3e355d69bf59d141ea85" + integrity sha1-ZhQOzs447wa/DT41XWm/WdFB6oU= + dependencies: + postcss "^6.0.1" + +postcss-modules-local-by-default@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069" + integrity sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk= + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-scope@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90" + integrity sha1-1upkmUx5+XtipytCb75gVqGUu5A= + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-values@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20" + integrity sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA= + dependencies: + icss-replace-symbols "^1.1.0" + postcss "^6.0.1" + +postcss-nesting@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-7.0.0.tgz#6e26a770a0c8fcba33782a6b6f350845e1a448f6" + integrity sha512-WSsbVd5Ampi3Y0nk/SKr5+K34n52PqMqEfswu6RtU4r7wA8vSD+gM8/D9qq4aJkHImwn1+9iEFTbjoWsQeqtaQ== + dependencies: + postcss "^7.0.2" + +postcss-normalize-charset@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4" + integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g== + dependencies: + postcss "^7.0.0" + +postcss-normalize-display-values@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.1.tgz#d9a83d47c716e8a980f22f632c8b0458cfb48a4c" + integrity sha512-R5mC4vaDdvsrku96yXP7zak+O3Mm9Y8IslUobk7IMP+u/g+lXvcN4jngmHY5zeJnrQvE13dfAg5ViU05ZFDwdg== + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-positions@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.1.tgz#ee2d4b67818c961964c6be09d179894b94fd6ba1" + integrity sha512-GNoOaLRBM0gvH+ZRb2vKCIujzz4aclli64MBwDuYGU2EY53LwiP7MxOZGE46UGtotrSnmarPPZ69l2S/uxdaWA== + dependencies: + cssnano-util-get-arguments "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-repeat-style@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.1.tgz#5293f234b94d7669a9f805495d35b82a581c50e5" + integrity sha512-fFHPGIjBUyUiswY2rd9rsFcC0t3oRta4wxE1h3lpwfQZwFeFjXFSiDtdJ7APCmHQOnUZnqYBADNRPKPwFAONgA== + dependencies: + cssnano-util-get-arguments "^4.0.0" + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-string@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.1.tgz#23c5030c2cc24175f66c914fa5199e2e3c10fef3" + integrity sha512-IJoexFTkAvAq5UZVxWXAGE0yLoNN/012v7TQh5nDo6imZJl2Fwgbhy3J2qnIoaDBrtUP0H7JrXlX1jjn2YcvCQ== + dependencies: + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-timing-functions@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.1.tgz#8be83e0b9cb3ff2d1abddee032a49108f05f95d7" + integrity sha512-1nOtk7ze36+63ONWD8RCaRDYsnzorrj+Q6fxkQV+mlY5+471Qx9kspqv0O/qQNMeApg8KNrRf496zHwJ3tBZ7w== + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-unicode@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb" + integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg== + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-url@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1" + integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA== + dependencies: + is-absolute-url "^2.0.0" + normalize-url "^3.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-whitespace@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.1.tgz#d14cb639b61238418ac8bc8d3b7bdd65fc86575e" + integrity sha512-U8MBODMB2L+nStzOk6VvWWjZgi5kQNShCyjRhMT3s+W9Jw93yIjOnrEkKYD3Ul7ChWbEcjDWmXq0qOL9MIAnAw== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-ordered-values@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.1.tgz#2e3b432ef3e489b18333aeca1f1295eb89be9fc2" + integrity sha512-PeJiLgJWPzkVF8JuKSBcylaU+hDJ/TX3zqAMIjlghgn1JBi6QwQaDZoDIlqWRcCAI8SxKrt3FCPSRmOgKRB97Q== + dependencies: + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-overflow-shorthand@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz#31ecf350e9c6f6ddc250a78f0c3e111f32dd4c30" + integrity sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g== + dependencies: + postcss "^7.0.2" + +postcss-page-break@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-2.0.0.tgz#add52d0e0a528cabe6afee8b46e2abb277df46bf" + integrity sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ== + dependencies: + postcss "^7.0.2" + +postcss-place@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-4.0.1.tgz#e9f39d33d2dc584e46ee1db45adb77ca9d1dcc62" + integrity sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-preset-env@6.0.6: + version "6.0.6" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-6.0.6.tgz#f728b9a43bf01c24eb06efeeff59de0b31ee1105" + integrity sha512-W1Wtqngl7BMe4s9o76odTaVs4HXVLhOHD+L5Ez+7x15yiA+98W/WVO6IPlC1q9BIkgAckRtUFmEDr0sNufXZIQ== + dependencies: + autoprefixer "^9.1.5" + browserslist "^4.1.1" + caniuse-lite "^1.0.30000887" + cssdb "^3.2.1" + postcss "^7.0.2" + postcss-attribute-case-insensitive "^4.0.0" + postcss-color-functional-notation "^2.0.1" + postcss-color-hex-alpha "^5.0.2" + postcss-color-mod-function "^3.0.3" + postcss-color-rebeccapurple "^4.0.1" + postcss-custom-media "^7.0.4" + postcss-custom-properties "^8.0.5" + postcss-custom-selectors "^5.1.2" + postcss-dir-pseudo-class "^5.0.0" + postcss-env-function "^2.0.2" + postcss-focus-visible "^4.0.0" + postcss-focus-within "^3.0.0" + postcss-font-variant "^4.0.0" + postcss-gap-properties "^2.0.0" + postcss-image-set-function "^3.0.1" + postcss-initial "^3.0.0" + postcss-lab-function "^2.0.1" + postcss-logical "^3.0.0" + postcss-media-minmax "^4.0.0" + postcss-nesting "^7.0.0" + postcss-overflow-shorthand "^2.0.0" + postcss-page-break "^2.0.0" + postcss-place "^4.0.1" + postcss-pseudo-class-any-link "^6.0.0" + postcss-replace-overflow-wrap "^3.0.0" + postcss-selector-matches "^4.0.0" + postcss-selector-not "^4.0.0" + +postcss-pseudo-class-any-link@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz#2ed3eed393b3702879dec4a87032b210daeb04d1" + integrity sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + +postcss-reduce-initial@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.2.tgz#bac8e325d67510ee01fa460676dc8ea9e3b40f15" + integrity sha512-epUiC39NonKUKG+P3eAOKKZtm5OtAtQJL7Ye0CBN1f+UQTHzqotudp+hki7zxXm7tT0ZAKDMBj1uihpPjP25ug== + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + +postcss-reduce-transforms@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.1.tgz#8600d5553bdd3ad640f43bff81eb52f8760d4561" + integrity sha512-sZVr3QlGs0pjh6JAIe6DzWvBaqYw05V1t3d9Tp+VnFRT5j+rsqoWsysh/iSD7YNsULjq9IAylCznIwVd5oU/zA== + dependencies: + cssnano-util-get-match "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-replace-overflow-wrap@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz#61b360ffdaedca84c7c918d2b0f0d0ea559ab01c" + integrity sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw== + dependencies: + postcss "^7.0.2" + +postcss-safe-parser@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.1.tgz#8756d9e4c36fdce2c72b091bbc8ca176ab1fcdea" + integrity sha512-xZsFA3uX8MO3yAda03QrG3/Eg1LN3EPfjjf07vke/46HERLZyHrTsQ9E1r1w1W//fWEhtYNndo2hQplN2cVpCQ== + dependencies: + postcss "^7.0.0" + +postcss-selector-matches@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz#71c8248f917ba2cc93037c9637ee09c64436fcff" + integrity sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww== + dependencies: + balanced-match "^1.0.0" + postcss "^7.0.2" + +postcss-selector-not@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-4.0.0.tgz#c68ff7ba96527499e832724a2674d65603b645c0" + integrity sha512-W+bkBZRhqJaYN8XAnbbZPLWMvZD1wKTu0UxtFKdhtGjWYmxhkUneoeOhRJKdAE5V7ZTlnbHfCR+6bNwK9e1dTQ== + dependencies: + balanced-match "^1.0.0" + postcss "^7.0.2" + +postcss-selector-parser@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" + integrity sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A= + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-selector-parser@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865" + integrity sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU= + dependencies: + dot-prop "^4.1.1" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-selector-parser@^5.0.0-rc.3: + version "5.0.0-rc.3" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0-rc.3.tgz#c4525dcc8eb90166c53dcbf0cb9317ceff5a15b5" + integrity sha512-kBl1vc+zJgWCBmmxEXE2/15tmmYdD50lO5r6tLNXEx3K4LtszdLFaSNo8SNVuoI+BGODbWhavoG/n1DrYphBsw== + dependencies: + babel-eslint "^8.2.3" + cssesc "^1.0.1" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-svgo@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.1.tgz#5628cdb38f015de6b588ce6d0bf0724b492b581d" + integrity sha512-YD5uIk5NDRySy0hcI+ZJHwqemv2WiqqzDgtvgMzO8EGSkK5aONyX8HMVFRFJSdO8wUWTuisUFn/d7yRRbBr5Qw== + dependencies: + is-svg "^3.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + svgo "^1.0.0" + +postcss-unique-selectors@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac" + integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg== + dependencies: + alphanum-sort "^1.0.0" + postcss "^7.0.0" + uniqs "^2.0.0" + +postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.0, postcss-value-parser@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + +postcss-values-parser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.0.tgz#1ba42cae31367c44f96721cb5eb99462bfb39705" + integrity sha512-cyRdkgbRRefu91ByAlJow4y9w/hnBmmWgLpWmlFQ2bpIy2eKrqowt3VeYcaHQ08otVXmC9V2JtYW1Z/RpvYR8A== + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss@^6.0.1, postcss@^6.0.23: + version "6.0.23" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" + integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== + dependencies: + chalk "^2.4.1" + source-map "^0.6.1" + supports-color "^5.4.0" + +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.2, postcss@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.5.tgz#70e6443e36a6d520b0fd4e7593fcca3635ee9f55" + integrity sha512-HBNpviAUFCKvEh7NZhw1e8MBPivRszIiUnhrJ+sBFVSYSqubrzwX3KG51mYgcRHX8j/cAgZJedONZcm5jTBdgQ== + dependencies: + chalk "^2.4.1" + source-map "^0.6.1" + supports-color "^5.5.0" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= + +prettier@^1.14.2: + version "1.14.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.14.3.tgz#90238dd4c0684b7edce5f83b0fb7328e48bd0895" + integrity sha512-qZDVnCrnpsRJJq5nSsiHCE3BYMED2OtsI+cmzIzF1QIfqm5ALf8tEJcO27zV1gKNKRPdhjO0dNWnrzssDQ1tFg== + +pretty-bytes@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" + integrity sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk= + +pretty-error@^2.0.2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" + integrity sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM= + dependencies: + renderkid "^2.0.1" + utila "~0.4" + +pretty-format@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.6.0.tgz#5eaac8eeb6b33b987b7fe6097ea6a8a146ab5760" + integrity sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw== + dependencies: + ansi-regex "^3.0.0" + ansi-styles "^3.2.0" + +private@^0.1.6, private@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" + integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + +progress@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" + integrity sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8= + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= + +promise@8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/promise/-/promise-8.0.2.tgz#9dcd0672192c589477d56891271bdc27547ae9f0" + integrity sha512-EIyzM39FpVOMbqgzEHhxdrEhtOSDOtjMZQ0M6iVfCE+kWNgCkAyOdnuCWqfmflylftfadU6FkiMgHZA2kUzwRw== + dependencies: + asap "~2.0.6" + +prompts@^0.1.9: + version "0.1.14" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-0.1.14.tgz#a8e15c612c5c9ec8f8111847df3337c9cbd443b2" + integrity sha512-rxkyiE9YH6zAz/rZpywySLKkpaj0NMVyNw1qhsubdbjjSgcayjTShDreZGlFMcGSu5sab3bAKPfFk78PB90+8w== + dependencies: + kleur "^2.0.1" + sisteransi "^0.1.1" + +prop-types@^15.6.2: + version "15.6.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" + integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== + dependencies: + loose-envify "^1.3.1" + object-assign "^4.1.1" + +proxy-addr@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" + integrity sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.8.0" + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + +psl@^1.1.24: + version "1.1.29" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67" + integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ== + +public-encrypt@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" + integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + safe-buffer "^5.1.2" + +pump@^2.0.0, pump@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@2.x.x, punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +punycode@^1.2.4, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +q@^1.1.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + +qs@6.5.2, qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +querystringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef" + integrity sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg== + +raf@3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" + integrity sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw== + dependencies: + performance-now "^2.1.0" + +randomatic@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.0.tgz#36f2ca708e9e567f5ed2ec01949026d50aa10116" + integrity sha512-KnGPVE0lo2WoXxIZ7cPR8YBpiol4gsSuOwDSg410oHh80ZMp5EiypNqL2K4Z77vJn6lB5rap7IkAmcUlalcnBQ== + dependencies: + is-number "^4.0.0" + kind-of "^6.0.0" + math-random "^1.0.1" + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" + integrity sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A== + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +range-parser@^1.0.3, range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= + +raw-body@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" + integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== + dependencies: + bytes "3.0.0" + http-errors "1.6.3" + iconv-lite "0.4.23" + unpipe "1.0.0" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +react-app-polyfill@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-0.1.3.tgz#e57bb50f3751dac0e6b3ac27673812c68c679a1d" + integrity sha512-Fl5Pic4F15G05qX7RmUqPZr1MtyFKJKSlRwMhel4kvDLrk/KcQ9QbpvyMTzv/0NN5957XFQ7r1BNHWi7qN59Pw== + dependencies: + core-js "2.5.7" + object-assign "4.1.1" + promise "8.0.2" + raf "3.4.0" + whatwg-fetch "3.0.0" + +react-dev-utils@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-6.0.5.tgz#6ef34d0a416dc1c97ac20025031ea1f0d819b21d" + integrity sha512-X3/q2y8GHvcn6qzqlFhFNoIQgU4TfyerYpBTc5BrMtrSO0q1ctIheAaxDQawNeRgsm2W7L3QSQ9po+YLAGcYFQ== + dependencies: + "@babel/code-frame" "7.0.0" + address "1.0.3" + browserslist "4.1.1" + chalk "2.4.1" + cross-spawn "6.0.5" + detect-port-alt "1.1.6" + escape-string-regexp "1.0.5" + filesize "3.6.1" + find-up "3.0.0" + global-modules "1.0.0" + gzip-size "5.0.0" + inquirer "6.2.0" + is-root "2.0.0" + loader-utils "1.1.0" + opn "5.4.0" + pkg-up "2.0.0" + react-error-overlay "^5.0.5" + recursive-readdir "2.2.2" + shell-quote "1.6.1" + sockjs-client "1.1.5" + strip-ansi "4.0.0" + text-table "0.2.0" + +react-dom@^16.6.0: + version "16.6.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.6.0.tgz#6375b8391e019a632a89a0988bce85f0cc87a92f" + integrity sha512-Stm2D9dXEUUAQdvpvhvFj/DEXwC2PAL/RwEMhoN4dvvD2ikTlJegEXf97xryg88VIAU22ZAP7n842l+9BTz6+w== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.10.0" + +react-error-overlay@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.0.5.tgz#716ff1a92fda76bb89a39adf9ce046a5d740e71a" + integrity sha512-ab0HWBgxdIsngHtMGU8+8gYFdTBXpUGd4AE4lN2VZvOIlBmWx9dtaWEViihuGSIGosCKPeHCnzFoRWB42UtnLg== + +react-scripts@2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-2.0.5.tgz#74b8e9fa6a7c5f0f11221dd18c10df2ae3df3d69" + integrity sha512-ApwQM91U5FK9CPktUJQu+UhGIdgcBqqw0pvCUPYshW1jBhIgl8N3SJtd+o+I+jwRXPWAXcbJNvzsBAFDW5HHAQ== + dependencies: + "@babel/core" "7.1.0" + "@svgr/webpack" "2.4.1" + babel-core "7.0.0-bridge.0" + babel-eslint "9.0.0" + babel-jest "23.6.0" + babel-loader "8.0.4" + babel-plugin-named-asset-import "^0.2.2" + babel-preset-react-app "^5.0.4" + bfj "6.1.1" + case-sensitive-paths-webpack-plugin "2.1.2" + chalk "2.4.1" + css-loader "1.0.0" + dotenv "6.0.0" + dotenv-expand "4.2.0" + eslint "5.6.0" + eslint-config-react-app "^3.0.4" + eslint-loader "2.1.1" + eslint-plugin-flowtype "2.50.1" + eslint-plugin-import "2.14.0" + eslint-plugin-jsx-a11y "6.1.2" + eslint-plugin-react "7.11.1" + file-loader "2.0.0" + fs-extra "7.0.0" + html-webpack-plugin "4.0.0-alpha.2" + identity-obj-proxy "3.0.0" + jest "23.6.0" + jest-pnp-resolver "1.0.1" + jest-resolve "23.6.0" + mini-css-extract-plugin "0.4.3" + optimize-css-assets-webpack-plugin "5.0.1" + pnp-webpack-plugin "1.1.0" + postcss-flexbugs-fixes "4.1.0" + postcss-loader "3.0.0" + postcss-preset-env "6.0.6" + postcss-safe-parser "4.0.1" + react-app-polyfill "^0.1.3" + react-dev-utils "^6.0.5" + resolve "1.8.1" + sass-loader "7.1.0" + style-loader "0.23.0" + terser-webpack-plugin "1.1.0" + url-loader "1.1.1" + webpack "4.19.1" + webpack-dev-server "3.1.9" + webpack-manifest-plugin "2.0.4" + workbox-webpack-plugin "3.6.2" + optionalDependencies: + fsevents "1.2.4" + +react@^16.6.0: + version "16.6.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.6.0.tgz#b34761cfaf3e30f5508bc732fb4736730b7da246" + integrity sha512-zJPnx/jKtuOEXCbQ9BKaxDMxR0001/hzxXwYxG8septeyYGfsgAei6NgfbVgOhbY1WOP2o3VPs/E9HaN+9hV3Q== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.10.0" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3, readable-stream@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@1.0: + 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@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + +realpath-native@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.2.tgz#cd51ce089b513b45cf9b1516c82989b51ccc6560" + integrity sha512-+S3zTvVt9yTntFrBpm7TQmQ3tzpCrnA1a/y+3cUHAc9ZR6aIjG0WNLR+Rj79QpJktY+VeW/TQtFlQ1bzsehI8g== + dependencies: + util.promisify "^1.0.0" + +recursive-readdir@2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" + integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg== + dependencies: + minimatch "3.0.4" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +reduce-css-calc@^2.0.0: + version "2.1.5" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.5.tgz#f283712f0c9708ef952d328f4b16112d57b03714" + integrity sha512-AybiBU03FKbjYzyvJvwkJZY6NLN+80Ufc2EqEs+41yQH+8wqBEslD6eGiS0oIeq5TNLA5PrhBeYHXWdn8gtW7A== + dependencies: + css-unit-converter "^1.1.1" + postcss-value-parser "^3.3.0" + +regenerate-unicode-properties@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c" + integrity sha512-s5NGghCE4itSlUS+0WUj88G6cfMVMmH8boTPNvABf8od+2dhT9WDlWu8n01raQAJZMOK8Ch6jSexaRO7swd6aw== + dependencies: + regenerate "^1.4.0" + +regenerate@^1.2.1, regenerate@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" + integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== + +regenerator-runtime@^0.12.0: + version "0.12.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" + integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== + +regenerator-transform@^0.13.3: + version "0.13.3" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.3.tgz#264bd9ff38a8ce24b06e0636496b2c856b57bcbb" + integrity sha512-5ipTrZFSq5vU2YoGoww4uaRVAK4wyYC4TSICibbfEPOruUu8FFP7ErV0BjmbIOEpn3O/k9na9UEdYR/3m7N6uA== + dependencies: + private "^0.1.6" + +regex-cache@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" + integrity sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ== + dependencies: + is-equal-shallow "^0.1.3" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regexpp@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" + integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== + +regexpu-core@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" + integrity sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs= + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regexpu-core@^4.1.3, regexpu-core@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.2.0.tgz#a3744fa03806cffe146dea4421a3e73bdcc47b1d" + integrity sha512-Z835VSnJJ46CNBttalHD/dB+Sj2ezmY6Xp38npwU87peK6mqOzOpV8eYktdkLTEkzzD+JsTcxd84ozd8I14+rw== + dependencies: + regenerate "^1.4.0" + regenerate-unicode-properties "^7.0.0" + regjsgen "^0.4.0" + regjsparser "^0.3.0" + unicode-match-property-ecmascript "^1.0.4" + unicode-match-property-value-ecmascript "^1.0.2" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + integrity sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc= + +regjsgen@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.4.0.tgz#c1eb4c89a209263f8717c782591523913ede2561" + integrity sha512-X51Lte1gCYUdlwhF28+2YMO0U6WeN0GLpgpA7LK7mbdDnkQYiwvEpmpe0F/cv5L14EbxgrdayAG3JETBv0dbXA== + +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + integrity sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw= + dependencies: + jsesc "~0.5.0" + +regjsparser@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.3.0.tgz#3c326da7fcfd69fa0d332575a41c8c0cdf588c96" + integrity sha512-zza72oZBBHzt64G7DxdqrOo/30bhHkwMUoT0WqfGu98XLd7N+1tsy5MJ96Bk4MD0y74n629RhmrGW6XlnLLwCA== + dependencies: + jsesc "~0.5.0" + +relateurl@0.2.x: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +renderkid@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.2.tgz#12d310f255360c07ad8fde253f6c9e9de372d2aa" + integrity sha512-FsygIxevi1jSiPY9h7vZmBFUbAOcbYm9UwyiLNdVsLRs/5We9Ob5NMPbGYUTWiLq5L+ezlVdE0A8bbME5CWTpg== + dependencies: + css-select "^1.1.0" + dom-converter "~0.2" + htmlparser2 "~3.3.0" + strip-ansi "^3.0.0" + utila "^0.4.0" + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.5.2, repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= + dependencies: + is-finite "^1.0.0" + +request-promise-core@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6" + integrity sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY= + dependencies: + lodash "^4.13.1" + +request-promise-native@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.5.tgz#5281770f68e0c9719e5163fd3fab482215f4fda5" + integrity sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU= + dependencies: + request-promise-core "1.1.1" + stealthy-require "^1.1.0" + tough-cookie ">=2.3.3" + +request@^2.87.0, request@^2.88.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-from-string@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= + +require-uncached@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" + integrity sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM= + dependencies: + caller-path "^0.1.0" + resolve-from "^1.0.0" + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= + dependencies: + resolve-from "^3.0.0" + +resolve-dir@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" + integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + +resolve-from@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" + integrity sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY= + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= + +resolve@1.8.1, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.6.0, resolve@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" + integrity sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA== + dependencies: + path-parse "^1.0.5" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +rgb-regex@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" + integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= + +rgba-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" + integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= + +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w== + dependencies: + glob "^7.0.5" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +rsvp@^3.3.3: + version "3.6.2" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" + integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw== + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= + dependencies: + is-promise "^2.1.0" + +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec= + dependencies: + aproba "^1.1.1" + +rxjs@^6.1.0: + version "6.3.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz#3c6a7fa420e844a81390fb1158a9ec614f4bad55" + integrity sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw== + dependencies: + tslib "^1.9.0" + +safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sane@^2.0.0: + version "2.5.2" + resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.2.tgz#b4dc1861c21b427e929507a3e751e2a2cb8ab3fa" + integrity sha1-tNwYYcIbQn6SlQej51HiosuKs/o= + dependencies: + anymatch "^2.0.0" + capture-exit "^1.2.0" + exec-sh "^0.2.0" + fb-watchman "^2.0.0" + micromatch "^3.1.4" + minimist "^1.1.1" + walker "~1.0.5" + watch "~0.18.0" + optionalDependencies: + fsevents "^1.2.3" + +sass-graph@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" + integrity sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k= + dependencies: + glob "^7.0.0" + lodash "^4.0.0" + scss-tokenizer "^0.2.3" + yargs "^7.0.0" + +sass-loader@7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.1.0.tgz#16fd5138cb8b424bf8a759528a1972d72aad069d" + integrity sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w== + dependencies: + clone-deep "^2.0.1" + loader-utils "^1.0.1" + lodash.tail "^4.1.1" + neo-async "^2.5.0" + pify "^3.0.0" + semver "^5.5.0" + +sax@^1.2.4, sax@~1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +saxes@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.3.tgz#334ab3b802a465ccda96fff9bdefbd505546ffa8" + integrity sha512-Nc5DXc5A+m3rUDtkS+vHlBWKT7mCKjJPyia7f8YMW773hsXVv2wEHQZGE0zs4+5PLwz9U5Sbl/94Cnd9vHV7Bg== + dependencies: + xmlchars "^1.3.1" + +scheduler@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.10.0.tgz#7988de90fe7edccc774ea175a783e69c40c521e1" + integrity sha512-+TSTVTCBAA3h8Anei3haDc1IRwMeDmtI/y/o3iBe3Mjl2vwYF9DtPDt929HyRmV/e7au7CLu8sc4C4W0VOs29w== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + +schema-utils@^0.4.4, schema-utils@^0.4.5: + version "0.4.7" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" + integrity sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ== + dependencies: + ajv "^6.1.0" + ajv-keywords "^3.1.0" + +schema-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" + integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== + dependencies: + ajv "^6.1.0" + ajv-errors "^1.0.0" + ajv-keywords "^3.1.0" + +scss-tokenizer@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" + integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE= + dependencies: + js-base64 "^2.1.8" + source-map "^0.4.2" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= + +selfsigned@^1.9.1: + version "1.10.4" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.4.tgz#cdd7eccfca4ed7635d47a08bf2d5d3074092e2cd" + integrity sha512-9AukTiDmHXGXWtWjembZ5NDmVvP2695EtpgbCsxCa68w3c88B+alqbmZ4O3hZ4VWGXeGWzEVdvqgAJD8DQPCDw== + dependencies: + node-forge "0.7.5" + +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1: + version "5.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" + integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== + +semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= + +send@0.16.2: + version "0.16.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" + integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.6.2" + mime "1.4.1" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.4.0" + +serialize-javascript@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.5.0.tgz#1aa336162c88a890ddad5384baebc93a655161fe" + integrity sha512-Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ== + +serve-index@^1.7.2: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.13.2: + version "1.13.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" + integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.2" + send "0.16.2" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +set-value@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" + integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE= + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.1" + to-object-path "^0.3.0" + +set-value@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" + integrity sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shallow-clone@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060" + integrity sha1-WQnodLp3EG1zrEFM/sH/yofZcGA= + dependencies: + is-extendable "^0.1.1" + kind-of "^2.0.1" + lazy-cache "^0.2.3" + mixin-object "^2.0.1" + +shallow-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571" + integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA== + dependencies: + is-extendable "^0.1.1" + kind-of "^5.0.0" + mixin-object "^2.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shell-quote@1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767" + integrity sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c= + dependencies: + array-filter "~0.0.0" + array-map "~0.0.0" + array-reduce "~0.0.0" + jsonify "~0.0.0" + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + +sisteransi@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-0.1.1.tgz#5431447d5f7d1675aac667ccd0b865a4994cb3ce" + integrity sha512-PmGOd02bM9YO5ifxpw36nrNMBTptEtfRl4qUYl9SndkolplkrZZOW7PGHjrZL53QvMVj9nQ+TKqUnRsw4tJa4g== + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= + +slice-ansi@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" + integrity sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg== + dependencies: + is-fullwidth-code-point "^2.0.0" + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +sockjs-client@1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.5.tgz#1bb7c0f7222c40f42adf14f4442cbd1269771a83" + integrity sha1-G7fA9yIsQPQq3xT0RCy9Eml3GoM= + dependencies: + debug "^2.6.6" + eventsource "0.1.6" + faye-websocket "~0.11.0" + inherits "^2.0.1" + json3 "^3.3.2" + url-parse "^1.1.8" + +sockjs@0.3.19: + version "0.3.19" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d" + integrity sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw== + dependencies: + faye-websocket "^0.10.0" + uuid "^3.0.1" + +source-list-map@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== + +source-map-resolve@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" + integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== + dependencies: + atob "^2.1.1" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.4.15: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== + dependencies: + source-map "^0.5.6" + +source-map-support@^0.5.6, source-map-support@~0.5.6: + version "0.5.9" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" + integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + integrity sha1-66T12pwNyZneaAMti092FzZSA2s= + dependencies: + amdefine ">=0.0.4" + +source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdx-correct@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.2.tgz#19bb409e91b47b1ad54159243f7312a858db3c2e" + integrity sha512-q9hedtzyXHr5S0A1vEPoK/7l8NpfkFYTq6iCY+Pno2ZbdZR6WexZFtqeVGkGxW3TEJMN914Z55EnAGMmenlIQQ== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.1.tgz#e2a303236cac54b04031fa7a5a79c7e701df852f" + integrity sha512-TfOfPcYGBB5sDuPn3deByxPhmfegAhpDYKSOXZQN81Oyrrif8ZCodOLzK3AesELnCx03kikhyDwh0pfvvQvF8w== + +spdy-transport@^2.0.18: + version "2.1.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.1.0.tgz#4bbb15aaffed0beefdd56ad61dbdc8ba3e2cb7a1" + integrity sha512-bpUeGpZcmZ692rrTiqf9/2EUakI6/kXX1Rpe0ib/DyOzbiexVfXkw6GnvI9hVGvIwVaUhkaBojjCZwLNRGQg1g== + dependencies: + debug "^2.6.8" + detect-node "^2.0.3" + hpack.js "^2.1.6" + obuf "^1.1.1" + readable-stream "^2.2.9" + safe-buffer "^5.0.1" + wbuf "^1.7.2" + +spdy@^3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.7.tgz#42ff41ece5cc0f99a3a6c28aabb73f5c3b03acbc" + integrity sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw= + dependencies: + debug "^2.6.8" + handle-thing "^1.2.5" + http-deceiver "^1.2.7" + safe-buffer "^5.0.1" + select-hose "^2.0.0" + spdy-transport "^2.0.18" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sshpk@^1.7.0: + version "1.15.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.15.1.tgz#b79a089a732e346c6e0714830f36285cd38191a2" + integrity sha512-mSdgNUaidk+dRU5MhYtN9zebdzF2iG0cNPWy8HG+W8y+fT1JnSkh0fzzpjOa0L7P8i1Rscz38t0h4gPcKz43xA== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +ssri@^5.2.4: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06" + integrity sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ== + dependencies: + safe-buffer "^5.1.1" + +ssri@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" + integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== + dependencies: + figgy-pudding "^3.5.1" + +stable@~0.1.6: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +stack-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" + integrity sha1-1PM6tU6OOHeLDKXP07OvsS22hiA= + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +statuses@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" + integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== + +stdout-stream@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de" + integrity sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA== + dependencies: + readable-stream "^2.0.1" + +stealthy-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + +stream-browserify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + integrity sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds= + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-each@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" + integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + +stream-http@^2.7.2: + version "2.8.3" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" + integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= + +string-length@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" + integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0= + dependencies: + astral-regex "^1.0.0" + strip-ansi "^4.0.0" + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@^1.0.0, string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +stringify-object@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.2.tgz#9853052e5a88fb605a44cd27445aa257ad7ffbcd" + integrity sha512-O696NF21oLiDy8PhpWu8AEqoZHw++QW6mUv0UvKZe8gWSdSvMXkiLufK7OmnP27Dro4GU5kb9U7JIO0mBuCRQg== + dependencies: + get-own-enumerable-property-symbols "^2.0.1" + is-obj "^1.0.1" + is-regexp "^1.0.0" + +strip-ansi@4.0.0, strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-bom@3.0.0, strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= + dependencies: + is-utf8 "^0.2.0" + +strip-comments@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-1.0.2.tgz#82b9c45e7f05873bee53f37168af930aa368679d" + integrity sha512-kL97alc47hoyIQSV165tTt9rG5dn4w1dNnBhOQ3bOU1Nc1hel09jnXANaHJ7vzHLd4Ju8kseDGzlev96pghLFw== + dependencies: + babel-extract-comments "^1.0.0" + babel-plugin-transform-object-rest-spread "^6.26.0" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +style-loader@0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.0.tgz#8377fefab68416a2e05f1cabd8c3a3acfcce74f1" + integrity sha512-uCcN7XWHkqwGVt7skpInW6IGO1tG6ReyFQ1Cseh0VcN6VdcFQi62aG/2F3Y9ueA8x4IVlfaSUxpmQXQD9QrEuQ== + dependencies: + loader-utils "^1.1.0" + schema-utils "^0.4.5" + +stylehacks@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.1.tgz#3186595d047ab0df813d213e51c8b94e0b9010f2" + integrity sha512-TK5zEPeD9NyC1uPIdjikzsgWxdQQN/ry1X3d1iOz1UkYDCmcr928gWD1KHgyC27F50UnE0xCTrBOO1l6KR8M4w== + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + +supports-color@^3.1.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= + dependencies: + has-flag "^1.0.0" + +supports-color@^5.1.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +svgo@^1.0.0, svgo@^1.0.5: + version "1.1.1" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.1.1.tgz#12384b03335bcecd85cfa5f4e3375fed671cb985" + integrity sha512-GBkJbnTuFpM4jFbiERHDWhZc/S/kpHToqmZag3aEBjPYK44JAN2QBjvrGIxLOoCyMZjuFQIfTO2eJd8uwLY/9g== + dependencies: + coa "~2.0.1" + colors "~1.1.2" + css-select "^2.0.0" + css-select-base-adapter "~0.1.0" + css-tree "1.0.0-alpha.28" + css-url-regex "^1.1.0" + csso "^3.5.0" + js-yaml "^3.12.0" + mkdirp "~0.5.1" + object.values "^1.0.4" + sax "~1.2.4" + stable "~0.1.6" + unquote "~1.1.1" + util.promisify "~1.0.0" + +symbol-tree@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" + integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY= + +table@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc" + integrity sha512-S7rnFITmBH1EnyKcvxBh1LjYeQMmnZtCXSEbHcH6S0NoKit24ZuFO/T1vDcLdYsLQkM188PVVhQmzKIuThNkKg== + dependencies: + ajv "^6.0.1" + ajv-keywords "^3.0.0" + chalk "^2.1.0" + lodash "^4.17.4" + slice-ansi "1.0.0" + string-width "^2.1.1" + +tapable@^1.0.0, tapable@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.0.tgz#0d076a172e3d9ba088fd2272b2668fb8d194b78c" + integrity sha512-IlqtmLVaZA2qab8epUXbVWRn3aB1imbDMJtjB3nu4X0NqPkcY/JH9ZtCBWKHWPxs8Svi9tyo8w2dBoi07qZbBA== + +tar@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + integrity sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE= + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +tar@^4: + version "4.4.6" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b" + integrity sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg== + dependencies: + chownr "^1.0.1" + fs-minipass "^1.2.5" + minipass "^2.3.3" + minizlib "^1.1.0" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.2" + +terser-webpack-plugin@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.1.0.tgz#cf7c25a1eee25bf121f4a587bb9e004e3f80e528" + integrity sha512-61lV0DSxMAZ8AyZG7/A4a3UPlrbOBo8NIQ4tJzLPAdGOQ+yoNC7l5ijEow27lBAL2humer01KLS6bGIMYQxKoA== + dependencies: + cacache "^11.0.2" + find-cache-dir "^2.0.0" + schema-utils "^1.0.0" + serialize-javascript "^1.4.0" + source-map "^0.6.1" + terser "^3.8.1" + webpack-sources "^1.1.0" + worker-farm "^1.5.2" + +terser@^3.8.1: + version "3.10.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-3.10.0.tgz#6ae15dafecbd02c9788d5f36d27fca32196b533a" + integrity sha512-hNh2WR3YxtKoY7BNSb3+CJ9Xv9bbUuOU9uriIf2F1tiAYNA4rNy2wWuSDV8iKcag27q65KPJ/sPpMWEh6qttgw== + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + source-map-support "~0.5.6" + +test-exclude@^4.2.1: + version "4.2.3" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.3.tgz#a9a5e64474e4398339245a0a769ad7c2f4a97c20" + integrity sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA== + dependencies: + arrify "^1.0.1" + micromatch "^2.3.11" + object-assign "^4.1.0" + read-pkg-up "^1.0.1" + require-main-filename "^1.0.1" + +text-table@0.2.0, text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +throat@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" + integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= + +through2@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + integrity sha1-AARWmzfHx0ujnEPzzteNGtlBQL4= + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +thunky@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.2.tgz#a862e018e3fb1ea2ec3fce5d55605cf57f247371" + integrity sha1-qGLgGOP7HqLsP85dVWBc9X8kc3E= + +timers-browserify@^2.0.4: + version "2.0.10" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae" + integrity sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg== + dependencies: + setimmediate "^1.0.4" + +timsort@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" + integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= + +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +topo@2.x.x: + version "2.0.2" + resolved "https://registry.yarnpkg.com/topo/-/topo-2.0.2.tgz#cd5615752539057c0dc0491a621c3bc6fbe1d182" + integrity sha1-zVYVdSU5BXwNwEkaYhw7xvvh0YI= + dependencies: + hoek "4.x.x" + +tough-cookie@>=2.3.3, tough-cookie@^2.3.4, tough-cookie@^2.4.3, tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= + dependencies: + punycode "^2.1.0" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= + +"true-case-path@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" + integrity sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew== + dependencies: + glob "^7.1.2" + +tryer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" + integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== + +tslib@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" + integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-is@~1.6.16: + version "1.6.16" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" + integrity sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.18" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +uglify-es@^3.3.4: + version "3.3.9" + resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" + integrity sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ== + dependencies: + commander "~2.13.0" + source-map "~0.6.1" + +uglify-js@3.4.x, uglify-js@^3.1.4: + version "3.4.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + integrity sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q== + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + +uglifyjs-webpack-plugin@^1.2.4: + version "1.3.0" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz#75f548160858163a08643e086d5fefe18a5d67de" + integrity sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw== + dependencies: + cacache "^10.0.4" + find-cache-dir "^1.0.0" + schema-utils "^0.4.5" + serialize-javascript "^1.4.0" + source-map "^0.6.1" + uglify-es "^3.3.4" + webpack-sources "^1.1.0" + worker-farm "^1.5.2" + +unicode-canonical-property-names-ecmascript@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" + integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== + +unicode-match-property-ecmascript@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" + integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== + dependencies: + unicode-canonical-property-names-ecmascript "^1.0.4" + unicode-property-aliases-ecmascript "^1.0.4" + +unicode-match-property-value-ecmascript@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.0.2.tgz#9f1dc76926d6ccf452310564fd834ace059663d4" + integrity sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ== + +unicode-property-aliases-ecmascript@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0" + integrity sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg== + +union-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" + integrity sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ= + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^0.4.3" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= + +unique-filename@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.1.tgz#5e9edc6d1ce8fb264db18a507ef9bd8544451ca6" + integrity sha512-n9cU6+gITaVu7VGj1Z8feKMmfAjEAQGhwD9fE3zvpRRa0wEIx8ODYkVGfSc94M2OX00tUFV8wH3zYbm1I8mxFg== + dependencies: + imurmurhash "^0.1.4" + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +unquote@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" + integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@^1.0.5: + version "1.1.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" + integrity sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw== + +upper-case@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" + integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg= + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +url-loader@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.1.1.tgz#4d1f3b4f90dde89f02c008e662d604d7511167c1" + integrity sha512-vugEeXjyYFBCUOpX+ZuaunbK3QXMKaQ3zUnRfIpRBlGkY7QizCnzyyn2ASfcxsvyU3ef+CJppVywnl3Kgf13Gg== + dependencies: + loader-utils "^1.1.0" + mime "^2.0.3" + schema-utils "^1.0.0" + +url-parse@^1.1.8, url-parse@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.3.tgz#bfaee455c889023219d757e045fa6a684ec36c15" + integrity sha512-rh+KuAW36YKo0vClhQzLLveoj8FwPJNu65xLb7Mrt+eZht0IPT0IXgSv8gcMegZ6NvjJUALf6Mf25POlMwD1Fw== + dependencies: + querystringify "^2.0.0" + requires-port "^1.0.0" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util.promisify@1.0.0, util.promisify@^1.0.0, util.promisify@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== + dependencies: + define-properties "^1.1.2" + object.getownpropertydescriptors "^2.0.3" + +util@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= + dependencies: + inherits "2.0.1" + +util@^0.10.3: + version "0.10.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== + dependencies: + inherits "2.0.3" + +utila@^0.4.0, utila@~0.4: + version "0.4.0" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@^3.0.1, uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +vendors@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.2.tgz#7fcb5eef9f5623b156bcea89ec37d63676f21801" + integrity sha512-w/hry/368nO21AN9QljsaIhb9ZiZtZARoVH5f3CsFbawdLdayCgKRPup7CggujvySMxx0I91NOyxdVENohprLQ== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vm-browserify@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + integrity sha1-XX6kW7755Kb/ZflUOOCofDV9WnM= + dependencies: + indexof "0.0.1" + +w3c-hr-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" + integrity sha1-gqwr/2PZUOqeMYmlimViX+3xkEU= + dependencies: + browser-process-hrtime "^0.1.2" + +walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= + dependencies: + makeerror "1.0.x" + +watch@~0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" + integrity sha1-KAlUdsbffJDJYxOJkMClQj60uYY= + dependencies: + exec-sh "^0.2.0" + minimist "^1.2.0" + +watchpack@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" + integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA== + dependencies: + chokidar "^2.0.2" + graceful-fs "^4.1.2" + neo-async "^2.5.0" + +wbuf@^1.1.0, wbuf@^1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +webpack-dev-middleware@3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.4.0.tgz#1132fecc9026fd90f0ecedac5cbff75d1fb45890" + integrity sha512-Q9Iyc0X9dP9bAsYskAVJ/hmIZZQwf/3Sy4xCAZgL5cUkjZmUZLt4l5HpbST/Pdgjn3u6pE7u5OdGd1apgzRujA== + dependencies: + memory-fs "~0.4.1" + mime "^2.3.1" + range-parser "^1.0.3" + webpack-log "^2.0.0" + +webpack-dev-server@3.1.9: + version "3.1.9" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.9.tgz#8b32167624d2faff40dcedc2cbce17ed1f34d3e0" + integrity sha512-fqPkuNalLuc/hRC2QMkVYJkgNmRvxZQo7ykA2e1XRg/tMJm3qY7ZaD6d89/Fqjxtj9bOrn5wZzLD2n84lJdvWg== + dependencies: + ansi-html "0.0.7" + bonjour "^3.5.0" + chokidar "^2.0.0" + compression "^1.5.2" + connect-history-api-fallback "^1.3.0" + debug "^3.1.0" + del "^3.0.0" + express "^4.16.2" + html-entities "^1.2.0" + http-proxy-middleware "~0.18.0" + import-local "^2.0.0" + internal-ip "^3.0.1" + ip "^1.1.5" + killable "^1.0.0" + loglevel "^1.4.1" + opn "^5.1.0" + portfinder "^1.0.9" + schema-utils "^1.0.0" + selfsigned "^1.9.1" + serve-index "^1.7.2" + sockjs "0.3.19" + sockjs-client "1.1.5" + spdy "^3.4.1" + strip-ansi "^3.0.0" + supports-color "^5.1.0" + webpack-dev-middleware "3.4.0" + webpack-log "^2.0.0" + yargs "12.0.2" + +webpack-log@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" + integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== + dependencies: + ansi-colors "^3.0.0" + uuid "^3.3.2" + +webpack-manifest-plugin@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-2.0.4.tgz#e4ca2999b09557716b8ba4475fb79fab5986f0cd" + integrity sha512-nejhOHexXDBKQOj/5v5IZSfCeTO3x1Dt1RZEcGfBSul891X/eLIcIVH31gwxPDdsi2Z8LKKFGpM4w9+oTBOSCg== + dependencies: + fs-extra "^7.0.0" + lodash ">=3.5 <5" + tapable "^1.0.0" + +webpack-sources@^1.1.0, webpack-sources@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" + integrity sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack@4.19.1: + version "4.19.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.19.1.tgz#096674bc3b573f8756c762754366e5b333d6576f" + integrity sha512-j7Q/5QqZRqIFXJvC0E59ipLV5Hf6lAnS3ezC3I4HMUybwEDikQBVad5d+IpPtmaQPQArvgUZLXIN6lWijHBn4g== + dependencies: + "@webassemblyjs/ast" "1.7.6" + "@webassemblyjs/helper-module-context" "1.7.6" + "@webassemblyjs/wasm-edit" "1.7.6" + "@webassemblyjs/wasm-parser" "1.7.6" + acorn "^5.6.2" + acorn-dynamic-import "^3.0.0" + ajv "^6.1.0" + ajv-keywords "^3.1.0" + chrome-trace-event "^1.0.0" + enhanced-resolve "^4.1.0" + eslint-scope "^4.0.0" + json-parse-better-errors "^1.0.2" + loader-runner "^2.3.0" + loader-utils "^1.1.0" + memory-fs "~0.4.1" + micromatch "^3.1.8" + mkdirp "~0.5.0" + neo-async "^2.5.0" + node-libs-browser "^2.0.0" + schema-utils "^0.4.4" + tapable "^1.1.0" + uglifyjs-webpack-plugin "^1.2.4" + watchpack "^1.5.0" + webpack-sources "^1.2.0" + +websocket-driver@>=0.5.1: + version "0.7.0" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb" + integrity sha1-DK+dLXVdk67gSdS90NP+LMoqJOs= + dependencies: + http-parser-js ">=0.4.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" + integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== + +whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== + dependencies: + iconv-lite "0.4.24" + +whatwg-fetch@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" + integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== + +whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz#a3d58ef10b76009b042d03e25591ece89b88d171" + integrity sha512-5YSO1nMd5D1hY3WzAQV3PzZL83W3YeyR1yW9PcH26Weh1t+Vzh9B6XkDh7aXm83HBZ4nSMvkjvN2H2ySWIvBgw== + +whatwg-url@^6.4.1: + version "6.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" + integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +whatwg-url@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd" + integrity sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@1, which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +workbox-background-sync@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-3.6.2.tgz#a8e95c6ed0e267fa100aaebd81568b86d81e032e" + integrity sha512-K34wiTM50gSpzJUuRmGRqbd91IpJj0vwMBSHCpixw/jiTg10uytSfnixMNGzeTK0i7LTd/bkA8ptx4HXP+MliA== + dependencies: + workbox-core "^3.6.2" + +workbox-broadcast-cache-update@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/workbox-broadcast-cache-update/-/workbox-broadcast-cache-update-3.6.2.tgz#a08cf843484665e2c8e0c7e4b23b1608c0c6f787" + integrity sha512-wmN3k94Kv3/lYOqRy08ymp8RyTPCpgLI9UW/BrQ1XuZHJyFejWnBoy/pCKk9mRZYZX7EyvnzA4O1PLILgLC43g== + dependencies: + workbox-core "^3.6.2" + +workbox-build@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-3.6.2.tgz#8171283cd82076e166c34f28e1d0bd1d7034450c" + integrity sha512-PYw4SRbfbUE/+DDhb89zbspDLBi86hpra+l6SsX7yBqCthw4sHyH8IIQw5MMHI04HPV5ZDYru8A5SNLXVDGMcg== + dependencies: + babel-runtime "^6.26.0" + common-tags "^1.4.0" + fs-extra "^4.0.2" + glob "^7.1.2" + joi "^11.1.1" + lodash.template "^4.4.0" + pretty-bytes "^4.0.2" + stringify-object "^3.2.2" + strip-comments "^1.0.2" + workbox-background-sync "^3.6.2" + workbox-broadcast-cache-update "^3.6.2" + workbox-cache-expiration "^3.6.2" + workbox-cacheable-response "^3.6.2" + workbox-core "^3.6.2" + workbox-google-analytics "^3.6.2" + workbox-navigation-preload "^3.6.2" + workbox-precaching "^3.6.2" + workbox-range-requests "^3.6.2" + workbox-routing "^3.6.2" + workbox-strategies "^3.6.2" + workbox-streams "^3.6.2" + workbox-sw "^3.6.2" + +workbox-cache-expiration@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/workbox-cache-expiration/-/workbox-cache-expiration-3.6.2.tgz#0471cfe5231518a98a9ae791909d71cee1275f08" + integrity sha512-LJLYfqG7ItYucppun5I92fcN21kDZFEVqZ8uAOz5t8piOsHh1ThAiiLv/4ubG/d7CUgqW/1bmcX6DM4xqackzg== + dependencies: + workbox-core "^3.6.2" + +workbox-cacheable-response@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-3.6.2.tgz#db12a1cf330514fd48ce94399cdf08ed83217636" + integrity sha512-WvICMN3SfEi48C96KEfkLDIqnU0rkQeajdLjYXuzbUID3EX31gzUVlIbqQGrc+9xtIlvxs2+ZoaTR3Rjdtbh/Q== + dependencies: + workbox-core "^3.6.2" + +workbox-core@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-3.6.2.tgz#4c79a15685970efc2e86dc31bf25900dd405a3e5" + integrity sha512-5T5WBFy5nMm7zx+P2RwdzEVu5CK++bqwiEsGF+INwUxsOKpH9oXUlUdJE/KfUaMsKcZtHXEb74mMB6vvE88a/w== + +workbox-google-analytics@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-3.6.2.tgz#e6fcbf6c77b215a556c4e342aa20c378205a9f63" + integrity sha512-NXBbo9xyHQvkHcvYoZkNJw7DB53dJUnmusKdSPg138A6HGt2ilycwTUuXNDWpkXXp3YHxcslrBMdptolwbzidg== + dependencies: + workbox-background-sync "^3.6.2" + workbox-core "^3.6.2" + workbox-routing "^3.6.2" + workbox-strategies "^3.6.2" + +workbox-navigation-preload@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-3.6.2.tgz#b124606cdffe7a87c341d5da53611740ebd91ef1" + integrity sha512-fN/CWSFZiySQH/OEJQsIizAM4ob6IgZVDfWvA58jAwiyI5QziqfFtL/EiHHNvmIa5jTdcoXfuNNv1WUdpRV18A== + dependencies: + workbox-core "^3.6.2" + +workbox-precaching@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-3.6.2.tgz#409c991bbb56299a148dd0f232cdbe2a947ac917" + integrity sha512-oQmBfvCzCUfLcwTokfbVhIIcyNS9aF692EhdqAz/SB2e40ehUgcctAUhQOezsedZFqBBnwphJQUhs+hD3mu72A== + dependencies: + workbox-core "^3.6.2" + +workbox-range-requests@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-3.6.2.tgz#049a58244043b64a204d5d95917109bfc851943a" + integrity sha512-y1MFB97ydbT8PxBiihndLzG66sNIRzL0lkyoeaWPGfaPGWTP8ghMe4SkGqqdiY+E54rhd7lTdb7RZdv3Av1lTg== + dependencies: + workbox-core "^3.6.2" + +workbox-routing@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-3.6.2.tgz#a074a5291d8c4bec1af36b4ce269e7585ae59dea" + integrity sha512-rhoH1AlETUfffJXJSlc0/T5rBB6vatxpD/8IZgxgHByBnYokV+/HxO7It6wBbxIzdO31UrWVroYm0iVa5sO7Jw== + dependencies: + workbox-core "^3.6.2" + +workbox-strategies@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-3.6.2.tgz#38fac856b49f4e578b3bc66729e68a03d39a1f78" + integrity sha512-4jAyL3n0Fl1BLB3QDUoUoBTzBsE8FwH0K7He1JvLzFiDtYp1ewcKjDecYCNZyTsFVgaLL7WClEQCOKSBquBfOg== + dependencies: + workbox-core "^3.6.2" + +workbox-streams@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-3.6.2.tgz#2e1ed4eb88446fdcd11c48d4399c2fb91a9ee731" + integrity sha512-lKTh5fOAf+Qae7GHYXZve40ZXULCf9kxlkrWjTXqGcTh6cxeibuWl6Mnt4aroChNB8jOEbHfGOy0iaG0R159ew== + dependencies: + workbox-core "^3.6.2" + +workbox-sw@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-3.6.2.tgz#6b2a069baff510da4fe1b74ab6861a9c702f65e3" + integrity sha512-EwQZaeGB+tEogABMj9FaEDuszaSBQgjAUEqTFiizZWSU8owZrt0BFfi69TMAhILOfWLFh3aASMzQnPMDY7id4w== + +workbox-webpack-plugin@3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/workbox-webpack-plugin/-/workbox-webpack-plugin-3.6.2.tgz#fc94124b71e7842e09972f2fe3ec98766223d887" + integrity sha512-FGSkcaiMDM41uTGkYf7O6hf2W7UvkNc+iUIltfGiRp+qeQfXKOOh5fJCz+a6AFkeuGELSSYROsQRuOqX8LytcQ== + dependencies: + babel-runtime "^6.26.0" + json-stable-stringify "^1.0.1" + workbox-build "^3.6.2" + +worker-farm@^1.5.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0" + integrity sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ== + dependencies: + errno "~0.1.7" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" + integrity sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" + integrity sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c= + dependencies: + mkdirp "^0.5.1" + +ws@^5.2.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" + integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== + dependencies: + async-limiter "~1.0.0" + +ws@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.0.tgz#119a9dbf92c54e190ec18d10e871d55c95cf9373" + integrity sha512-H3dGVdGvW2H8bnYpIDc3u3LH8Wue3Qh+Zto6aXXFzvESkTVT6rAfKR6tR/+coaUvxs8yHtmNV0uioBF62ZGSTg== + dependencies: + async-limiter "~1.0.0" + +xml-name-validator@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== + +xmlchars@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-1.3.1.tgz#1dda035f833dbb4f86a0c28eaa6ca769214793cf" + integrity sha512-tGkGJkN8XqCod7OT+EvGYK5Z4SfDQGD30zAa58OcnAa0RRWgzUEK72tkXhsX1FZd+rgnhRxFtmO+ihkp8LHSkw== + +xregexp@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020" + integrity sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg== + +xtend@^4.0.0, xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= + +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= + +"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + +yallist@^3.0.0, yallist@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" + integrity sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k= + +yargs-parser@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== + dependencies: + camelcase "^4.1.0" + +yargs-parser@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" + integrity sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo= + dependencies: + camelcase "^3.0.0" + +yargs-parser@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" + integrity sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc= + dependencies: + camelcase "^4.1.0" + +yargs@12.0.2: + version "12.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.2.tgz#fe58234369392af33ecbef53819171eff0f5aadc" + integrity sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ== + dependencies: + cliui "^4.0.0" + decamelize "^2.0.0" + find-up "^3.0.0" + get-caller-file "^1.0.1" + os-locale "^3.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1 || ^4.0.0" + yargs-parser "^10.1.0" + +yargs@^11.0.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77" + integrity sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A== + dependencies: + cliui "^4.0.0" + decamelize "^1.1.1" + find-up "^2.1.0" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^9.0.2" + +yargs@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" + integrity sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg= + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^5.0.0" From 6001b66044b41c19c7204d5b246d2f677945dd97 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 27 Oct 2018 14:21:07 +0300 Subject: [PATCH 002/101] Add initial management API spec --- .gitignore | 2 +- maubot/management/api/spec.yaml | 319 ++++++++++++++++++++++++++++++++ 2 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 maubot/management/api/spec.yaml diff --git a/.gitignore b/.gitignore index eb9ae03..9036bb6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ pip-selfcheck.json __pycache__ *.db -*.yaml +/*.yaml !example-config.yaml logs/ diff --git a/maubot/management/api/spec.yaml b/maubot/management/api/spec.yaml new file mode 100644 index 0000000..38c88b5 --- /dev/null +++ b/maubot/management/api/spec.yaml @@ -0,0 +1,319 @@ +openapi: 3.0.0 +info: + title: Maubot Management + version: 0.1.0 + description: The API to manage a [maubot](https://github.com/maubot/maubot) instance + license: + name: GNU Affero General Public License version 3 + url: 'https://github.com/maubot/maubot/blob/master/LICENSE' +security: + - bearer: [] +servers: + - url: /_matrix/maubot/v1 + +paths: + /plugins: + get: + operationId: get_plugins + summary: Get the list of installed plugins + tags: [Plugin] + responses: + 200: + description: The list of plugins + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Plugin' + 401: + $ref: '#/components/responses/Unauthorized' + /plugins/upload: + post: + operationId: upload_plugin + summary: Upload a new plugin + tags: [Plugin] + responses: + 200: + description: Plugin uploaded and replaced current version successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Plugin' + 201: + description: New plugin uploaded successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Plugin' + 401: + $ref: '#/components/responses/Unauthorized' + 412: + description: >- + Instances of the uploaded plugin type are currently active, + therefore the plugin can't be updated + requestBody: + content: + application/zip: + schema: + type: string + format: binary + example: The plugin maubot archive (.mbp) + '/plugin/{type}': + parameters: + - name: type + in: path + description: The ID of the plugin to get + required: true + schema: + type: string + get: + operationId: get_plugin + summary: Get information about a specific plugin + tags: [Plugin] + responses: + 200: + description: Plugin found + content: + application/json: + schema: + $ref: '#/components/schemas/Plugin' + 401: + $ref: '#/components/responses/Unauthorized' + 404: + description: Plugin not found + delete: + operationId: delete_plugin + summary: Delete a plugin + tags: [Plugin] + responses: + 204: + description: Plugin deleted + 401: + $ref: '#/components/responses/Unauthorized' + 404: + description: Plugin not found + 412: + description: Instances of the uploaded plugin type exists, so the plugin can't be deleted + '/instance/{type}/{id}': + parameters: + - name: type + in: path + description: The ID of the plugin whose instance to get + required: true + schema: + type: string + - name: id + in: path + description: The ID of the instance to get + required: true + schema: + type: string + get: + operationId: get_instance + summary: Get information about a specific plugin instance + tags: [Plugin instances] + responses: + 200: + description: Plugin instance found + content: + application/json: + schema: + $ref: '#/components/schemas/PluginInstance' + 401: + $ref: '#/components/responses/Unauthorized' + 404: + description: Plugin or instance not found + delete: + operationId: delete_instance + summary: Delete a specific plugin instance + tags: [Plugin instances] + responses: + 204: + description: Plugin instance deleted + 401: + $ref: '#/components/responses/Unauthorized' + 404: + description: Plugin or instance not found + put: + operationId: update_instance + summary: Edit the details of a plugin instance + tags: [Plugin instances] + responses: + 200: + description: Plugin instance edited + 201: + description: Plugin instance created + 401: + $ref: '#/components/responses/Unauthorized' + 404: + description: Plugin or instance not found + + '/clients': + get: + operationId: get_clients + summary: Get the list of Matrix clients + tags: [Client] + responses: + 200: + description: The list of plugins + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClientList' + 401: + $ref: '#/components/responses/Unauthorized' + '/client/{user_id}': + parameters: + - name: user_id + in: path + description: The Matrix user ID of the client to get + required: true + schema: + type: string + get: + operationId: get_client + summary: Get information about a specific Matrix client + tags: [Client] + responses: + 200: + description: Client found + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClient' + 401: + $ref: '#/components/responses/Unauthorized' + 404: + description: Client not found + put: + operationId: update_client + summary: Update a Matrix client + tags: [Client] + responses: + 200: + description: Client updated + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClient' + 201: + description: Client created + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClient' + 401: + $ref: '#/components/responses/Unauthorized' + 404: + description: Client not found + delete: + operationId: delete_client + summary: Delete a Matrix client + tags: [Client] + responses: + 204: + description: Client deleted + 401: + $ref: '#/components/responses/Unauthorized' + 404: + description: Client not found + +components: + responses: + Unauthorized: + description: Invalid or missing access token + securitySchemes: + bearer: + type: http + scheme: bearer + description: Required authentication for all endpoints + schemas: + Plugin: + type: object + properties: + id: + type: string + example: xyz.maubot.jesaribot + version: + type: string + example: 2.0.0 + instances: + type: array + items: + $ref: '#/components/schemas/PluginInstance' + PluginInstance: + type: object + properties: + id: + type: string + example: jesaribot + readOnly: true + type: + type: string + example: xyz.maubot.jesaribot + enabled: + type: boolean + example: true + primary_user: + type: string + example: '@putkiteippi:maunium.net' + MatrixClientList: + type: array + items: + type: object + properties: + id: + type: string + example: '@putkiteippi:maunium.net' + homeserver: + type: string + example: 'https://maunium.net' + enabled: + type: boolean + example: true + sync: + type: boolean + example: true + autojoin: + type: boolean + example: true + displayname: + type: string + example: J. E. Saarinen + avatar_url: + type: string + example: 'mxc://maunium.net/FsPQQTntCCqhJMFtwArmJdaU' + instance_count: + type: integer + example: 1 + MatrixClient: + type: object + properties: + id: + type: string + example: '@putkiteippi:maunium.net' + homeserver: + type: string + example: 'https://maunium.net' + access_token: + type: string + enabled: + type: boolean + example: true + sync: + type: boolean + example: true + autojoin: + type: boolean + example: true + displayname: + type: string + example: J. E. Saarinen + avatar_url: + type: string + example: 'mxc://maunium.net/FsPQQTntCCqhJMFtwArmJdaU' + instances: + type: array + items: + $ref: '#/components/schemas/PluginInstance' From aefdcd94476f7cf207a09a364e50a4874ae11422 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 27 Oct 2018 19:58:35 +0300 Subject: [PATCH 003/101] Fix minor things in management API spec --- maubot/management/api/spec.yaml | 37 ++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/maubot/management/api/spec.yaml b/maubot/management/api/spec.yaml index 38c88b5..d1bf9a4 100644 --- a/maubot/management/api/spec.yaml +++ b/maubot/management/api/spec.yaml @@ -59,9 +59,9 @@ paths: type: string format: binary example: The plugin maubot archive (.mbp) - '/plugin/{type}': + '/plugin/{id}': parameters: - - name: type + - name: id in: path description: The ID of the plugin to get required: true @@ -94,15 +94,26 @@ paths: 404: description: Plugin not found 412: - description: Instances of the uploaded plugin type exists, so the plugin can't be deleted - '/instance/{type}/{id}': + description: One or more plugin instances of this type exist + + /instances: + get: + operationId: get_instances + summary: Get all plugin instances + tags: [Plugin instances] + responses: + 200: + description: The list of instances + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PluginInstance' + 401: + $ref: '#/components/responses/Unauthorized' + '/instance/{id}': parameters: - - name: type - in: path - description: The ID of the plugin whose instance to get - required: true - schema: - type: string - name: id in: path description: The ID of the instance to get @@ -137,7 +148,7 @@ paths: description: Plugin or instance not found put: operationId: update_instance - summary: Edit the details of a plugin instance + summary: Create a plugin instance or edit the details of an existing plugin instance tags: [Plugin instances] responses: 200: @@ -188,7 +199,7 @@ paths: description: Client not found put: operationId: update_client - summary: Update a Matrix client + summary: Create or update a Matrix client tags: [Client] responses: 200: @@ -218,6 +229,8 @@ paths: $ref: '#/components/responses/Unauthorized' 404: description: Client not found + 412: + description: One or more plugin instances with this as their primary client exist components: responses: From f2449e2eba448b6973237ddb3ac6ae293e651613 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 28 Oct 2018 01:31:03 +0300 Subject: [PATCH 004/101] Start working on management API implementation --- example-config.yaml | 22 +++++--- maubot/__main__.py | 42 ++++++++++++--- maubot/client.py | 7 ++- maubot/config.py | 2 +- maubot/{plugin.py => instance.py} | 33 +++++++++--- maubot/loader/__init__.py | 2 +- maubot/loader/abc.py | 19 ++++++- maubot/loader/zip.py | 66 ++++++++++++++--------- maubot/management/api/__init__.py | 46 ++++++++++++++++ maubot/management/api/middleware.py | 67 +++++++++++++++++++++++ maubot/management/api/plugin.py | 83 +++++++++++++++++++++++++++++ maubot/management/api/responses.py | 38 +++++++++++++ maubot/plugin_base.py | 2 +- maubot/server.py | 4 +- 14 files changed, 379 insertions(+), 54 deletions(-) rename maubot/{plugin.py => instance.py} (86%) create mode 100644 maubot/management/api/middleware.py create mode 100644 maubot/management/api/plugin.py create mode 100644 maubot/management/api/responses.py diff --git a/example-config.yaml b/example-config.yaml index e76e430..9bcf2bf 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -5,24 +5,30 @@ # Postgres: postgres://username:password@hostname/dbname database: sqlite:///maubot.db -# The directory where plugin databases should be stored. -plugin_db_directory: ./plugins - -# If multiple directories have a plugin with the same name, the first directory is used. plugin_directories: -- ./plugins + # The directory where uploaded new plugins should be stored. + upload: ./plugins + # The directories from which plugins should be loaded. + # Duplicate plugin IDs will be moved to the trash. + load: + - ./plugins + # The directory where old plugin versions and conflicting plugins should be moved. + # Set to "delete" to delete files immediately. + trash: ./trash + # The directory where plugin databases should be stored. + db: ./plugins server: # The IP and port to listen to. hostname: 0.0.0.0 port: 29316 # The base management API path. - base_path: /_matrix/maubot + base_path: /_matrix/maubot/v1 # The base appservice API path. Use / for legacy appservice API and /_matrix/app/v1 for v1. appservice_base_path: /_matrix/app/v1 - # The shared secret to authorize users of the API. + # The shared secret to sign API access tokens. # Set to "generate" to generate and save a new token at startup. - shared_secret: generate + unshared_secret: generate admins: - "@admin:example.com" diff --git a/maubot/__main__.py b/maubot/__main__.py index fe9f168..3c2b42e 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -14,20 +14,23 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from sqlalchemy import orm +from time import time import sqlalchemy as sql import logging.config import argparse import asyncio +import signal import copy import sys -import signal +import os from .config import Config from .db import Base, init as init_db from .server import MaubotServer from .client import Client, init as init_client -from .loader import ZippedPluginLoader -from .plugin import PluginInstance, init as init_plugin_instance_class +from .loader import ZippedPluginLoader, MaubotZipImportError, IDConflictError +from .instance import PluginInstance, init as init_plugin_instance_class +from .management.api import init as init_management from .__meta__ import __version__ parser = argparse.ArgumentParser(description="A plugin-based Matrix bot system.", @@ -57,9 +60,36 @@ loop = asyncio.get_event_loop() init_db(db_session) init_client(loop) -init_plugin_instance_class(config) -server = MaubotServer(config, loop) -ZippedPluginLoader.load_all(*config["plugin_directories"]) +init_plugin_instance_class(db_session, config) +management_api = init_management(config, loop) +server = MaubotServer(config, management_api, loop) + +trash_path = config["plugin_directories.trash"] + + +def trash(file_path: str, new_name: Optional[str] = None) -> None: + if trash_path == "delete": + os.remove(file_path) + else: + new_name = new_name or f"{int(time())}-{os.path.basename(file_path)}" + os.rename(file_path, os.path.abspath(os.path.join(trash_path, new_name))) + + +ZippedPluginLoader.log.debug("Preloading plugins...") +for directory in config["plugin_directories.load"]: + for file in os.listdir(directory): + if not file.endswith(".mbp"): + continue + path = os.path.abspath(os.path.join(directory, file)) + try: + ZippedPluginLoader.get(path) + except MaubotZipImportError: + ZippedPluginLoader.log.exception(f"Failed to load plugin at {path}, trashing...") + trash(path) + except IDConflictError: + ZippedPluginLoader.log.warn(f"Duplicate plugin ID at {path}, trashing...") + trash(path) + plugins = PluginInstance.all() for plugin in plugins: diff --git a/maubot/client.py b/maubot/client.py index 807e7a2..e96c918 100644 --- a/maubot/client.py +++ b/maubot/client.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Set, TYPE_CHECKING from aiohttp import ClientSession import asyncio import logging @@ -24,6 +24,9 @@ from mautrix.types import (UserID, SyncToken, FilterID, ContentURI, StrippedStat from .db import DBClient from .matrix import MaubotMatrixClient +if TYPE_CHECKING: + from .instance import PluginInstance + log = logging.getLogger("maubot.client") @@ -32,6 +35,7 @@ class Client: cache: Dict[UserID, 'Client'] = {} http_client: ClientSession = None + references: Set['PluginInstance'] db_instance: DBClient client: MaubotMatrixClient @@ -39,6 +43,7 @@ class Client: self.db_instance = db_instance self.cache[self.id] = self self.log = log.getChild(self.id) + self.references = set() self.client = MaubotMatrixClient(mxid=self.id, base_url=self.homeserver, token=self.access_token, client_session=self.http_client, log=self.log, loop=self.loop, store=self.db_instance) diff --git a/maubot/config.py b/maubot/config.py index 6858482..699003c 100644 --- a/maubot/config.py +++ b/maubot/config.py @@ -16,7 +16,7 @@ import random import string -from mautrix.util import BaseFileConfig, ConfigUpdateHelper +from mautrix.util.config import BaseFileConfig, ConfigUpdateHelper class Config(BaseFileConfig): diff --git a/maubot/plugin.py b/maubot/instance.py similarity index 86% rename from maubot/plugin.py rename to maubot/instance.py index 65875ef..0438713 100644 --- a/maubot/plugin.py +++ b/maubot/instance.py @@ -14,12 +14,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from typing import Dict, List, Optional +from sqlalchemy.orm import Session from ruamel.yaml.comments import CommentedMap from ruamel.yaml import YAML import logging import io -from mautrix.util import BaseProxyConfig, RecursiveDict +from mautrix.util.config import BaseProxyConfig, RecursiveDict from mautrix.types import UserID from .db import DBPlugin @@ -35,6 +36,7 @@ yaml.indent(4) class PluginInstance: + db: Session = None mb_config: Config = None cache: Dict[str, 'PluginInstance'] = {} plugin_directories: List[str] = [] @@ -44,13 +46,24 @@ class PluginInstance: client: Client plugin: Plugin config: BaseProxyConfig + running: bool def __init__(self, db_instance: DBPlugin): self.db_instance = db_instance self.log = logging.getLogger(f"maubot.plugin.{self.id}") self.config = None + self.running = False self.cache[self.id] = self + def to_dict(self) -> dict: + return { + "id": self.id, + "type": self.type, + "enabled": self.enabled, + "running": self.running, + "primary_user": self.primary_user, + } + def load(self) -> None: try: self.loader = PluginLoader.find(self.type) @@ -63,6 +76,13 @@ class PluginInstance: self.log.error(f"Failed to get client for user {self.primary_user}") self.enabled = False self.log.debug("Plugin instance dependencies loaded") + self.loader.references.add(self) + self.client.references.add(self) + + def delete(self) -> None: + self.loader.references.remove(self) + self.db.delete(self.db_instance) + # TODO delete plugin db def load_config(self) -> CommentedMap: return yaml.load(self.db_instance.config) @@ -90,14 +110,14 @@ class PluginInstance: self.save_config) self.plugin = cls(self.client.client, self.id, self.log, self.config, self.mb_config["plugin_db_directory"]) - self.loader.references |= {self} await self.plugin.start() + self.running = True self.log.info(f"Started instance of {self.loader.id} v{self.loader.version} " f"with user {self.client.id}") async def stop(self) -> None: self.log.debug("Stopping plugin instance...") - self.loader.references -= {self} + self.running = False await self.plugin.stop() self.plugin = None @@ -130,10 +150,6 @@ class PluginInstance: def type(self) -> str: return self.db_instance.type - @type.setter - def type(self, value: str) -> None: - self.db_instance.type = value - @property def enabled(self) -> bool: return self.db_instance.enabled @@ -153,5 +169,6 @@ class PluginInstance: # endregion -def init(config: Config): +def init(db: Session, config: Config): + PluginInstance.db = db PluginInstance.mb_config = config diff --git a/maubot/loader/__init__.py b/maubot/loader/__init__.py index 304cc57..e2a356e 100644 --- a/maubot/loader/__init__.py +++ b/maubot/loader/__init__.py @@ -1,2 +1,2 @@ -from .abc import PluginLoader, PluginClass +from .abc import PluginLoader, PluginClass, IDConflictError from .zip import ZippedPluginLoader, MaubotZipImportError diff --git a/maubot/loader/abc.py b/maubot/loader/abc.py index f41d848..6308a27 100644 --- a/maubot/loader/abc.py +++ b/maubot/loader/abc.py @@ -15,11 +15,12 @@ # along with this program. If not, see . from typing import TypeVar, Type, Dict, Set, TYPE_CHECKING from abc import ABC, abstractmethod +import asyncio from ..plugin_base import Plugin if TYPE_CHECKING: - from ..plugin import PluginInstance + from ..instance import PluginInstance PluginClass = TypeVar("PluginClass", bound=Plugin) @@ -42,6 +43,12 @@ class PluginLoader(ABC): def find(cls, plugin_id: str) -> 'PluginLoader': return cls.id_cache[plugin_id] + def to_dict(self) -> dict: + return { + "id": self.id, + "version": self.version, + } + @property @abstractmethod def source(self) -> str: @@ -51,6 +58,12 @@ class PluginLoader(ABC): def read_file(self, path: str) -> bytes: pass + async def stop_instances(self) -> None: + await asyncio.gather([instance.stop() for instance in self.references if instance.running]) + + async def start_instances(self) -> None: + await asyncio.gather([instance.start() for instance in self.references if instance.enabled]) + @abstractmethod def load(self) -> Type[PluginClass]: pass @@ -62,3 +75,7 @@ class PluginLoader(ABC): @abstractmethod def unload(self) -> None: pass + + @abstractmethod + def delete(self) -> None: + pass diff --git a/maubot/loader/zip.py b/maubot/loader/zip.py index a6a107e..7630c9e 100644 --- a/maubot/loader/zip.py +++ b/maubot/loader/zip.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, List, Type +from typing import Dict, List, Type, Tuple from zipfile import ZipFile, BadZipFile import configparser import logging @@ -60,8 +60,15 @@ class ZippedPluginLoader(PluginLoader): self.id_cache[self.id] = self self.log.debug(f"Preloaded plugin {self.id} from {self.path}") + def to_dict(self) -> dict: + return { + **super().to_dict(), + "path": self.path + } + @classmethod def get(cls, path: str) -> 'ZippedPluginLoader': + path = os.path.abspath(path) try: return cls.path_cache[path] except KeyError: @@ -80,10 +87,11 @@ class ZippedPluginLoader(PluginLoader): def read_file(self, path: str) -> bytes: return self._file.read(path) - def _load_meta(self) -> None: + @staticmethod + def _open_meta(source) -> Tuple[ZipFile, configparser.ConfigParser]: try: - self._file = ZipFile(self.path) - data = self._file.read("maubot.ini") + file = ZipFile(source) + data = file.read("maubot.ini") except FileNotFoundError as e: raise MaubotZipImportError("Maubot plugin not found") from e except BadZipFile as e: @@ -92,7 +100,14 @@ class ZippedPluginLoader(PluginLoader): raise MaubotZipImportError("File does not contain a maubot plugin definition") from e config = configparser.ConfigParser() try: - config.read_string(data.decode("utf-8"), source=f"{self.path}/maubot.ini") + config.read_string(data.decode("utf-8")) + except (configparser.Error, KeyError, IndexError, ValueError) as e: + raise MaubotZipImportError("Maubot plugin definition in file is invalid") from e + return file, config + + @classmethod + def _read_meta(cls, config: configparser.ConfigParser) -> Tuple[str, str, List[str], str, str]: + try: meta = config["maubot"] meta_id = meta["ID"] version = meta["Version"] @@ -103,10 +118,21 @@ class ZippedPluginLoader(PluginLoader): main_module, main_class = main_class.split("/")[:2] except (configparser.Error, KeyError, IndexError, ValueError) as e: raise MaubotZipImportError("Maubot plugin definition in file is invalid") from e - if self.id and meta_id != self.id: + return meta_id, version, modules, main_class, main_module + + @classmethod + def verify_meta(cls, source) -> Tuple[str, str]: + _, config = cls._open_meta(source) + meta = cls._read_meta(config) + return meta[0], meta[1] + + def _load_meta(self) -> None: + file, config = self._open_meta(self.path) + meta = self._read_meta(config) + if self.id and meta[0] != self.id: raise MaubotZipImportError("Maubot plugin ID changed during reload") - self.id, self.version, self.modules = meta_id, version, modules - self.main_class, self.main_module = main_class, main_module + self.id, self.version, self.modules, self.main_class, self.main_module = meta + self._file = file def _get_importer(self, reset_cache: bool = False) -> zipimporter: try: @@ -161,7 +187,7 @@ class ZippedPluginLoader(PluginLoader): self._loaded = None self.log.debug(f"Unloaded plugin {self.id} at {self.path}") - def destroy(self) -> None: + def delete(self) -> None: self.unload() try: del self.path_cache[self.path] @@ -171,24 +197,12 @@ class ZippedPluginLoader(PluginLoader): del self.id_cache[self.id] except KeyError: pass - self.id = None - self.path = None - self.version = None - self.modules = None if self._importer: self._importer.remove_cache() self._importer = None self._loaded = None - - @classmethod - def load_all(cls, *args: str) -> None: - cls.log.debug("Preloading plugins...") - for directory in args: - for file in os.listdir(directory): - if not file.endswith(".mbp"): - continue - path = os.path.join(directory, file) - try: - ZippedPluginLoader.get(path) - except (MaubotZipImportError, IDConflictError): - cls.log.exception(f"Failed to load plugin at {path}") + os.remove(self.path) + self.id = None + self.path = None + self.version = None + self.modules = None diff --git a/maubot/management/api/__init__.py b/maubot/management/api/__init__.py index e69de29..dc08a5f 100644 --- a/maubot/management/api/__init__.py +++ b/maubot/management/api/__init__.py @@ -0,0 +1,46 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from aiohttp import web +from asyncio import AbstractEventLoop + +from mautrix.types import UserID +from mautrix.util.signed_token import sign_token, verify_token + +from ...config import Config + +routes = web.RouteTableDef() +config: Config = None + + +def is_valid_token(token: str) -> bool: + data = verify_token(config["server.unshared_secret"], token) + user_id = data.get("user_id", None) + return user_id is not None and user_id in config["admins"] + + +def create_token(user: UserID) -> str: + return sign_token(config["server.unshared_secret"], { + "user_id": user, + }) + + +def init(cfg: Config, loop: AbstractEventLoop) -> web.Application: + global config + config = cfg + from .middleware import auth, error, log + app = web.Application(loop=loop, middlewares=[auth, log, error]) + app.add_routes(routes) + return app diff --git a/maubot/management/api/middleware.py b/maubot/management/api/middleware.py new file mode 100644 index 0000000..f1b76ad --- /dev/null +++ b/maubot/management/api/middleware.py @@ -0,0 +1,67 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import Callable, Awaitable +from aiohttp import web +import logging + +from .responses import ErrNoToken, ErrInvalidToken +from . import is_valid_token + +Handler = Callable[[web.Request], Awaitable[web.Response]] + +req_log = logging.getLogger("maubot.mgmt.request") +resp_log = logging.getLogger("maubot.mgmt.response") + + +@web.middleware +async def auth(request: web.Request, handler: Handler) -> web.Response: + token = request.headers.get("Authorization", "") + if not token or not token.startswith("Bearer "): + req_log.debug(f"Request missing auth: {request.remote} {request.method} {request.path}") + return ErrNoToken + if not is_valid_token(token[len("Bearer "):]): + req_log.debug(f"Request invalid auth: {request.remote} {request.method} {request.path}") + return ErrInvalidToken + return await handler(request) + + +@web.middleware +async def error(request: web.Request, handler: Handler) -> web.Response: + try: + return await handler(request) + except web.HTTPException as ex: + return web.json_response({ + "error": f"Unhandled HTTP {ex.status}", + "errcode": f"unhandled_http_{ex.status}", + }, status=ex.status) + + +req_no = 0 + + +def get_req_no(): + global req_no + req_no += 1 + return req_no + + +@web.middleware +async def log(request: web.Request, handler: Handler) -> web.Response: + local_req_no = get_req_no() + req_log.info(f"Request {local_req_no}: {request.remote} {request.method} {request.path}") + resp = await handler(request) + resp_log.info(f"Responded to {local_req_no} from {request.remote}: {resp}") + return resp diff --git a/maubot/management/api/plugin.py b/maubot/management/api/plugin.py new file mode 100644 index 0000000..c85f88b --- /dev/null +++ b/maubot/management/api/plugin.py @@ -0,0 +1,83 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from aiohttp import web +from io import BytesIO +import os.path + +from ...loader import PluginLoader, ZippedPluginLoader, MaubotZipImportError +from .responses import ErrPluginNotFound, ErrPluginInUse, RespDeleted +from . import routes, config + + +def _plugin_to_dict(plugin: PluginLoader) -> dict: + return { + **plugin.to_dict(), + "instances": [instance.to_dict() for instance in plugin.references] + } + + +@routes.get("/plugins") +async def get_plugins(_) -> web.Response: + return web.json_response([_plugin_to_dict(plugin) for plugin in PluginLoader.id_cache.values()]) + + +@routes.get("/plugin/{id}") +async def get_plugin(request: web.Request) -> web.Response: + plugin_id = request.match_info.get("id", None) + plugin = PluginLoader.id_cache.get(plugin_id, None) + if not plugin: + return ErrPluginNotFound + return web.json_response(_plugin_to_dict(plugin)) + + +@routes.delete("/plugin/{id}") +async def delete_plugin(request: web.Request) -> web.Response: + plugin_id = request.match_info.get("id", None) + plugin = PluginLoader.id_cache.get(plugin_id, None) + if not plugin: + return ErrPluginNotFound + elif len(plugin.references) > 0: + return ErrPluginInUse + plugin.delete() + return RespDeleted + + +@routes.post("/plugins/upload") +async def upload_plugin(request: web.Request) -> web.Response: + content = await request.read() + file = BytesIO(content) + try: + pid, version = ZippedPluginLoader.verify_meta(file) + except MaubotZipImportError as e: + return web.json_response({ + "error": str(e), + "errcode": "invalid_plugin", + }, status=web.HTTPBadRequest) + plugin = PluginLoader.id_cache.get(pid, None) + if not plugin: + path = os.path.join(config["plugin_directories.upload"], f"{pid}-{version}.mbp") + with open(path, "wb") as p: + p.write(content) + try: + ZippedPluginLoader.get(path) + except MaubotZipImportError as e: + trash(path) + return web.json_response({ + "error": str(e), + "errcode": "invalid_plugin", + }, status=web.HTTPBadRequest) + else: + pass diff --git a/maubot/management/api/responses.py b/maubot/management/api/responses.py new file mode 100644 index 0000000..bab2b01 --- /dev/null +++ b/maubot/management/api/responses.py @@ -0,0 +1,38 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from aiohttp import web + +ErrNoToken = web.json_response({ + "error": "Authorization token missing", + "errcode": "auth_token_missing", +}, status=web.HTTPUnauthorized) + +ErrInvalidToken = web.json_response({ + "error": "Invalid authorization token", + "errcode": "auth_token_invalid", +}, status=web.HTTPUnauthorized) + +ErrPluginNotFound = web.json_response({ + "error": "Plugin not found", + "errcode": "plugin_not_found", +}, status=web.HTTPNotFound) + +ErrPluginInUse = web.json_response({ + "error": "Plugin instances of this type still exist", + "errcode": "plugin_in_use", +}) + +RespDeleted = web.Response(status=204) diff --git a/maubot/plugin_base.py b/maubot/plugin_base.py index b28ab4a..08d1211 100644 --- a/maubot/plugin_base.py +++ b/maubot/plugin_base.py @@ -24,7 +24,7 @@ import sqlalchemy as sql if TYPE_CHECKING: from .client import MaubotMatrixClient from .command_spec import CommandSpec - from mautrix.util import BaseProxyConfig + from mautrix.util.config import BaseProxyConfig class Plugin(ABC): diff --git a/maubot/server.py b/maubot/server.py index 7a666ec..4bd7bd2 100644 --- a/maubot/server.py +++ b/maubot/server.py @@ -23,13 +23,15 @@ from .__meta__ import __version__ class MaubotServer: - def __init__(self, config: Config, loop: asyncio.AbstractEventLoop): + def __init__(self, config: Config, management: web.Application, + loop: asyncio.AbstractEventLoop) -> None: self.loop = loop or asyncio.get_event_loop() self.app = web.Application(loop=self.loop) self.config = config path = PathBuilder(config["server.base_path"]) self.add_route(Method.GET, path.version, self.version) + self.app.add_subapp(config["server.base_path"], management) as_path = PathBuilder(config["server.appservice_base_path"]) self.add_route(Method.PUT, as_path.transactions, self.handle_transaction) From d5353430a808d65943a393efac361c6225382b08 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 30 Oct 2018 00:50:38 +0200 Subject: [PATCH 005/101] More plugin API stuff --- maubot/__main__.py | 30 +++------------- maubot/loader/abc.py | 10 +++--- maubot/loader/zip.py | 55 ++++++++++++++++++++++++------ maubot/management/api/plugin.py | 54 ++++++++++++++++++++++------- maubot/management/api/responses.py | 28 +++++++++++++-- maubot/management/api/spec.yaml | 43 ++++++++++++++++++----- 6 files changed, 156 insertions(+), 64 deletions(-) diff --git a/maubot/__main__.py b/maubot/__main__.py index 3c2b42e..dee04bc 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -28,7 +28,7 @@ from .config import Config from .db import Base, init as init_db from .server import MaubotServer from .client import Client, init as init_client -from .loader import ZippedPluginLoader, MaubotZipImportError, IDConflictError +from .loader import ZippedPluginLoader from .instance import PluginInstance, init as init_plugin_instance_class from .management.api import init as init_management from .__meta__ import __version__ @@ -64,31 +64,9 @@ init_plugin_instance_class(db_session, config) management_api = init_management(config, loop) server = MaubotServer(config, management_api, loop) -trash_path = config["plugin_directories.trash"] - - -def trash(file_path: str, new_name: Optional[str] = None) -> None: - if trash_path == "delete": - os.remove(file_path) - else: - new_name = new_name or f"{int(time())}-{os.path.basename(file_path)}" - os.rename(file_path, os.path.abspath(os.path.join(trash_path, new_name))) - - -ZippedPluginLoader.log.debug("Preloading plugins...") -for directory in config["plugin_directories.load"]: - for file in os.listdir(directory): - if not file.endswith(".mbp"): - continue - path = os.path.abspath(os.path.join(directory, file)) - try: - ZippedPluginLoader.get(path) - except MaubotZipImportError: - ZippedPluginLoader.log.exception(f"Failed to load plugin at {path}, trashing...") - trash(path) - except IDConflictError: - ZippedPluginLoader.log.warn(f"Duplicate plugin ID at {path}, trashing...") - trash(path) +ZippedPluginLoader.trash_path = config["plugin_directories.trash"] +ZippedPluginLoader.directories = config["plugin_directories.load"] +ZippedPluginLoader.load_all() plugins = PluginInstance.all() diff --git a/maubot/loader/abc.py b/maubot/loader/abc.py index 6308a27..23a1de5 100644 --- a/maubot/loader/abc.py +++ b/maubot/loader/abc.py @@ -55,7 +55,7 @@ class PluginLoader(ABC): pass @abstractmethod - def read_file(self, path: str) -> bytes: + async def read_file(self, path: str) -> bytes: pass async def stop_instances(self) -> None: @@ -65,17 +65,17 @@ class PluginLoader(ABC): await asyncio.gather([instance.start() for instance in self.references if instance.enabled]) @abstractmethod - def load(self) -> Type[PluginClass]: + async def load(self) -> Type[PluginClass]: pass @abstractmethod - def reload(self) -> Type[PluginClass]: + async def reload(self) -> Type[PluginClass]: pass @abstractmethod - def unload(self) -> None: + async def unload(self) -> None: pass @abstractmethod - def delete(self) -> None: + async def delete(self) -> None: pass diff --git a/maubot/loader/zip.py b/maubot/loader/zip.py index 7630c9e..48fafce 100644 --- a/maubot/loader/zip.py +++ b/maubot/loader/zip.py @@ -13,8 +13,9 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, List, Type, Tuple +from typing import Dict, List, Type, Tuple, Optional from zipfile import ZipFile, BadZipFile +from time import time import configparser import logging import sys @@ -31,7 +32,9 @@ class MaubotZipImportError(Exception): class ZippedPluginLoader(PluginLoader): path_cache: Dict[str, 'ZippedPluginLoader'] = {} - log = logging.getLogger("maubot.loader.zip") + log: logging.Logger = logging.getLogger("maubot.loader.zip") + trash_path: str = "delete" + directories: List[str] = [] path: str id: str @@ -84,7 +87,7 @@ class ZippedPluginLoader(PluginLoader): f"id='{self.id}' " f"loaded={self._loaded is not None}>") - def read_file(self, path: str) -> bytes: + async def read_file(self, path: str) -> bytes: return self._file.read(path) @staticmethod @@ -159,7 +162,14 @@ class ZippedPluginLoader(PluginLoader): except ZipImportError as e: raise MaubotZipImportError(f"Module {module} not found in file") from e - def load(self, reset_cache: bool = False) -> Type[PluginClass]: + async def load(self, reset_cache: bool = False) -> Type[PluginClass]: + try: + return self._load(reset_cache) + except MaubotZipImportError: + self.log.exception(f"Failed to load {self.id} v{self.version}") + raise + + def _load(self, reset_cache: bool = False) -> Type[PluginClass]: if self._loaded is not None and not reset_cache: return self._loaded importer = self._get_importer(reset_cache=reset_cache) @@ -176,19 +186,19 @@ class ZippedPluginLoader(PluginLoader): self.log.debug(f"Loaded and imported plugin {self.id} from {self.path}") return plugin - def reload(self) -> Type[PluginClass]: - self.unload() - return self.load(reset_cache=True) + async def reload(self) -> Type[PluginClass]: + await self.unload() + return await self.load(reset_cache=True) - def unload(self) -> None: + async def unload(self) -> None: for name, mod in list(sys.modules.items()): if getattr(mod, "__file__", "").startswith(self.path): del sys.modules[name] self._loaded = None self.log.debug(f"Unloaded plugin {self.id} at {self.path}") - def delete(self) -> None: - self.unload() + async def delete(self) -> None: + await self.unload() try: del self.path_cache[self.path] except KeyError: @@ -206,3 +216,28 @@ class ZippedPluginLoader(PluginLoader): self.path = None self.version = None self.modules = None + + @classmethod + def trash(cls, file_path: str, new_name: Optional[str] = None, reason: str = "error") -> None: + if cls.trash_path == "delete": + os.remove(file_path) + else: + new_name = new_name or f"{int(time())}-{reason}-{os.path.basename(file_path)}" + os.rename(file_path, os.path.abspath(os.path.join(cls.trash_path, new_name))) + + @classmethod + def load_all(cls): + cls.log.debug("Preloading plugins...") + for directory in cls.directories: + for file in os.listdir(directory): + if not file.endswith(".mbp"): + continue + path = os.path.abspath(os.path.join(directory, file)) + try: + cls.get(path) + except MaubotZipImportError: + cls.log.exception(f"Failed to load plugin at {path}, trashing...") + cls.trash(path) + except IDConflictError: + cls.log.error(f"Duplicate plugin ID at {path}, trashing...") + cls.trash(path) diff --git a/maubot/management/api/plugin.py b/maubot/management/api/plugin.py index c85f88b..6e67c06 100644 --- a/maubot/management/api/plugin.py +++ b/maubot/management/api/plugin.py @@ -18,7 +18,8 @@ from io import BytesIO import os.path from ...loader import PluginLoader, ZippedPluginLoader, MaubotZipImportError -from .responses import ErrPluginNotFound, ErrPluginInUse, RespDeleted +from .responses import (ErrPluginNotFound, ErrPluginInUse, ErrInputPluginInvalid, + ErrPluginReloadFailed, RespDeleted, RespOK) from . import routes, config @@ -51,10 +52,19 @@ async def delete_plugin(request: web.Request) -> web.Response: return ErrPluginNotFound elif len(plugin.references) > 0: return ErrPluginInUse - plugin.delete() + await plugin.delete() return RespDeleted +@routes.post("/plugin/{id}/reload") +async def reload_plugin(request: web.Request) -> web.Response: + plugin_id = request.match_info.get("id", None) + plugin = PluginLoader.id_cache.get(plugin_id, None) + if not plugin: + return ErrPluginNotFound + return await reload(plugin) + + @routes.post("/plugins/upload") async def upload_plugin(request: web.Request) -> web.Response: content = await request.read() @@ -62,22 +72,40 @@ async def upload_plugin(request: web.Request) -> web.Response: try: pid, version = ZippedPluginLoader.verify_meta(file) except MaubotZipImportError as e: - return web.json_response({ - "error": str(e), - "errcode": "invalid_plugin", - }, status=web.HTTPBadRequest) + return ErrInputPluginInvalid(e) plugin = PluginLoader.id_cache.get(pid, None) if not plugin: - path = os.path.join(config["plugin_directories.upload"], f"{pid}-{version}.mbp") + path = os.path.join(config["plugin_directories.upload"], f"{pid}-v{version}.mbp") with open(path, "wb") as p: p.write(content) try: ZippedPluginLoader.get(path) except MaubotZipImportError as e: - trash(path) - return web.json_response({ - "error": str(e), - "errcode": "invalid_plugin", - }, status=web.HTTPBadRequest) + ZippedPluginLoader.trash(path) + # TODO log error? + return ErrInputPluginInvalid(e) + elif isinstance(plugin, ZippedPluginLoader): + dirname = os.path.dirname(plugin.path) + filename = os.path.basename(plugin.path) + if plugin.version in filename: + filename = filename.replace(plugin.version, version) + else: + filename = filename.rstrip(".mbp") + version + ".mbp" + path = os.path.join(dirname, filename) + with open(path, "wb") as p: + p.write(content) + ZippedPluginLoader.trash(plugin.path, reason="update") + plugin.path = path + return await reload(plugin) else: - pass + return web.json_response({}) + + +async def reload(plugin: PluginLoader) -> web.Response: + await plugin.stop_instances() + try: + await plugin.reload() + except MaubotZipImportError as e: + return ErrPluginReloadFailed(e) + await plugin.start_instances() + return RespOK diff --git a/maubot/management/api/responses.py b/maubot/management/api/responses.py index bab2b01..993a354 100644 --- a/maubot/management/api/responses.py +++ b/maubot/management/api/responses.py @@ -33,6 +33,30 @@ ErrPluginNotFound = web.json_response({ ErrPluginInUse = web.json_response({ "error": "Plugin instances of this type still exist", "errcode": "plugin_in_use", -}) +}, status=web.HTTPPreconditionFailed) -RespDeleted = web.Response(status=204) + +def ErrInputPluginInvalid(error) -> web.Response: + return web.json_response({ + "error": str(error), + "errcode": "plugin_invalid", + }, status=web.HTTPBadRequest) + + +def ErrPluginReloadFailed(error) -> web.Response: + return web.json_response({ + "error": str(error), + "errcode": "plugin_invalid", + }, status=web.HTTPInternalServerError) + + +ErrNotImplemented = web.json_response({ + "error": "Not implemented", + "errcode": "not_implemented", +}, status=web.HTTPNotImplemented) + +RespOK = web.json_response({ + "success": True, +}, status=web.HTTPOk) + +RespDeleted = web.Response(status=web.HTTPNoContent) diff --git a/maubot/management/api/spec.yaml b/maubot/management/api/spec.yaml index d1bf9a4..4389f23 100644 --- a/maubot/management/api/spec.yaml +++ b/maubot/management/api/spec.yaml @@ -32,6 +32,7 @@ paths: post: operationId: upload_plugin summary: Upload a new plugin + description: Upload a new plugin. If the plugin already exists, enabled instances will be restarted. tags: [Plugin] responses: 200: @@ -81,10 +82,11 @@ paths: 401: $ref: '#/components/responses/Unauthorized' 404: - description: Plugin not found + $ref: '#/components/responses/PluginNotFound' delete: operationId: delete_plugin summary: Delete a plugin + description: Delete a plugin. All instances of the plugin must be deleted before deleting the plugin. tags: [Plugin] responses: 204: @@ -92,9 +94,28 @@ paths: 401: $ref: '#/components/responses/Unauthorized' 404: - description: Plugin not found + $ref: '#/components/responses/PluginNotFound' 412: description: One or more plugin instances of this type exist + /plugin/{id}/reload: + parameters: + - name: id + in: path + description: The ID of the plugin to get + required: true + schema: + type: string + post: + operationId: reload_plugin + summary: Reload a plugin from disk + tags: [Plugin] + responses: + 200: + description: Plugin reloaded + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/PluginNotFound' /instances: get: @@ -134,7 +155,7 @@ paths: 401: $ref: '#/components/responses/Unauthorized' 404: - description: Plugin or instance not found + $ref: '#/components/responses/InstanceNotFound' delete: operationId: delete_instance summary: Delete a specific plugin instance @@ -145,7 +166,7 @@ paths: 401: $ref: '#/components/responses/Unauthorized' 404: - description: Plugin or instance not found + $ref: '#/components/responses/InstanceNotFound' put: operationId: update_instance summary: Create a plugin instance or edit the details of an existing plugin instance @@ -158,7 +179,7 @@ paths: 401: $ref: '#/components/responses/Unauthorized' 404: - description: Plugin or instance not found + $ref: '#/components/responses/InstanceNotFound' '/clients': get: @@ -196,7 +217,7 @@ paths: 401: $ref: '#/components/responses/Unauthorized' 404: - description: Client not found + $ref: '#/components/responses/ClientNotFound' put: operationId: update_client summary: Create or update a Matrix client @@ -217,7 +238,7 @@ paths: 401: $ref: '#/components/responses/Unauthorized' 404: - description: Client not found + $ref: '#/components/responses/ClientNotFound' delete: operationId: delete_client summary: Delete a Matrix client @@ -228,7 +249,7 @@ paths: 401: $ref: '#/components/responses/Unauthorized' 404: - description: Client not found + $ref: '#/components/responses/ClientNotFound' 412: description: One or more plugin instances with this as their primary client exist @@ -236,6 +257,12 @@ components: responses: Unauthorized: description: Invalid or missing access token + PluginNotFound: + description: Plugin not found + ClientNotFound: + description: Client not found + InstanceNotFound: + description: Plugin instance not found securitySchemes: bearer: type: http From 91f804d4cf13efb9c81ca911596bade6b9818c52 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 30 Oct 2018 21:29:58 +0200 Subject: [PATCH 006/101] Allow commands for all non-notice message types --- maubot/matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maubot/matrix.py b/maubot/matrix.py index 5f0150d..1ac9167 100644 --- a/maubot/matrix.py +++ b/maubot/matrix.py @@ -84,7 +84,7 @@ class MaubotMatrixClient(MatrixClient): pass async def _command_event_handler(self, evt: MessageEvent) -> None: - if evt.sender == self.mxid or evt.content.msgtype != MessageType.TEXT: + if evt.sender == self.mxid or evt.content.msgtype == MessageType.NOTICE: return for command in self.commands: if command.match(evt): From a2ba9b245eeb6acab4d155eecd43392aed64c806 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 30 Oct 2018 21:40:41 +0200 Subject: [PATCH 007/101] Update mautrix.util imports --- maubot/config.py | 2 +- maubot/plugin.py | 2 +- maubot/plugin_base.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/maubot/config.py b/maubot/config.py index 6858482..699003c 100644 --- a/maubot/config.py +++ b/maubot/config.py @@ -16,7 +16,7 @@ import random import string -from mautrix.util import BaseFileConfig, ConfigUpdateHelper +from mautrix.util.config import BaseFileConfig, ConfigUpdateHelper class Config(BaseFileConfig): diff --git a/maubot/plugin.py b/maubot/plugin.py index 65875ef..facb0bf 100644 --- a/maubot/plugin.py +++ b/maubot/plugin.py @@ -19,7 +19,7 @@ from ruamel.yaml import YAML import logging import io -from mautrix.util import BaseProxyConfig, RecursiveDict +from mautrix.util.config import BaseProxyConfig, RecursiveDict from mautrix.types import UserID from .db import DBPlugin diff --git a/maubot/plugin_base.py b/maubot/plugin_base.py index b28ab4a..08d1211 100644 --- a/maubot/plugin_base.py +++ b/maubot/plugin_base.py @@ -24,7 +24,7 @@ import sqlalchemy as sql if TYPE_CHECKING: from .client import MaubotMatrixClient from .command_spec import CommandSpec - from mautrix.util import BaseProxyConfig + from mautrix.util.config import BaseProxyConfig class Plugin(ABC): From 26c4fae4645c0636fa43f439542a9e9ada9a20e6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 30 Oct 2018 21:42:21 +0200 Subject: [PATCH 008/101] Update version --- maubot/__meta__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maubot/__meta__.py b/maubot/__meta__.py index 364550b..32cb26d 100644 --- a/maubot/__meta__.py +++ b/maubot/__meta__.py @@ -1 +1 @@ -__version__ = "0.1.0.dev4" +__version__ = "0.1.0.dev5" From 73213040f10e201c001324496548f2ad017003ab Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 30 Oct 2018 22:20:10 +0200 Subject: [PATCH 009/101] Update list of plugins --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 64bfe07..97e4b33 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,16 @@ Matrix room: [#maubot:maunium.net](https://matrix.to/#/#maubot:maunium.net) * [sed](https://github.com/maubot/sed) - A bot to do sed-like replacements. * [factorial](https://github.com/maubot/factorial) - A bot to calculate unexpected factorials. * [dictionary](https://github.com/maubot/dictionary) - A bot that provides dictionary definitions for words. +* [media](https://github.com/maubot/media) - A bot that replies with the MXC URI of images you send it. +* [dice](https://github.com/maubot/dice) - A combined dice rolling and calculator bot. +* [karma](https://github.com/maubot/karma) - A user karma tracker bot. + +### Upcoming +* rss - A bot that posts new RSS entries to rooms. +* dictionary - A bot to get the dictionary definitions of words. +* poll - A simple poll bot. +* echo - A very simple echo bot. +* reminder - A bot to ping you about something after a certain amount of time. +* github - A GitHub client and webhook receiver bot. +* wolfram - A Wolfram Alpha bot +* gitlab - A GitLab client and webhook receiver bot. From d7f072aeff8e15445952101e2aa16212e6858a23 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 31 Oct 2018 00:27:50 +0200 Subject: [PATCH 010/101] Even more plugin API stuff --- maubot/loader/zip.py | 51 ++++++++++++----- maubot/management/api/plugin.py | 92 ++++++++++++++++++------------ maubot/management/api/responses.py | 17 ++++-- 3 files changed, 104 insertions(+), 56 deletions(-) diff --git a/maubot/loader/zip.py b/maubot/loader/zip.py index 48fafce..d18894f 100644 --- a/maubot/loader/zip.py +++ b/maubot/loader/zip.py @@ -30,6 +30,18 @@ class MaubotZipImportError(Exception): pass +class MaubotZipMetaError(MaubotZipImportError): + pass + + +class MaubotZipPreLoadError(MaubotZipImportError): + pass + + +class MaubotZipLoadError(MaubotZipImportError): + pass + + class ZippedPluginLoader(PluginLoader): path_cache: Dict[str, 'ZippedPluginLoader'] = {} log: logging.Logger = logging.getLogger("maubot.loader.zip") @@ -96,16 +108,16 @@ class ZippedPluginLoader(PluginLoader): file = ZipFile(source) data = file.read("maubot.ini") except FileNotFoundError as e: - raise MaubotZipImportError("Maubot plugin not found") from e + raise MaubotZipMetaError("Maubot plugin not found") from e except BadZipFile as e: - raise MaubotZipImportError("File is not a maubot plugin") from e + raise MaubotZipMetaError("File is not a maubot plugin") from e except KeyError as e: - raise MaubotZipImportError("File does not contain a maubot plugin definition") from e + raise MaubotZipMetaError("File does not contain a maubot plugin definition") from e config = configparser.ConfigParser() try: config.read_string(data.decode("utf-8")) except (configparser.Error, KeyError, IndexError, ValueError) as e: - raise MaubotZipImportError("Maubot plugin definition in file is invalid") from e + raise MaubotZipMetaError("Maubot plugin definition in file is invalid") from e return file, config @classmethod @@ -120,7 +132,7 @@ class ZippedPluginLoader(PluginLoader): if "/" in main_class: main_module, main_class = main_class.split("/")[:2] except (configparser.Error, KeyError, IndexError, ValueError) as e: - raise MaubotZipImportError("Maubot plugin definition in file is invalid") from e + raise MaubotZipMetaError("Maubot plugin definition in file is invalid") from e return meta_id, version, modules, main_class, main_module @classmethod @@ -133,7 +145,7 @@ class ZippedPluginLoader(PluginLoader): file, config = self._open_meta(self.path) meta = self._read_meta(config) if self.id and meta[0] != self.id: - raise MaubotZipImportError("Maubot plugin ID changed during reload") + raise MaubotZipMetaError("Maubot plugin ID changed during reload") self.id, self.version, self.modules, self.main_class, self.main_module = meta self._file = file @@ -145,22 +157,22 @@ class ZippedPluginLoader(PluginLoader): self._importer.reset_cache() return self._importer except ZipImportError as e: - raise MaubotZipImportError("File not found or not a maubot plugin") from e + raise MaubotZipMetaError("File not found or not a maubot plugin") from e def _run_preload_checks(self, importer: zipimporter) -> None: try: code = importer.get_code(self.main_module.replace(".", "/")) if self.main_class not in code.co_names: - raise MaubotZipImportError( + raise MaubotZipPreLoadError( f"Main class {self.main_class} not in {self.main_module}") except ZipImportError as e: - raise MaubotZipImportError( + raise MaubotZipPreLoadError( f"Main module {self.main_module} not found in file") from e for module in self.modules: try: importer.find_module(module) except ZipImportError as e: - raise MaubotZipImportError(f"Module {module} not found in file") from e + raise MaubotZipPreLoadError(f"Module {module} not found in file") from e async def load(self, reset_cache: bool = False) -> Type[PluginClass]: try: @@ -175,13 +187,22 @@ class ZippedPluginLoader(PluginLoader): importer = self._get_importer(reset_cache=reset_cache) self._run_preload_checks(importer) if reset_cache: - self.log.debug(f"Preloaded plugin {self.id} from {self.path}") + self.log.debug(f"Re-preloaded plugin {self.id} from {self.path}") for module in self.modules: - importer.load_module(module) - main_mod = sys.modules[self.main_module] - plugin = getattr(main_mod, self.main_class) + try: + importer.load_module(module) + except ZipImportError as e: + raise MaubotZipLoadError(f"Module {module} not found in file") + try: + main_mod = sys.modules[self.main_module] + except KeyError as e: + raise MaubotZipLoadError(f"Main module {self.main_module} of plugin not found") from e + try: + plugin = getattr(main_mod, self.main_class) + except AttributeError as e: + raise MaubotZipLoadError(f"Main class {self.main_class} of plugin not found") from e if not issubclass(plugin, Plugin): - raise MaubotZipImportError("Main class of plugin does not extend maubot.Plugin") + raise MaubotZipLoadError("Main class of plugin does not extend maubot.Plugin") self._loaded = plugin self.log.debug(f"Loaded and imported plugin {self.id} from {self.path}") return plugin diff --git a/maubot/management/api/plugin.py b/maubot/management/api/plugin.py index 6e67c06..7345bb3 100644 --- a/maubot/management/api/plugin.py +++ b/maubot/management/api/plugin.py @@ -15,11 +15,12 @@ # along with this program. If not, see . from aiohttp import web from io import BytesIO +import traceback import os.path from ...loader import PluginLoader, ZippedPluginLoader, MaubotZipImportError -from .responses import (ErrPluginNotFound, ErrPluginInUse, ErrInputPluginInvalid, - ErrPluginReloadFailed, RespDeleted, RespOK) +from .responses import (ErrPluginNotFound, ErrPluginInUse, plugin_import_error, + plugin_reload_error, RespDeleted, RespOK, ErrUnsupportedPluginLoader) from . import routes, config @@ -62,7 +63,55 @@ async def reload_plugin(request: web.Request) -> web.Response: plugin = PluginLoader.id_cache.get(plugin_id, None) if not plugin: return ErrPluginNotFound - return await reload(plugin) + + await plugin.stop_instances() + try: + await plugin.reload() + except MaubotZipImportError as e: + return plugin_reload_error(str(e), traceback.format_exc()) + await plugin.start_instances() + return RespOK + + +async def upload_new_plugin(content: bytes, pid: str, version: str) -> web.Response: + path = os.path.join(config["plugin_directories.upload"], f"{pid}-v{version}.mbp") + with open(path, "wb") as p: + p.write(content) + try: + ZippedPluginLoader.get(path) + except MaubotZipImportError as e: + ZippedPluginLoader.trash(path) + return plugin_import_error(str(e), traceback.format_exc()) + return RespOK + + +async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, new_version: str + ) -> web.Response: + dirname = os.path.dirname(plugin.path) + filename = os.path.basename(plugin.path) + if plugin.version in filename: + filename = filename.replace(plugin.version, new_version) + else: + filename = filename.rstrip(".mbp") + new_version + ".mbp" + path = os.path.join(dirname, filename) + with open(path, "wb") as p: + p.write(content) + old_path = plugin.path + plugin.path = path + await plugin.stop_instances() + try: + await plugin.reload() + except MaubotZipImportError as e: + plugin.path = old_path + try: + await plugin.reload() + except MaubotZipImportError: + pass + await plugin.start_instances() + return plugin_import_error(str(e), traceback.format_exc()) + await plugin.start_instances() + ZippedPluginLoader.trash(plugin.path, reason="update") + return RespOK @routes.post("/plugins/upload") @@ -72,40 +121,11 @@ async def upload_plugin(request: web.Request) -> web.Response: try: pid, version = ZippedPluginLoader.verify_meta(file) except MaubotZipImportError as e: - return ErrInputPluginInvalid(e) + return plugin_import_error(str(e), traceback.format_exc()) plugin = PluginLoader.id_cache.get(pid, None) if not plugin: - path = os.path.join(config["plugin_directories.upload"], f"{pid}-v{version}.mbp") - with open(path, "wb") as p: - p.write(content) - try: - ZippedPluginLoader.get(path) - except MaubotZipImportError as e: - ZippedPluginLoader.trash(path) - # TODO log error? - return ErrInputPluginInvalid(e) + return await upload_new_plugin(content, pid, version) elif isinstance(plugin, ZippedPluginLoader): - dirname = os.path.dirname(plugin.path) - filename = os.path.basename(plugin.path) - if plugin.version in filename: - filename = filename.replace(plugin.version, version) - else: - filename = filename.rstrip(".mbp") + version + ".mbp" - path = os.path.join(dirname, filename) - with open(path, "wb") as p: - p.write(content) - ZippedPluginLoader.trash(plugin.path, reason="update") - plugin.path = path - return await reload(plugin) + return await upload_replacement_plugin(plugin, content, version) else: - return web.json_response({}) - - -async def reload(plugin: PluginLoader) -> web.Response: - await plugin.stop_instances() - try: - await plugin.reload() - except MaubotZipImportError as e: - return ErrPluginReloadFailed(e) - await plugin.start_instances() - return RespOK + return ErrUnsupportedPluginLoader diff --git a/maubot/management/api/responses.py b/maubot/management/api/responses.py index 993a354..d328cc2 100644 --- a/maubot/management/api/responses.py +++ b/maubot/management/api/responses.py @@ -36,20 +36,27 @@ ErrPluginInUse = web.json_response({ }, status=web.HTTPPreconditionFailed) -def ErrInputPluginInvalid(error) -> web.Response: +def plugin_import_error(error: str, stacktrace: str) -> web.Response: return web.json_response({ - "error": str(error), + "error": error, + "stacktrace": stacktrace, "errcode": "plugin_invalid", }, status=web.HTTPBadRequest) -def ErrPluginReloadFailed(error) -> web.Response: +def plugin_reload_error(error: str, stacktrace: str) -> web.Response: return web.json_response({ - "error": str(error), - "errcode": "plugin_invalid", + "error": error, + "stacktrace": stacktrace, + "errcode": "plugin_reload_fail", }, status=web.HTTPInternalServerError) +ErrUnsupportedPluginLoader = web.json_response({ + "error": "Existing plugin with same ID uses unsupported plugin loader", + "errcode": "unsupported_plugin_loader", +}, status=web.HTTPBadRequest) + ErrNotImplemented = web.json_response({ "error": "Not implemented", "errcode": "not_implemented", From 14fd0d6ac990d51f0329c7844f5169d7261cae72 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 31 Oct 2018 02:03:27 +0200 Subject: [PATCH 011/101] Finish plugin API and add basic login system --- .gitignore | 1 + example-config.yaml | 4 ++- maubot/__main__.py | 2 -- maubot/config.py | 35 +++++++++++++++++++---- maubot/instance.py | 26 +++++++++-------- maubot/loader/abc.py | 6 ++-- maubot/loader/zip.py | 4 ++- maubot/management/api/__init__.py | 11 +++++--- maubot/management/api/auth.py | 43 +++++++++++++++++++++++++++++ maubot/management/api/middleware.py | 23 +++++---------- maubot/management/api/plugin.py | 13 ++++----- maubot/management/api/responses.py | 41 ++++++++++++++++++++------- maubot/plugin_base.py | 7 ++++- maubot/server.py | 4 +++ requirements.txt | 1 + setup.py | 1 + 16 files changed, 160 insertions(+), 62 deletions(-) create mode 100644 maubot/management/api/auth.py diff --git a/.gitignore b/.gitignore index 9036bb6..d475bc1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ __pycache__ logs/ plugins/ +trash/ diff --git a/example-config.yaml b/example-config.yaml index 9bcf2bf..b3987f1 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -30,8 +30,10 @@ server: # Set to "generate" to generate and save a new token at startup. unshared_secret: generate +# List of administrator users. Plaintext passwords will be bcrypted on startup. Set empty password +# to prevent normal login. Root is a special user that can't have a password and will always exist. admins: -- "@admin:example.com" + root: "" # Python logging configuration. # diff --git a/maubot/__main__.py b/maubot/__main__.py index dee04bc..6cf22a9 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from sqlalchemy import orm -from time import time import sqlalchemy as sql import logging.config import argparse @@ -22,7 +21,6 @@ import asyncio import signal import copy import sys -import os from .config import Config from .db import Base, init as init_db diff --git a/maubot/config.py b/maubot/config.py index 699003c..cf39d00 100644 --- a/maubot/config.py +++ b/maubot/config.py @@ -15,9 +15,13 @@ # along with this program. If not, see . import random import string +import bcrypt +import re from mautrix.util.config import BaseFileConfig, ConfigUpdateHelper +bcrypt_regex = re.compile(r"^\$2[ayb]\$.{56}$") + class Config(BaseFileConfig): @staticmethod @@ -27,16 +31,35 @@ class Config(BaseFileConfig): def do_update(self, helper: ConfigUpdateHelper) -> None: base, copy, _ = helper copy("database") - copy("plugin_directories") - copy("plugin_db_directory") + copy("plugin_directories.upload") + copy("plugin_directories.load") + copy("plugin_directories.trash") + copy("plugin_directories.db") copy("server.hostname") copy("server.port") copy("server.listen") - copy("server.base_path") - shared_secret = self["server.shared_secret"] + copy("server.appservice_base_path") + shared_secret = self["server.unshared_secret"] if shared_secret is None or shared_secret == "generate": - base["server.shared_secret"] = self._new_token() + base["server.unshared_secret"] = self._new_token() else: - base["server.shared_secret"] = shared_secret + base["server.unshared_secret"] = shared_secret copy("admins") + for username, password in base["admins"].items(): + if password and not bcrypt_regex.match(password): + if password == "password": + password = self._new_token() + base["admins"][username] = bcrypt.hashpw(password.encode("utf-8"), + bcrypt.gensalt()).decode("utf-8") copy("logging") + + def is_admin(self, user: str) -> bool: + return user == "root" or user in self["admins"] + + def check_password(self, user: str, passwd: str) -> bool: + if user == "root": + return False + passwd_hash = self["admins"].get(user, None) + if not passwd_hash: + return False + return bcrypt.checkpw(passwd.encode("utf-8"), passwd_hash.encode("utf-8")) diff --git a/maubot/instance.py b/maubot/instance.py index 0438713..71098fc 100644 --- a/maubot/instance.py +++ b/maubot/instance.py @@ -87,13 +87,6 @@ class PluginInstance: def load_config(self) -> CommentedMap: return yaml.load(self.db_instance.config) - def load_config_base(self) -> Optional[RecursiveDict[CommentedMap]]: - try: - base = self.loader.read_file("base-config.yaml") - return RecursiveDict(yaml.load(base.decode("utf-8")), CommentedMap) - except (FileNotFoundError, KeyError): - return None - def save_config(self, data: RecursiveDict[CommentedMap]) -> None: buf = io.StringIO() yaml.dump(data, buf) @@ -103,14 +96,23 @@ class PluginInstance: if not self.enabled: self.log.warning(f"Plugin disabled, not starting.") return - cls = self.loader.load() + cls = await self.loader.load() config_class = cls.get_config_class() if config_class: - self.config = config_class(self.load_config, self.load_config_base, - self.save_config) + try: + base = await self.loader.read_file("base-config.yaml") + base_file = RecursiveDict(yaml.load(base.decode("utf-8")), CommentedMap) + except (FileNotFoundError, KeyError): + base_file = None + self.config = config_class(self.load_config, lambda: base_file, self.save_config) self.plugin = cls(self.client.client, self.id, self.log, self.config, - self.mb_config["plugin_db_directory"]) - await self.plugin.start() + self.mb_config["plugin_directories.db"]) + try: + await self.plugin.start() + except Exception: + self.log.exception("Failed to start instance") + self.enabled = False + return self.running = True self.log.info(f"Started instance of {self.loader.id} v{self.loader.version} " f"with user {self.client.id}") diff --git a/maubot/loader/abc.py b/maubot/loader/abc.py index 23a1de5..09d0f75 100644 --- a/maubot/loader/abc.py +++ b/maubot/loader/abc.py @@ -59,10 +59,12 @@ class PluginLoader(ABC): pass async def stop_instances(self) -> None: - await asyncio.gather([instance.stop() for instance in self.references if instance.running]) + await asyncio.gather(*[instance.stop() for instance + in self.references if instance.running]) async def start_instances(self) -> None: - await asyncio.gather([instance.start() for instance in self.references if instance.enabled]) + await asyncio.gather(*[instance.start() for instance + in self.references if instance.enabled]) @abstractmethod async def load(self) -> Type[PluginClass]: diff --git a/maubot/loader/zip.py b/maubot/loader/zip.py index d18894f..c811848 100644 --- a/maubot/loader/zip.py +++ b/maubot/loader/zip.py @@ -207,8 +207,10 @@ class ZippedPluginLoader(PluginLoader): self.log.debug(f"Loaded and imported plugin {self.id} from {self.path}") return plugin - async def reload(self) -> Type[PluginClass]: + async def reload(self, new_path: Optional[str] = None) -> Type[PluginClass]: await self.unload() + if new_path is not None: + self.path = new_path return await self.load(reset_cache=True) async def unload(self) -> None: diff --git a/maubot/management/api/__init__.py b/maubot/management/api/__init__.py index dc08a5f..d3e2611 100644 --- a/maubot/management/api/__init__.py +++ b/maubot/management/api/__init__.py @@ -27,8 +27,9 @@ config: Config = None def is_valid_token(token: str) -> bool: data = verify_token(config["server.unshared_secret"], token) - user_id = data.get("user_id", None) - return user_id is not None and user_id in config["admins"] + if not data: + return False + return config.is_admin(data.get("user_id", None)) def create_token(user: UserID) -> str: @@ -40,7 +41,9 @@ def create_token(user: UserID) -> str: def init(cfg: Config, loop: AbstractEventLoop) -> web.Application: global config config = cfg - from .middleware import auth, error, log - app = web.Application(loop=loop, middlewares=[auth, log, error]) + from .middleware import auth, error + from .auth import web as _ + from .plugin import web as _ + app = web.Application(loop=loop, middlewares=[auth, error]) app.add_routes(routes) return app diff --git a/maubot/management/api/auth.py b/maubot/management/api/auth.py new file mode 100644 index 0000000..d08ca1c --- /dev/null +++ b/maubot/management/api/auth.py @@ -0,0 +1,43 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from aiohttp import web +import json + +from . import routes, config, create_token +from .responses import ErrBadAuth, ErrBodyNotJSON + + +@routes.post("/login") +async def login(request: web.Request) -> web.Response: + try: + data = await request.json() + except json.JSONDecodeError: + return ErrBodyNotJSON + secret = data.get("secret") + if secret and config["server.unshared_secret"] == secret: + user = data.get("user") or "root" + return web.json_response({ + "token": create_token(user), + }) + + username = data.get("username") + password = data.get("password") + if config.check_password(username, password): + return web.json_response({ + "token": create_token(username), + }) + + return ErrBadAuth diff --git a/maubot/management/api/middleware.py b/maubot/management/api/middleware.py index f1b76ad..61e7097 100644 --- a/maubot/management/api/middleware.py +++ b/maubot/management/api/middleware.py @@ -15,25 +15,21 @@ # along with this program. If not, see . from typing import Callable, Awaitable from aiohttp import web -import logging -from .responses import ErrNoToken, ErrInvalidToken +from .responses import ErrNoToken, ErrInvalidToken, ErrPathNotFound, ErrMethodNotAllowed from . import is_valid_token Handler = Callable[[web.Request], Awaitable[web.Response]] -req_log = logging.getLogger("maubot.mgmt.request") -resp_log = logging.getLogger("maubot.mgmt.response") - @web.middleware async def auth(request: web.Request, handler: Handler) -> web.Response: + if request.path.endswith("/login"): + return await handler(request) token = request.headers.get("Authorization", "") if not token or not token.startswith("Bearer "): - req_log.debug(f"Request missing auth: {request.remote} {request.method} {request.path}") return ErrNoToken if not is_valid_token(token[len("Bearer "):]): - req_log.debug(f"Request invalid auth: {request.remote} {request.method} {request.path}") return ErrInvalidToken return await handler(request) @@ -43,6 +39,10 @@ async def error(request: web.Request, handler: Handler) -> web.Response: try: return await handler(request) except web.HTTPException as ex: + if ex.status_code == 404: + return ErrPathNotFound + elif ex.status_code == 405: + return ErrMethodNotAllowed return web.json_response({ "error": f"Unhandled HTTP {ex.status}", "errcode": f"unhandled_http_{ex.status}", @@ -56,12 +56,3 @@ def get_req_no(): global req_no req_no += 1 return req_no - - -@web.middleware -async def log(request: web.Request, handler: Handler) -> web.Response: - local_req_no = get_req_no() - req_log.info(f"Request {local_req_no}: {request.remote} {request.method} {request.path}") - resp = await handler(request) - resp_log.info(f"Responded to {local_req_no} from {request.remote}: {resp}") - return resp diff --git a/maubot/management/api/plugin.py b/maubot/management/api/plugin.py index 7345bb3..644c158 100644 --- a/maubot/management/api/plugin.py +++ b/maubot/management/api/plugin.py @@ -92,25 +92,24 @@ async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, if plugin.version in filename: filename = filename.replace(plugin.version, new_version) else: - filename = filename.rstrip(".mbp") + new_version + ".mbp" + filename = filename.rstrip(".mbp") + filename = f"{filename}-v{new_version}.mbp" path = os.path.join(dirname, filename) with open(path, "wb") as p: p.write(content) old_path = plugin.path - plugin.path = path await plugin.stop_instances() try: - await plugin.reload() + await plugin.reload(new_path=path) except MaubotZipImportError as e: - plugin.path = old_path try: - await plugin.reload() + await plugin.reload(new_path=old_path) + await plugin.start_instances() except MaubotZipImportError: pass - await plugin.start_instances() return plugin_import_error(str(e), traceback.format_exc()) await plugin.start_instances() - ZippedPluginLoader.trash(plugin.path, reason="update") + ZippedPluginLoader.trash(old_path, reason="update") return RespOK diff --git a/maubot/management/api/responses.py b/maubot/management/api/responses.py index d328cc2..8b3d1c0 100644 --- a/maubot/management/api/responses.py +++ b/maubot/management/api/responses.py @@ -13,27 +13,48 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from http import HTTPStatus from aiohttp import web +ErrBadAuth = web.json_response({ + "error": "Invalid username or password", + "errcode": "invalid_auth", +}, status=HTTPStatus.UNAUTHORIZED) + ErrNoToken = web.json_response({ "error": "Authorization token missing", "errcode": "auth_token_missing", -}, status=web.HTTPUnauthorized) +}, status=HTTPStatus.UNAUTHORIZED) ErrInvalidToken = web.json_response({ "error": "Invalid authorization token", "errcode": "auth_token_invalid", -}, status=web.HTTPUnauthorized) +}, status=HTTPStatus.UNAUTHORIZED) ErrPluginNotFound = web.json_response({ "error": "Plugin not found", "errcode": "plugin_not_found", -}, status=web.HTTPNotFound) +}, status=HTTPStatus.NOT_FOUND) + +ErrPathNotFound = web.json_response({ + "error": "Resource not found", + "errcode": "resource_not_found", +}, status=HTTPStatus.NOT_FOUND) + +ErrMethodNotAllowed = web.json_response({ + "error": "Method not allowed", + "errcode": "method_not_allowed", +}, status=HTTPStatus.METHOD_NOT_ALLOWED) ErrPluginInUse = web.json_response({ "error": "Plugin instances of this type still exist", "errcode": "plugin_in_use", -}, status=web.HTTPPreconditionFailed) +}, status=HTTPStatus.PRECONDITION_FAILED) + +ErrBodyNotJSON = web.json_response({ + "error": "Request body is not JSON", + "errcode": "body_not_json", +}, status=HTTPStatus.BAD_REQUEST) def plugin_import_error(error: str, stacktrace: str) -> web.Response: @@ -41,7 +62,7 @@ def plugin_import_error(error: str, stacktrace: str) -> web.Response: "error": error, "stacktrace": stacktrace, "errcode": "plugin_invalid", - }, status=web.HTTPBadRequest) + }, status=HTTPStatus.BAD_REQUEST) def plugin_reload_error(error: str, stacktrace: str) -> web.Response: @@ -49,21 +70,21 @@ def plugin_reload_error(error: str, stacktrace: str) -> web.Response: "error": error, "stacktrace": stacktrace, "errcode": "plugin_reload_fail", - }, status=web.HTTPInternalServerError) + }, status=HTTPStatus.INTERNAL_SERVER_ERROR) ErrUnsupportedPluginLoader = web.json_response({ "error": "Existing plugin with same ID uses unsupported plugin loader", "errcode": "unsupported_plugin_loader", -}, status=web.HTTPBadRequest) +}, status=HTTPStatus.BAD_REQUEST) ErrNotImplemented = web.json_response({ "error": "Not implemented", "errcode": "not_implemented", -}, status=web.HTTPNotImplemented) +}, status=HTTPStatus.NOT_IMPLEMENTED) RespOK = web.json_response({ "success": True, -}, status=web.HTTPOk) +}, status=HTTPStatus.OK) -RespDeleted = web.Response(status=web.HTTPNoContent) +RespDeleted = web.Response(status=HTTPStatus.NO_CONTENT) diff --git a/maubot/plugin_base.py b/maubot/plugin_base.py index 08d1211..18620c6 100644 --- a/maubot/plugin_base.py +++ b/maubot/plugin_base.py @@ -27,6 +27,9 @@ if TYPE_CHECKING: from mautrix.util.config import BaseProxyConfig +DatabaseNotConfigured = ValueError("A database for this maubot instance has not been configured.") + + class Plugin(ABC): client: 'MaubotMatrixClient' id: str @@ -41,7 +44,9 @@ class Plugin(ABC): self.config = config self.__db_base_path = db_base_path - def request_db_engine(self) -> Engine: + def request_db_engine(self) -> Optional[Engine]: + if not self.__db_base_path: + raise DatabaseNotConfigured return sql.create_engine(f"sqlite:///{os.path.join(self.__db_base_path, self.id)}.db") def set_command_spec(self, spec: 'CommandSpec') -> None: diff --git a/maubot/server.py b/maubot/server.py index 4bd7bd2..501677a 100644 --- a/maubot/server.py +++ b/maubot/server.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from aiohttp import web +import logging import asyncio from mautrix.api import PathBuilder, Method @@ -23,6 +24,8 @@ from .__meta__ import __version__ class MaubotServer: + log: logging.Logger = logging.getLogger("maubot.server") + def __init__(self, config: Config, management: web.Application, loop: asyncio.AbstractEventLoop) -> None: self.loop = loop or asyncio.get_event_loop() @@ -45,6 +48,7 @@ class MaubotServer: await self.runner.setup() site = web.TCPSite(self.runner, self.config["server.hostname"], self.config["server.port"]) await site.start() + self.log.info(f"Listening on {site.name}") async def stop(self) -> None: await self.runner.cleanup() diff --git a/requirements.txt b/requirements.txt index f8671cd..2a918f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ alembic commonmark ruamel.yaml attrs +bcrypt diff --git a/setup.py b/setup.py index a546b06..0d8dad0 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,7 @@ setuptools.setup( "commonmark>=0.8.1,<1", "ruamel.yaml>=0.15.35,<0.16", "attrs>=18.1.0,<19", + "bcrypt>=3.1.4,<4", ], classifiers=[ From c7d16a5d4695883fc5dfeddddfb72f1cffc13571 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 31 Oct 2018 22:27:50 +0200 Subject: [PATCH 012/101] Update management spec --- .editorconfig | 3 ++ maubot/management/api/spec.yaml | 69 +++++++++++++++++++++++++++------ 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/.editorconfig b/.editorconfig index d00d5a4..f1ad5ca 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,3 +11,6 @@ max_line_length = 99 [*.json] indent_size = 2 + +[spec.yaml] +indent_size = 2 diff --git a/maubot/management/api/spec.yaml b/maubot/management/api/spec.yaml index 4389f23..db9e483 100644 --- a/maubot/management/api/spec.yaml +++ b/maubot/management/api/spec.yaml @@ -7,16 +7,50 @@ info: name: GNU Affero General Public License version 3 url: 'https://github.com/maubot/maubot/blob/master/LICENSE' security: - - bearer: [] +- bearer: [] servers: - - url: /_matrix/maubot/v1 +- url: /_matrix/maubot/v1 paths: + /login: + post: + operationId: login + summary: Log in with the unshared secret or username+password + tags: [Authentication] + requestBody: + content: + application/json: + schema: + type: object + description: Set either username+password or secret. + properties: + secret: + type: string + description: The unshared server secret for root login + username: + type: string + description: The username for normal login + password: + type: string + description: The password for normal login + responses: + 200: + description: Logged in successfully + content: + application/json: + schema: + type: object + properties: + token: + type: string + 401: + description: Invalid credentials + /plugins: get: operationId: get_plugins summary: Get the list of installed plugins - tags: [Plugin] + tags: [Plugins] responses: 200: description: The list of plugins @@ -33,7 +67,7 @@ paths: operationId: upload_plugin summary: Upload a new plugin description: Upload a new plugin. If the plugin already exists, enabled instances will be restarted. - tags: [Plugin] + tags: [Plugins] responses: 200: description: Plugin uploaded and replaced current version successfully @@ -71,7 +105,7 @@ paths: get: operationId: get_plugin summary: Get information about a specific plugin - tags: [Plugin] + tags: [Plugins] responses: 200: description: Plugin found @@ -87,7 +121,7 @@ paths: operationId: delete_plugin summary: Delete a plugin description: Delete a plugin. All instances of the plugin must be deleted before deleting the plugin. - tags: [Plugin] + tags: [Plugins] responses: 204: description: Plugin deleted @@ -108,7 +142,7 @@ paths: post: operationId: reload_plugin summary: Reload a plugin from disk - tags: [Plugin] + tags: [Plugins] responses: 200: description: Plugin reloaded @@ -171,6 +205,11 @@ paths: operationId: update_instance summary: Create a plugin instance or edit the details of an existing plugin instance tags: [Plugin instances] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PluginInstance' responses: 200: description: Plugin instance edited @@ -185,7 +224,7 @@ paths: get: operationId: get_clients summary: Get the list of Matrix clients - tags: [Client] + tags: [Clients] responses: 200: description: The list of plugins @@ -206,7 +245,7 @@ paths: get: operationId: get_client summary: Get information about a specific Matrix client - tags: [Client] + tags: [Clients] responses: 200: description: Client found @@ -221,7 +260,12 @@ paths: put: operationId: update_client summary: Create or update a Matrix client - tags: [Client] + tags: [Clients] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClient' responses: 200: description: Client updated @@ -242,7 +286,7 @@ paths: delete: operationId: delete_client summary: Delete a Matrix client - tags: [Client] + tags: [Clients] responses: 204: description: Client deleted @@ -288,7 +332,6 @@ components: id: type: string example: jesaribot - readOnly: true type: type: string example: xyz.maubot.jesaribot @@ -333,6 +376,7 @@ components: id: type: string example: '@putkiteippi:maunium.net' + readOnly: true homeserver: type: string example: 'https://maunium.net' @@ -355,5 +399,6 @@ components: example: 'mxc://maunium.net/FsPQQTntCCqhJMFtwArmJdaU' instances: type: array + readOnly: true items: $ref: '#/components/schemas/PluginInstance' From 057ccc19140af68efdaa9ef2a93ede6cae8ff919 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 31 Oct 2018 22:28:13 +0200 Subject: [PATCH 013/101] Refactor management API impl --- maubot/management/api/__init__.py | 29 +++++----------------------- maubot/management/api/auth.py | 18 ++++++++++++++++- maubot/management/api/base.py | 30 +++++++++++++++++++++++++++++ maubot/management/api/middleware.py | 2 +- maubot/management/api/plugin.py | 4 ++-- 5 files changed, 55 insertions(+), 28 deletions(-) create mode 100644 maubot/management/api/base.py diff --git a/maubot/management/api/__init__.py b/maubot/management/api/__init__.py index d3e2611..e66b527 100644 --- a/maubot/management/api/__init__.py +++ b/maubot/management/api/__init__.py @@ -16,34 +16,15 @@ from aiohttp import web from asyncio import AbstractEventLoop -from mautrix.types import UserID -from mautrix.util.signed_token import sign_token, verify_token - from ...config import Config - -routes = web.RouteTableDef() -config: Config = None - - -def is_valid_token(token: str) -> bool: - data = verify_token(config["server.unshared_secret"], token) - if not data: - return False - return config.is_admin(data.get("user_id", None)) - - -def create_token(user: UserID) -> str: - return sign_token(config["server.unshared_secret"], { - "user_id": user, - }) +from .base import routes, set_config +from .middleware import auth, error +from .auth import web as _ +from .plugin import web as _ def init(cfg: Config, loop: AbstractEventLoop) -> web.Application: - global config - config = cfg - from .middleware import auth, error - from .auth import web as _ - from .plugin import web as _ + set_config(cfg) app = web.Application(loop=loop, middlewares=[auth, error]) app.add_routes(routes) return app diff --git a/maubot/management/api/auth.py b/maubot/management/api/auth.py index d08ca1c..1b0bcf3 100644 --- a/maubot/management/api/auth.py +++ b/maubot/management/api/auth.py @@ -16,10 +16,26 @@ from aiohttp import web import json -from . import routes, config, create_token +from mautrix.types import UserID +from mautrix.util.signed_token import sign_token, verify_token + +from .base import routes, get_config from .responses import ErrBadAuth, ErrBodyNotJSON +def is_valid_token(token: str) -> bool: + data = verify_token(get_config()["server.unshared_secret"], token) + if not data: + return False + return get_config().is_admin(data.get("user_id", None)) + + +def create_token(user: UserID) -> str: + return sign_token(get_config()["server.unshared_secret"], { + "user_id": user, + }) + + @routes.post("/login") async def login(request: web.Request) -> web.Response: try: diff --git a/maubot/management/api/base.py b/maubot/management/api/base.py new file mode 100644 index 0000000..d9c2077 --- /dev/null +++ b/maubot/management/api/base.py @@ -0,0 +1,30 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from aiohttp import web + +from ...config import Config + +routes: web.RouteTableDef = web.RouteTableDef() +_config: Config = None + + +def set_config(config: Config) -> None: + global _config + _config = config + + +def get_config() -> Config: + return _config diff --git a/maubot/management/api/middleware.py b/maubot/management/api/middleware.py index 61e7097..fa5b93a 100644 --- a/maubot/management/api/middleware.py +++ b/maubot/management/api/middleware.py @@ -17,7 +17,7 @@ from typing import Callable, Awaitable from aiohttp import web from .responses import ErrNoToken, ErrInvalidToken, ErrPathNotFound, ErrMethodNotAllowed -from . import is_valid_token +from .auth import is_valid_token Handler = Callable[[web.Request], Awaitable[web.Response]] diff --git a/maubot/management/api/plugin.py b/maubot/management/api/plugin.py index 644c158..4bdcbbd 100644 --- a/maubot/management/api/plugin.py +++ b/maubot/management/api/plugin.py @@ -21,7 +21,7 @@ import os.path from ...loader import PluginLoader, ZippedPluginLoader, MaubotZipImportError from .responses import (ErrPluginNotFound, ErrPluginInUse, plugin_import_error, plugin_reload_error, RespDeleted, RespOK, ErrUnsupportedPluginLoader) -from . import routes, config +from .base import routes, get_config def _plugin_to_dict(plugin: PluginLoader) -> dict: @@ -74,7 +74,7 @@ async def reload_plugin(request: web.Request) -> web.Response: async def upload_new_plugin(content: bytes, pid: str, version: str) -> web.Response: - path = os.path.join(config["plugin_directories.upload"], f"{pid}-v{version}.mbp") + path = os.path.join(get_config()["plugin_directories.upload"], f"{pid}-v{version}.mbp") with open(path, "wb") as p: p.write(content) try: From 4b7c38ca2ac139b399af80a601c932a374c2a65c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 31 Oct 2018 22:41:00 +0200 Subject: [PATCH 014/101] Add clients and instances API stubs --- maubot/management/api/client.py | 39 +++++++++++++++++++++++++++++++ maubot/management/api/instance.py | 39 +++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 maubot/management/api/client.py create mode 100644 maubot/management/api/instance.py diff --git a/maubot/management/api/client.py b/maubot/management/api/client.py new file mode 100644 index 0000000..a7ec14e --- /dev/null +++ b/maubot/management/api/client.py @@ -0,0 +1,39 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from aiohttp import web + +from .base import routes +from .responses import ErrNotImplemented + + +@routes.get("/clients") +def get_clients(request: web.Request) -> web.Response: + return ErrNotImplemented + + +@routes.get("/client/{id}") +def get_client(request: web.Request) -> web.Response: + return ErrNotImplemented + + +@routes.put("/client/{id}") +def update_client(request: web.Request) -> web.Response: + return ErrNotImplemented + + +@routes.delete("/client/{id}") +def delete_client(request: web.Request) -> web.Response: + return ErrNotImplemented diff --git a/maubot/management/api/instance.py b/maubot/management/api/instance.py new file mode 100644 index 0000000..8baa9f9 --- /dev/null +++ b/maubot/management/api/instance.py @@ -0,0 +1,39 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from aiohttp import web + +from .base import routes +from .responses import ErrNotImplemented + + +@routes.get("/instances") +def get_instances(request: web.Request) -> web.Response: + return ErrNotImplemented + + +@routes.get("/instance/{id}") +def get_instance(request: web.Request) -> web.Response: + return ErrNotImplemented + + +@routes.put("/instance/{id}") +def update_instance(request: web.Request) -> web.Response: + return ErrNotImplemented + + +@routes.delete("/instance/{id}") +def delete_instance(request: web.Request) -> web.Response: + return ErrNotImplemented From b96d6e6a94f49d92903fcb2012f85866d027f10e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 31 Oct 2018 22:55:43 +0200 Subject: [PATCH 015/101] Remove command_spec table --- maubot/db.py | 41 +---------------------------------------- 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/maubot/db.py b/maubot/db.py index 6a4f82b..6210bee 100644 --- a/maubot/db.py +++ b/maubot/db.py @@ -13,40 +13,15 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Type -from sqlalchemy import (Column, String, Boolean, ForeignKey, Text, TypeDecorator) +from sqlalchemy import Column, String, Boolean, ForeignKey, Text from sqlalchemy.orm import Query, scoped_session from sqlalchemy.ext.declarative import declarative_base -import json from mautrix.types import UserID, FilterID, SyncToken, ContentURI -from mautrix.client.api.types.util import Serializable - -from .command_spec import CommandSpec Base: declarative_base = declarative_base() -def make_serializable_alchemy(serializable_type: Type[Serializable]): - class SerializableAlchemy(TypeDecorator): - impl = Text - - @property - def python_type(self): - return serializable_type - - def process_literal_param(self, value: Serializable, _) -> str: - return json.dumps(value.serialize()) if value is not None else None - - def process_bind_param(self, value: Serializable, _) -> str: - return json.dumps(value.serialize()) if value is not None else None - - def process_result_value(self, value: str, _) -> serializable_type: - return serializable_type.deserialize(json.loads(value)) if value is not None else None - - return SerializableAlchemy - - class DBPlugin(Base): query: Query __tablename__ = "plugin" @@ -78,20 +53,6 @@ class DBClient(Base): avatar_url: ContentURI = Column(String(255), nullable=False, default="") -class DBCommandSpec(Base): - query: Query - __tablename__ = "command_spec" - - plugin: str = Column(String(255), - ForeignKey("plugin.id", onupdate="CASCADE", ondelete="CASCADE"), - primary_key=True) - client: UserID = Column(String(255), - ForeignKey("client.id", onupdate="CASCADE", ondelete="CASCADE"), - primary_key=True) - spec: CommandSpec = Column(make_serializable_alchemy(CommandSpec), nullable=False) - - def init(session: scoped_session) -> None: DBPlugin.query = session.query_property() DBClient.query = session.query_property() - DBCommandSpec.query = session.query_property() From 9e066478a99ac19b861112e9d1081b02c48c2c8c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 1 Nov 2018 01:51:54 +0200 Subject: [PATCH 016/101] Refactor how plugins are started and update spec --- maubot/__main__.py | 9 +-- maubot/__meta__.py | 2 +- maubot/client.py | 106 +++++++++++++++++++++++------ maubot/db.py | 1 + maubot/instance.py | 11 ++- maubot/loader/abc.py | 1 + maubot/management/api/instance.py | 44 ++++++++++-- maubot/management/api/plugin.py | 17 ++--- maubot/management/api/responses.py | 5 ++ maubot/management/api/spec.yaml | 43 +++--------- 10 files changed, 160 insertions(+), 79 deletions(-) diff --git a/maubot/__main__.py b/maubot/__main__.py index 6cf22a9..5d5daec 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -57,7 +57,7 @@ Base.metadata.create_all() loop = asyncio.get_event_loop() init_db(db_session) -init_client(loop) +clients = init_client(loop) init_plugin_instance_class(db_session, config) management_api = init_management(config, loop) server = MaubotServer(config, management_api, loop) @@ -84,9 +84,10 @@ async def periodic_commit(): try: - loop.run_until_complete(asyncio.gather( - server.start(), - *[plugin.start() for plugin in plugins])) + log.debug("Starting server") + loop.run_until_complete(server.start()) + log.debug("Starting clients and plugins") + loop.run_until_complete(asyncio.gather(*[client.start() for client in clients])) log.debug("Startup actions complete, running forever") loop.run_until_complete(periodic_commit()) loop.run_forever() diff --git a/maubot/__meta__.py b/maubot/__meta__.py index 32cb26d..e305370 100644 --- a/maubot/__meta__.py +++ b/maubot/__meta__.py @@ -1 +1 @@ -__version__ = "0.1.0.dev5" +__version__ = "0.1.0.dev6" diff --git a/maubot/client.py b/maubot/client.py index e96c918..9f4fcc8 100644 --- a/maubot/client.py +++ b/maubot/client.py @@ -18,6 +18,7 @@ from aiohttp import ClientSession import asyncio import logging +from mautrix.errors import MatrixInvalidToken, MatrixRequestError from mautrix.types import (UserID, SyncToken, FilterID, ContentURI, StrippedStateEvent, Membership, EventType, Filter, RoomFilter, RoomEventFilter) @@ -31,6 +32,7 @@ log = logging.getLogger("maubot.client") class Client: + log: logging.Logger loop: asyncio.AbstractEventLoop cache: Dict[UserID, 'Client'] = {} http_client: ClientSession = None @@ -38,42 +40,97 @@ class Client: references: Set['PluginInstance'] db_instance: DBClient client: MaubotMatrixClient + started: bool def __init__(self, db_instance: DBClient) -> None: self.db_instance = db_instance self.cache[self.id] = self self.log = log.getChild(self.id) self.references = set() + self.started = False self.client = MaubotMatrixClient(mxid=self.id, base_url=self.homeserver, token=self.access_token, client_session=self.http_client, log=self.log, loop=self.loop, store=self.db_instance) if self.autojoin: self.client.add_event_handler(self._handle_invite, EventType.ROOM_MEMBER) - def start(self) -> None: - asyncio.ensure_future(self._start(), loop=self.loop) - - async def _start(self) -> None: + async def start(self, try_n: Optional[int] = 0) -> None: try: - if not self.filter_id: - self.filter_id = await self.client.create_filter(Filter( - room=RoomFilter( - timeline=RoomEventFilter( - limit=50, - ), - ), - )) - if self.displayname != "disable": - await self.client.set_displayname(self.displayname) - if self.avatar_url != "disable": - await self.client.set_avatar_url(self.avatar_url) - await self.client.start(self.filter_id) + if try_n > 0: + await asyncio.sleep(try_n * 10) + await self._start(try_n) except Exception: - self.log.exception("starting raised exception") + self.log.exception("Failed to start") + + async def _start(self, try_n: Optional[int] = 0) -> None: + if not self.enabled: + self.log.debug("Not starting disabled client") + return + elif self.started: + self.log.warning("Ignoring start() call to started client") + return + try: + user_id = await self.client.whoami() + except MatrixInvalidToken as e: + self.log.error(f"Invalid token: {e}. Disabling client") + self.enabled = False + return + except MatrixRequestError: + if try_n >= 5: + self.log.exception("Failed to get /account/whoami, disabling client") + self.enabled = False + else: + self.log.exception(f"Failed to get /account/whoami, " + f"retrying in {(try_n + 1) * 10}s") + _ = asyncio.ensure_future(self.start(try_n + 1), loop=self.loop) + return + if user_id != self.id: + self.log.error(f"User ID mismatch: expected {self.id}, but got {user_id}") + self.enabled = False + return + if not self.filter_id: + self.filter_id = await self.client.create_filter(Filter( + room=RoomFilter( + timeline=RoomEventFilter( + limit=50, + ), + ), + )) + if self.displayname != "disable": + await self.client.set_displayname(self.displayname) + if self.avatar_url != "disable": + await self.client.set_avatar_url(self.avatar_url) + if self.sync: + self.client.start(self.filter_id) + self.started = True + self.log.info("Client started, starting plugin instances...") + await self.start_plugins() + + async def start_plugins(self) -> None: + await asyncio.gather(*[plugin.start() for plugin in self.references], loop=self.loop) + + async def stop_plugins(self) -> None: + await asyncio.gather(*[plugin.stop() for plugin in self.references if plugin.running], + loop=self.loop) def stop(self) -> None: + self.started = False self.client.stop() + def to_dict(self) -> dict: + return { + "id": self.id, + "homeserver": self.homeserver, + "access_token": self.access_token, + "enabled": self.enabled, + "started": self.started, + "sync": self.sync, + "autojoin": self.autojoin, + "displayname": self.displayname, + "avatar_url": self.avatar_url, + "instances": [instance.to_dict() for instance in self.references], + } + @classmethod def get(cls, user_id: UserID, db_instance: Optional[DBClient] = None) -> Optional['Client']: try: @@ -111,6 +168,14 @@ class Client: self.client.api.token = value self.db_instance.access_token = value + @property + def enabled(self) -> bool: + return self.db_instance.enabled + + @enabled.setter + def enabled(self, value: bool) -> None: + self.db_instance.enabled = value + @property def next_batch(self) -> SyncToken: return self.db_instance.next_batch @@ -168,8 +233,7 @@ class Client: # endregion -def init(loop: asyncio.AbstractEventLoop) -> None: +def init(loop: asyncio.AbstractEventLoop) -> List[Client]: Client.http_client = ClientSession(loop=loop) Client.loop = loop - for client in Client.all(): - client.start() + return Client.all() diff --git a/maubot/db.py b/maubot/db.py index 6210bee..d53658c 100644 --- a/maubot/db.py +++ b/maubot/db.py @@ -42,6 +42,7 @@ class DBClient(Base): id: UserID = Column(String(255), primary_key=True) homeserver: str = Column(String(255), nullable=False) access_token: str = Column(String(255), nullable=False) + enabled: bool = Column(Boolean, nullable=False, default=False) next_batch: SyncToken = Column(String(255), nullable=False, default="") filter_id: FilterID = Column(String(255), nullable=False, default="") diff --git a/maubot/instance.py b/maubot/instance.py index 71098fc..83bb85c 100644 --- a/maubot/instance.py +++ b/maubot/instance.py @@ -75,6 +75,7 @@ class PluginInstance: if not self.client: self.log.error(f"Failed to get client for user {self.primary_user}") self.enabled = False + return self.log.debug("Plugin instance dependencies loaded") self.loader.references.add(self) self.client.references.add(self) @@ -93,8 +94,11 @@ class PluginInstance: self.db_instance.config = buf.getvalue() async def start(self) -> None: - if not self.enabled: - self.log.warning(f"Plugin disabled, not starting.") + if self.running: + self.log.warning("Ignoring start() call to already started plugin") + return + elif not self.enabled: + self.log.warning("Plugin disabled, not starting.") return cls = await self.loader.load() config_class = cls.get_config_class() @@ -118,6 +122,9 @@ class PluginInstance: f"with user {self.client.id}") async def stop(self) -> None: + if not self.running: + self.log.warning("Ignoring stop() call to non-running plugin") + return self.log.debug("Stopping plugin instance...") self.running = False await self.plugin.stop() diff --git a/maubot/loader/abc.py b/maubot/loader/abc.py index 09d0f75..4dbb299 100644 --- a/maubot/loader/abc.py +++ b/maubot/loader/abc.py @@ -47,6 +47,7 @@ class PluginLoader(ABC): return { "id": self.id, "version": self.version, + "instances": [instance.to_dict() for instance in self.references], } @property diff --git a/maubot/management/api/instance.py b/maubot/management/api/instance.py index 8baa9f9..064c3f8 100644 --- a/maubot/management/api/instance.py +++ b/maubot/management/api/instance.py @@ -14,26 +14,56 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from aiohttp import web +from json import JSONDecodeError +from mautrix.types import UserID + +from ...db import DBClient +from ...client import Client from .base import routes -from .responses import ErrNotImplemented +from .responses import ErrNotImplemented, ErrClientNotFound, ErrBodyNotJSON @routes.get("/instances") -def get_instances(request: web.Request) -> web.Response: - return ErrNotImplemented +async def get_instances(_: web.Request) -> web.Response: + return web.json_response([client.to_dict() for client in Client.cache.values()]) @routes.get("/instance/{id}") -def get_instance(request: web.Request) -> web.Response: +async def get_instance(request: web.Request) -> web.Response: + user_id = request.match_info.get("id", None) + client = Client.get(user_id, None) + if not client: + return ErrClientNotFound + return web.json_response(client.to_dict()) + + +async def create_instance(user_id: UserID, data: dict) -> web.Response: + return ErrNotImplemented + + +async def update_instance(client: Client, data: dict) -> web.Response: return ErrNotImplemented @routes.put("/instance/{id}") -def update_instance(request: web.Request) -> web.Response: - return ErrNotImplemented +async def update_instance(request: web.Request) -> web.Response: + user_id = request.match_info.get("id", None) + client = Client.get(user_id, None) + try: + data = await request.json() + except JSONDecodeError: + return ErrBodyNotJSON + if not client: + return await create_instance(user_id, data) + else: + return await update_instance(client, data) @routes.delete("/instance/{id}") -def delete_instance(request: web.Request) -> web.Response: +async def delete_instance(request: web.Request) -> web.Response: + user_id = request.match_info.get("id", None) + client = Client.get(user_id, None) + if not client: + return ErrClientNotFound return ErrNotImplemented diff --git a/maubot/management/api/plugin.py b/maubot/management/api/plugin.py index 4bdcbbd..d07030c 100644 --- a/maubot/management/api/plugin.py +++ b/maubot/management/api/plugin.py @@ -24,16 +24,9 @@ from .responses import (ErrPluginNotFound, ErrPluginInUse, plugin_import_error, from .base import routes, get_config -def _plugin_to_dict(plugin: PluginLoader) -> dict: - return { - **plugin.to_dict(), - "instances": [instance.to_dict() for instance in plugin.references] - } - - @routes.get("/plugins") async def get_plugins(_) -> web.Response: - return web.json_response([_plugin_to_dict(plugin) for plugin in PluginLoader.id_cache.values()]) + return web.json_response([plugin.to_dict() for plugin in PluginLoader.id_cache.values()]) @routes.get("/plugin/{id}") @@ -42,7 +35,7 @@ async def get_plugin(request: web.Request) -> web.Response: plugin = PluginLoader.id_cache.get(plugin_id, None) if not plugin: return ErrPluginNotFound - return web.json_response(_plugin_to_dict(plugin)) + return web.json_response(plugin.to_dict()) @routes.delete("/plugin/{id}") @@ -78,11 +71,11 @@ async def upload_new_plugin(content: bytes, pid: str, version: str) -> web.Respo with open(path, "wb") as p: p.write(content) try: - ZippedPluginLoader.get(path) + plugin = ZippedPluginLoader.get(path) except MaubotZipImportError as e: ZippedPluginLoader.trash(path) return plugin_import_error(str(e), traceback.format_exc()) - return RespOK + return web.json_response(plugin.to_dict()) async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, new_version: str @@ -110,7 +103,7 @@ async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, return plugin_import_error(str(e), traceback.format_exc()) await plugin.start_instances() ZippedPluginLoader.trash(old_path, reason="update") - return RespOK + return web.json_response(plugin.to_dict()) @routes.post("/plugins/upload") diff --git a/maubot/management/api/responses.py b/maubot/management/api/responses.py index 8b3d1c0..ad3033b 100644 --- a/maubot/management/api/responses.py +++ b/maubot/management/api/responses.py @@ -36,6 +36,11 @@ ErrPluginNotFound = web.json_response({ "errcode": "plugin_not_found", }, status=HTTPStatus.NOT_FOUND) +ErrClientNotFound = web.json_response({ + "error": "Client not found", + "errcode": "client_not_found", +}, status=HTTPStatus.NOT_FOUND) + ErrPathNotFound = web.json_response({ "error": "Resource not found", "errcode": "resource_not_found", diff --git a/maubot/management/api/spec.yaml b/maubot/management/api/spec.yaml index db9e483..8cfde21 100644 --- a/maubot/management/api/spec.yaml +++ b/maubot/management/api/spec.yaml @@ -231,12 +231,14 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/MatrixClientList' + type: array + items: + $ref: '#/components/schemas/MatrixClient' 401: $ref: '#/components/responses/Unauthorized' - '/client/{user_id}': + '/client/{id}': parameters: - - name: user_id + - name: id in: path description: The Matrix user ID of the client to get required: true @@ -338,38 +340,12 @@ components: enabled: type: boolean example: true + started: + type: boolean + example: true primary_user: type: string example: '@putkiteippi:maunium.net' - MatrixClientList: - type: array - items: - type: object - properties: - id: - type: string - example: '@putkiteippi:maunium.net' - homeserver: - type: string - example: 'https://maunium.net' - enabled: - type: boolean - example: true - sync: - type: boolean - example: true - autojoin: - type: boolean - example: true - displayname: - type: string - example: J. E. Saarinen - avatar_url: - type: string - example: 'mxc://maunium.net/FsPQQTntCCqhJMFtwArmJdaU' - instance_count: - type: integer - example: 1 MatrixClient: type: object properties: @@ -385,6 +361,9 @@ components: enabled: type: boolean example: true + started: + type: boolean + example: true sync: type: boolean example: true From 767885cec7d549731e7205d7fdf8d91dabdbde23 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 1 Nov 2018 11:58:58 +0200 Subject: [PATCH 017/101] Pass asyncio event loop and http session to plugin instances --- maubot/client.py | 4 ++-- maubot/instance.py | 9 ++++++--- maubot/plugin_base.py | 10 ++++++++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/maubot/client.py b/maubot/client.py index 9f4fcc8..dee1d8c 100644 --- a/maubot/client.py +++ b/maubot/client.py @@ -32,8 +32,8 @@ log = logging.getLogger("maubot.client") class Client: - log: logging.Logger - loop: asyncio.AbstractEventLoop + log: logging.Logger = None + loop: asyncio.AbstractEventLoop = None cache: Dict[UserID, 'Client'] = {} http_client: ClientSession = None diff --git a/maubot/instance.py b/maubot/instance.py index 83bb85c..924bf5a 100644 --- a/maubot/instance.py +++ b/maubot/instance.py @@ -17,6 +17,7 @@ from typing import Dict, List, Optional from sqlalchemy.orm import Session from ruamel.yaml.comments import CommentedMap from ruamel.yaml import YAML +from asyncio import AbstractEventLoop import logging import io @@ -38,6 +39,7 @@ yaml.indent(4) class PluginInstance: db: Session = None mb_config: Config = None + loop: AbstractEventLoop = None cache: Dict[str, 'PluginInstance'] = {} plugin_directories: List[str] = [] @@ -109,8 +111,8 @@ class PluginInstance: except (FileNotFoundError, KeyError): base_file = None self.config = config_class(self.load_config, lambda: base_file, self.save_config) - self.plugin = cls(self.client.client, self.id, self.log, self.config, - self.mb_config["plugin_directories.db"]) + self.plugin = cls(self.client.client, self.loop, self.client.http_client, self.id, + self.log, self.config, self.mb_config["plugin_directories.db"]) try: await self.plugin.start() except Exception: @@ -178,6 +180,7 @@ class PluginInstance: # endregion -def init(db: Session, config: Config): +def init(db: Session, config: Config, loop: AbstractEventLoop): PluginInstance.db = db PluginInstance.mb_config = config + PluginInstance.loop = loop diff --git a/maubot/plugin_base.py b/maubot/plugin_base.py index 18620c6..9b394f1 100644 --- a/maubot/plugin_base.py +++ b/maubot/plugin_base.py @@ -16,6 +16,8 @@ from typing import Type, Optional, TYPE_CHECKING from logging import Logger from abc import ABC, abstractmethod +from asyncio import AbstractEventLoop +from aiohttp import ClientSession import os.path from sqlalchemy.engine.base import Engine @@ -34,11 +36,15 @@ class Plugin(ABC): client: 'MaubotMatrixClient' id: str log: Logger + loop: AbstractEventLoop config: Optional['BaseProxyConfig'] - def __init__(self, client: 'MaubotMatrixClient', plugin_instance_id: str, log: Logger, - config: Optional['BaseProxyConfig'], db_base_path: str) -> None: + def __init__(self, client: 'MaubotMatrixClient', loop: AbstractEventLoop, http: ClientSession, + plugin_instance_id: str, log: Logger, config: Optional['BaseProxyConfig'], + db_base_path: str) -> None: self.client = client + self.loop = loop + self.http = http self.id = plugin_instance_id self.log = log self.config = config From 28d7731e704d4cc035d83074e662535605bad020 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 1 Nov 2018 12:28:56 +0200 Subject: [PATCH 018/101] Fix bugs in init and plugin upload filenames --- maubot/__main__.py | 2 +- maubot/instance.py | 5 ++++- maubot/loader/zip.py | 2 +- maubot/management/api/plugin.py | 13 +++++++++---- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/maubot/__main__.py b/maubot/__main__.py index 5d5daec..9f8cafe 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -58,7 +58,7 @@ loop = asyncio.get_event_loop() init_db(db_session) clients = init_client(loop) -init_plugin_instance_class(db_session, config) +init_plugin_instance_class(db_session, config, loop) management_api = init_management(config, loop) server = MaubotServer(config, management_api, loop) diff --git a/maubot/instance.py b/maubot/instance.py index 924bf5a..51c9dcd 100644 --- a/maubot/instance.py +++ b/maubot/instance.py @@ -129,7 +129,10 @@ class PluginInstance: return self.log.debug("Stopping plugin instance...") self.running = False - await self.plugin.stop() + try: + await self.plugin.stop() + except Exception: + self.log.exception("Failed to stop instance") self.plugin = None @classmethod diff --git a/maubot/loader/zip.py b/maubot/loader/zip.py index c811848..a4f1d6e 100644 --- a/maubot/loader/zip.py +++ b/maubot/loader/zip.py @@ -151,7 +151,7 @@ class ZippedPluginLoader(PluginLoader): def _get_importer(self, reset_cache: bool = False) -> zipimporter: try: - if not self._importer: + if not self._importer or self._importer.archive != self.path: self._importer = zipimporter(self.path) if reset_cache: self._importer.reset_cache() diff --git a/maubot/management/api/plugin.py b/maubot/management/api/plugin.py index d07030c..03fb609 100644 --- a/maubot/management/api/plugin.py +++ b/maubot/management/api/plugin.py @@ -15,8 +15,10 @@ # along with this program. If not, see . from aiohttp import web from io import BytesIO +from time import time import traceback import os.path +import re from ...loader import PluginLoader, ZippedPluginLoader, MaubotZipImportError from .responses import (ErrPluginNotFound, ErrPluginInUse, plugin_import_error, @@ -81,11 +83,14 @@ async def upload_new_plugin(content: bytes, pid: str, version: str) -> web.Respo async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, new_version: str ) -> web.Response: dirname = os.path.dirname(plugin.path) - filename = os.path.basename(plugin.path) - if plugin.version in filename: - filename = filename.replace(plugin.version, new_version) + old_filename = os.path.basename(plugin.path) + if plugin.version in old_filename: + filename = old_filename.replace(plugin.version, new_version) + if filename == old_filename: + filename = re.sub(f"{re.escape(plugin.version)}(-ts[0-9]+)?", + f"{new_version}-ts{int(time())}", old_filename) else: - filename = filename.rstrip(".mbp") + filename = old_filename.rstrip(".mbp") filename = f"{filename}-v{new_version}.mbp" path = os.path.join(dirname, filename) with open(path, "wb") as p: From b39225119b8e535d91f5c916b021d0315f018113 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 1 Nov 2018 12:34:08 +0200 Subject: [PATCH 019/101] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 97e4b33..aecceac 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Matrix room: [#maubot:maunium.net](https://matrix.to/#/#maubot:maunium.net) * [media](https://github.com/maubot/media) - A bot that replies with the MXC URI of images you send it. * [dice](https://github.com/maubot/dice) - A combined dice rolling and calculator bot. * [karma](https://github.com/maubot/karma) - A user karma tracker bot. +* [xkcd](https://github.com/maubot/xkcd) - A bot to view xkcd comics. ### Upcoming * rss - A bot that posts new RSS entries to rooms. From e9d48e863c0011a790cb5f8e5745d2ebad1a6dc6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 1 Nov 2018 12:41:53 +0200 Subject: [PATCH 020/101] Install py3-bcrypt and py3-cffi from apk in docker image --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 91d5bf9..d9a43f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,8 @@ RUN apk add --no-cache \ py3-aiohttp \ py3-sqlalchemy \ py3-attrs \ + py3-bcrypt \ + py3-cffi \ ca-certificates \ && pip3 install -r requirements.txt From e48cd26cb01219973c730c9583c9a6ab871469d0 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 1 Nov 2018 12:56:24 +0200 Subject: [PATCH 021/101] Fix docker-run and login endpoint --- docker-run.sh | 11 ++++++----- maubot/management/api/auth.py | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docker-run.sh b/docker-run.sh index ffeab63..424953a 100755 --- a/docker-run.sh +++ b/docker-run.sh @@ -5,19 +5,20 @@ cd /opt/maubot # Replace database path in config. sed -i "s#sqlite:///maubot.db#sqlite:////data/maubot.db#" /data/config.yaml sed -i "s#- ./plugins#- /data/plugins#" /data/config.yaml +sed -i "s#upload: ./plugins#upload: /data/plugins#" /data/config.yaml +sed -i "s#trash: ./trash#trash: /data/trash#" /data/config.yaml +sed -i "s#db: ./plugins#trash: /data/dbs#" /data/config.yaml sed -i "s#./logs/maubot.log#/var/log/maubot/maubot.log#" /data/config.yaml -mkdir -p /var/log/maubot +mkdir -p /var/log/maubot /data/plugins /data/trash /data/dbs # Check that database is in the right state alembic -x config=/data/config.yaml upgrade head if [ ! -f /data/config.yaml ]; then cp example-config.yaml /data/config.yaml - echo "Didn't find a config file." - echo "Copied default config file to /data/config.yaml" - echo "Modify that config file to your liking." - echo "Start the container again after that to generate the registration file." + echo "Config file not found. Example config copied to /data/config.yaml" + echo "Please modify the config file to your liking and restart the container." exit fi diff --git a/maubot/management/api/auth.py b/maubot/management/api/auth.py index 1b0bcf3..fcca6fb 100644 --- a/maubot/management/api/auth.py +++ b/maubot/management/api/auth.py @@ -43,7 +43,7 @@ async def login(request: web.Request) -> web.Response: except json.JSONDecodeError: return ErrBodyNotJSON secret = data.get("secret") - if secret and config["server.unshared_secret"] == secret: + if secret and get_config()["server.unshared_secret"] == secret: user = data.get("user") or "root" return web.json_response({ "token": create_token(user), @@ -51,7 +51,7 @@ async def login(request: web.Request) -> web.Response: username = data.get("username") password = data.get("password") - if config.check_password(username, password): + if get_config().check_password(username, password): return web.json_response({ "token": create_token(username), }) From cbeff0c0cb75d6e3b004cfdbfade47e5a8d397c3 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 1 Nov 2018 12:59:22 +0200 Subject: [PATCH 022/101] Add created at timestamp to tokens for future-proofing --- maubot/management/api/auth.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/maubot/management/api/auth.py b/maubot/management/api/auth.py index fcca6fb..b813a7c 100644 --- a/maubot/management/api/auth.py +++ b/maubot/management/api/auth.py @@ -13,9 +13,11 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from aiohttp import web +from time import time import json +from aiohttp import web + from mautrix.types import UserID from mautrix.util.signed_token import sign_token, verify_token @@ -47,6 +49,7 @@ async def login(request: web.Request) -> web.Response: user = data.get("user") or "root" return web.json_response({ "token": create_token(user), + "created_at": int(time()), }) username = data.get("username") @@ -54,6 +57,7 @@ async def login(request: web.Request) -> web.Response: if get_config().check_password(username, password): return web.json_response({ "token": create_token(username), + "created_at": int(time()), }) return ErrBadAuth From bc87b2a02b8fdd28d13d7c80d1eeb125d5c3a49a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 1 Nov 2018 18:11:54 +0200 Subject: [PATCH 023/101] Refactor things and implement instance API --- maubot/__main__.py | 49 ++++++--------- maubot/client.py | 28 ++++++--- maubot/db.py | 21 +++++-- maubot/instance.py | 78 +++++++++++++++++------- maubot/loader/abc.py | 2 +- maubot/loader/zip.py | 7 +++ maubot/management/api/__init__.py | 2 + maubot/management/api/client.py | 44 +++++++++++--- maubot/management/api/instance.py | 79 +++++++++++++++++-------- maubot/management/api/responses.py | 25 ++++++++ maubot/management/api/spec.yaml | 3 + maubot/management/frontend/src/index.js | 2 +- maubot/plugin_base.py | 5 +- setup.py | 4 ++ 14 files changed, 249 insertions(+), 100 deletions(-) diff --git a/maubot/__main__.py b/maubot/__main__.py index 9f8cafe..3e18a68 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -13,8 +13,6 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sqlalchemy import orm -import sqlalchemy as sql import logging.config import argparse import asyncio @@ -23,11 +21,11 @@ import copy import sys from .config import Config -from .db import Base, init as init_db +from .db import init as init_db from .server import MaubotServer -from .client import Client, init as init_client -from .loader import ZippedPluginLoader -from .instance import PluginInstance, init as init_plugin_instance_class +from .client import Client, init as init_client_class +from .loader.zip import init as init_zip_loader +from .instance import init as init_plugin_instance_class from .management.api import init as init_management from .__meta__ import __version__ @@ -46,57 +44,48 @@ config.update() logging.config.dictConfig(copy.deepcopy(config["logging"])) log = logging.getLogger("maubot.init") -log.debug(f"Initializing maubot {__version__}") - -db_engine: sql.engine.Engine = sql.create_engine(config["database"]) -db_factory = orm.sessionmaker(bind=db_engine) -db_session = orm.scoping.scoped_session(db_factory) -Base.metadata.bind = db_engine -Base.metadata.create_all() +log.info(f"Initializing maubot {__version__}") loop = asyncio.get_event_loop() -init_db(db_session) -clients = init_client(loop) -init_plugin_instance_class(db_session, config, loop) +init_zip_loader(config) +db_session = init_db(config) +clients = init_client_class(db_session, loop) +plugins = init_plugin_instance_class(db_session, config, loop) management_api = init_management(config, loop) server = MaubotServer(config, management_api, loop) -ZippedPluginLoader.trash_path = config["plugin_directories.trash"] -ZippedPluginLoader.directories = config["plugin_directories.load"] -ZippedPluginLoader.load_all() - -plugins = PluginInstance.all() - for plugin in plugins: plugin.load() signal.signal(signal.SIGINT, signal.default_int_handler) signal.signal(signal.SIGTERM, signal.default_int_handler) -stop = False - async def periodic_commit(): - while not stop: + while True: await asyncio.sleep(60) db_session.commit() +periodic_commit_task: asyncio.Future = None + try: - log.debug("Starting server") + log.info("Starting server") loop.run_until_complete(server.start()) - log.debug("Starting clients and plugins") + log.info("Starting clients and plugins") loop.run_until_complete(asyncio.gather(*[client.start() for client in clients])) - log.debug("Startup actions complete, running forever") - loop.run_until_complete(periodic_commit()) + log.info("Startup actions complete, running forever") + periodic_commit_task = asyncio.ensure_future(periodic_commit(), loop=loop) loop.run_forever() except KeyboardInterrupt: log.debug("Interrupt received, stopping HTTP clients/servers and saving database") - stop = True + if periodic_commit_task is not None: + periodic_commit_task.cancel() for client in Client.cache.values(): client.stop() db_session.commit() loop.run_until_complete(server.stop()) + loop.close() log.debug("Everything stopped, shutting down") sys.exit(0) diff --git a/maubot/client.py b/maubot/client.py index dee1d8c..684eaea 100644 --- a/maubot/client.py +++ b/maubot/client.py @@ -14,10 +14,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from typing import Dict, List, Optional, Set, TYPE_CHECKING -from aiohttp import ClientSession import asyncio import logging +from sqlalchemy.orm import Session +from aiohttp import ClientSession + from mautrix.errors import MatrixInvalidToken, MatrixRequestError from mautrix.types import (UserID, SyncToken, FilterID, ContentURI, StrippedStateEvent, Membership, EventType, Filter, RoomFilter, RoomEventFilter) @@ -32,6 +34,7 @@ log = logging.getLogger("maubot.client") class Client: + db: Session = None log: logging.Logger = None loop: asyncio.AbstractEventLoop = None cache: Dict[UserID, 'Client'] = {} @@ -73,12 +76,12 @@ class Client: user_id = await self.client.whoami() except MatrixInvalidToken as e: self.log.error(f"Invalid token: {e}. Disabling client") - self.enabled = False + self.db_instance.enabled = False return except MatrixRequestError: if try_n >= 5: self.log.exception("Failed to get /account/whoami, disabling client") - self.enabled = False + self.db_instance.enabled = False else: self.log.exception(f"Failed to get /account/whoami, " f"retrying in {(try_n + 1) * 10}s") @@ -86,7 +89,7 @@ class Client: return if user_id != self.id: self.log.error(f"User ID mismatch: expected {self.id}, but got {user_id}") - self.enabled = False + self.db_instance.enabled = False return if not self.filter_id: self.filter_id = await self.client.create_filter(Filter( @@ -100,8 +103,7 @@ class Client: await self.client.set_displayname(self.displayname) if self.avatar_url != "disable": await self.client.set_avatar_url(self.avatar_url) - if self.sync: - self.client.start(self.filter_id) + self.start_sync() self.started = True self.log.info("Client started, starting plugin instances...") await self.start_plugins() @@ -110,12 +112,19 @@ class Client: await asyncio.gather(*[plugin.start() for plugin in self.references], loop=self.loop) async def stop_plugins(self) -> None: - await asyncio.gather(*[plugin.stop() for plugin in self.references if plugin.running], + await asyncio.gather(*[plugin.stop() for plugin in self.references if plugin.started], loop=self.loop) + def start_sync(self) -> None: + if self.sync: + self.client.start(self.filter_id) + + def stop_sync(self) -> None: + self.client.stop() + def stop(self) -> None: self.started = False - self.client.stop() + self.stop_sync() def to_dict(self) -> dict: return { @@ -233,7 +242,8 @@ class Client: # endregion -def init(loop: asyncio.AbstractEventLoop) -> List[Client]: +def init(db: Session, loop: asyncio.AbstractEventLoop) -> List[Client]: + Client.db = db Client.http_client = ClientSession(loop=loop) Client.loop = loop return Client.all() diff --git a/maubot/db.py b/maubot/db.py index d53658c..2373fb2 100644 --- a/maubot/db.py +++ b/maubot/db.py @@ -13,12 +13,17 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import cast + from sqlalchemy import Column, String, Boolean, ForeignKey, Text -from sqlalchemy.orm import Query, scoped_session +from sqlalchemy.orm import Query, Session, sessionmaker, scoped_session from sqlalchemy.ext.declarative import declarative_base +import sqlalchemy as sql from mautrix.types import UserID, FilterID, SyncToken, ContentURI +from .config import Config + Base: declarative_base = declarative_base() @@ -54,6 +59,14 @@ class DBClient(Base): avatar_url: ContentURI = Column(String(255), nullable=False, default="") -def init(session: scoped_session) -> None: - DBPlugin.query = session.query_property() - DBClient.query = session.query_property() +def init(config: Config) -> Session: + db_engine: sql.engine.Engine = sql.create_engine(config["database"]) + db_factory = sessionmaker(bind=db_engine) + db_session = scoped_session(db_factory) + Base.metadata.bind = db_engine + Base.metadata.create_all() + + DBPlugin.query = db_session.query_property() + DBClient.query = db_session.query_property() + + return cast(Session, db_session) diff --git a/maubot/instance.py b/maubot/instance.py index 51c9dcd..6e3590f 100644 --- a/maubot/instance.py +++ b/maubot/instance.py @@ -48,13 +48,14 @@ class PluginInstance: client: Client plugin: Plugin config: BaseProxyConfig - running: bool + base_cfg: RecursiveDict[CommentedMap] + started: bool def __init__(self, db_instance: DBPlugin): self.db_instance = db_instance self.log = logging.getLogger(f"maubot.plugin.{self.id}") self.config = None - self.running = False + self.started = False self.cache[self.id] = self def to_dict(self) -> dict: @@ -62,7 +63,7 @@ class PluginInstance: "id": self.id, "type": self.type, "enabled": self.enabled, - "running": self.running, + "started": self.started, "primary_user": self.primary_user, } @@ -71,19 +72,26 @@ class PluginInstance: self.loader = PluginLoader.find(self.type) except KeyError: self.log.error(f"Failed to find loader for type {self.type}") - self.enabled = False + self.db_instance.enabled = False return self.client = Client.get(self.primary_user) if not self.client: self.log.error(f"Failed to get client for user {self.primary_user}") - self.enabled = False + self.db_instance.enabled = False return self.log.debug("Plugin instance dependencies loaded") self.loader.references.add(self) self.client.references.add(self) def delete(self) -> None: - self.loader.references.remove(self) + if self.loader is not None: + self.loader.references.remove(self) + if self.client is not None: + self.client.references.remove(self) + try: + del self.cache[self.id] + except KeyError: + pass self.db.delete(self.db_instance) # TODO delete plugin db @@ -96,7 +104,7 @@ class PluginInstance: self.db_instance.config = buf.getvalue() async def start(self) -> None: - if self.running: + if self.started: self.log.warning("Ignoring start() call to already started plugin") return elif not self.enabled: @@ -107,28 +115,28 @@ class PluginInstance: if config_class: try: base = await self.loader.read_file("base-config.yaml") - base_file = RecursiveDict(yaml.load(base.decode("utf-8")), CommentedMap) + self.base_cfg = RecursiveDict(yaml.load(base.decode("utf-8")), CommentedMap) except (FileNotFoundError, KeyError): - base_file = None - self.config = config_class(self.load_config, lambda: base_file, self.save_config) + self.base_cfg = None + self.config = config_class(self.load_config, lambda: self.base_cfg, self.save_config) self.plugin = cls(self.client.client, self.loop, self.client.http_client, self.id, self.log, self.config, self.mb_config["plugin_directories.db"]) try: await self.plugin.start() except Exception: self.log.exception("Failed to start instance") - self.enabled = False + self.db_instance.enabled = False return - self.running = True + self.started = True self.log.info(f"Started instance of {self.loader.id} v{self.loader.version} " f"with user {self.client.id}") async def stop(self) -> None: - if not self.running: + if not self.started: self.log.warning("Ignoring stop() call to non-running plugin") return self.log.debug("Stopping plugin instance...") - self.running = False + self.started = False try: await self.plugin.stop() except Exception: @@ -150,6 +158,37 @@ class PluginInstance: def all(cls) -> List['PluginInstance']: return [cls.get(plugin.id, plugin) for plugin in DBPlugin.query.all()] + def update_id(self, new_id: str) -> None: + if new_id is not None and new_id != self.id: + self.db_instance.id = new_id + + def update_config(self, config: str) -> None: + if not config or self.db_instance.config == config: + return + self.db_instance.config = config + if self.started and self.plugin is not None: + self.plugin.on_external_config_update() + + async def update_primary_user(self, primary_user: UserID) -> bool: + client = Client.get(primary_user) + if not client: + return False + await self.stop() + self.db_instance.primary_user = client.id + self.client.references.remove(self) + self.client = client + await self.start() + self.log.debug(f"Primary user switched to {self.client.id}") + return True + + async def update_started(self, started: bool) -> None: + if started is not None and started != self.started: + await (self.start() if started else self.stop()) + + def update_enabled(self, enabled: bool) -> None: + if enabled is not None and enabled != self.enabled: + self.db_instance.enabled = enabled + # region Properties @property @@ -168,22 +207,15 @@ class PluginInstance: def enabled(self) -> bool: return self.db_instance.enabled - @enabled.setter - def enabled(self, value: bool) -> None: - self.db_instance.enabled = value - @property def primary_user(self) -> UserID: return self.db_instance.primary_user - @primary_user.setter - def primary_user(self, value: UserID) -> None: - self.db_instance.primary_user = value - # endregion -def init(db: Session, config: Config, loop: AbstractEventLoop): +def init(db: Session, config: Config, loop: AbstractEventLoop) -> List[PluginInstance]: PluginInstance.db = db PluginInstance.mb_config = config PluginInstance.loop = loop + return PluginInstance.all() diff --git a/maubot/loader/abc.py b/maubot/loader/abc.py index 4dbb299..111e469 100644 --- a/maubot/loader/abc.py +++ b/maubot/loader/abc.py @@ -61,7 +61,7 @@ class PluginLoader(ABC): async def stop_instances(self) -> None: await asyncio.gather(*[instance.stop() for instance - in self.references if instance.running]) + in self.references if instance.started]) async def start_instances(self) -> None: await asyncio.gather(*[instance.start() for instance diff --git a/maubot/loader/zip.py b/maubot/loader/zip.py index a4f1d6e..b8c54ea 100644 --- a/maubot/loader/zip.py +++ b/maubot/loader/zip.py @@ -23,6 +23,7 @@ import os from ..lib.zipimport import zipimporter, ZipImportError from ..plugin_base import Plugin +from ..config import Config from .abc import PluginLoader, PluginClass, IDConflictError @@ -264,3 +265,9 @@ class ZippedPluginLoader(PluginLoader): except IDConflictError: cls.log.error(f"Duplicate plugin ID at {path}, trashing...") cls.trash(path) + + +def init(config: Config) -> None: + ZippedPluginLoader.trash_path = config["plugin_directories.trash"] + ZippedPluginLoader.directories = config["plugin_directories.load"] + ZippedPluginLoader.load_all() diff --git a/maubot/management/api/__init__.py b/maubot/management/api/__init__.py index e66b527..d8d1917 100644 --- a/maubot/management/api/__init__.py +++ b/maubot/management/api/__init__.py @@ -21,6 +21,8 @@ from .base import routes, set_config from .middleware import auth, error from .auth import web as _ from .plugin import web as _ +from .instance import web as _ +from .client import web as _ def init(cfg: Config, loop: AbstractEventLoop) -> web.Application: diff --git a/maubot/management/api/client.py b/maubot/management/api/client.py index a7ec14e..8f5eed0 100644 --- a/maubot/management/api/client.py +++ b/maubot/management/api/client.py @@ -13,27 +13,57 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from json import JSONDecodeError + from aiohttp import web +from mautrix.types import UserID + +from ...client import Client from .base import routes -from .responses import ErrNotImplemented +from .responses import ErrNotImplemented, ErrClientNotFound, ErrBodyNotJSON @routes.get("/clients") -def get_clients(request: web.Request) -> web.Response: - return ErrNotImplemented +async def get_clients(request: web.Request) -> web.Response: + return web.json_response([client.to_dict() for client in Client.cache.values()]) @routes.get("/client/{id}") -def get_client(request: web.Request) -> web.Response: +async def get_client(request: web.Request) -> web.Response: + user_id = request.match_info.get("id", None) + client = Client.get(user_id, None) + if not client: + return ErrClientNotFound + return web.json_response(client.to_dict()) + + +async def create_client(user_id: UserID, data: dict) -> web.Response: + return ErrNotImplemented + + +async def update_client(client: Client, data: dict) -> web.Response: return ErrNotImplemented @routes.put("/client/{id}") -def update_client(request: web.Request) -> web.Response: - return ErrNotImplemented +async def update_client(request: web.Request) -> web.Response: + user_id = request.match_info.get("id", None) + client = Client.get(user_id, None) + try: + data = await request.json() + except JSONDecodeError: + return ErrBodyNotJSON + if not client: + return await create_client(user_id, data) + else: + return await update_client(client, data) @routes.delete("/client/{id}") -def delete_client(request: web.Request) -> web.Response: +async def delete_client(request: web.Request) -> web.Response: + user_id = request.match_info.get("id", None) + client = Client.get(user_id, None) + if not client: + return ErrClientNotFound return ErrNotImplemented diff --git a/maubot/management/api/instance.py b/maubot/management/api/instance.py index 064c3f8..166108a 100644 --- a/maubot/management/api/instance.py +++ b/maubot/management/api/instance.py @@ -13,57 +13,88 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from aiohttp import web from json import JSONDecodeError -from mautrix.types import UserID +from aiohttp import web -from ...db import DBClient +from ...db import DBPlugin +from ...instance import PluginInstance +from ...loader import PluginLoader from ...client import Client from .base import routes -from .responses import ErrNotImplemented, ErrClientNotFound, ErrBodyNotJSON +from .responses import (ErrInstanceNotFound, ErrBodyNotJSON, RespDeleted, ErrPrimaryUserNotFound, + ErrPluginTypeRequired, ErrPrimaryUserRequired, ErrPluginTypeNotFound) @routes.get("/instances") async def get_instances(_: web.Request) -> web.Response: - return web.json_response([client.to_dict() for client in Client.cache.values()]) + return web.json_response([instance.to_dict() for instance in PluginInstance.cache.values()]) @routes.get("/instance/{id}") async def get_instance(request: web.Request) -> web.Response: - user_id = request.match_info.get("id", None) - client = Client.get(user_id, None) - if not client: - return ErrClientNotFound - return web.json_response(client.to_dict()) + instance_id = request.match_info.get("id", "").lower() + instance = PluginInstance.get(instance_id, None) + if not instance: + return ErrInstanceNotFound + return web.json_response(instance.to_dict()) -async def create_instance(user_id: UserID, data: dict) -> web.Response: - return ErrNotImplemented +async def create_instance(instance_id: str, data: dict) -> web.Response: + plugin_type = data.get("type", None) + primary_user = data.get("primary_user", None) + if not plugin_type: + return ErrPluginTypeRequired + elif not primary_user: + return ErrPrimaryUserRequired + elif not Client.get(primary_user): + return ErrPrimaryUserNotFound + try: + PluginLoader.find(plugin_type) + except KeyError: + return ErrPluginTypeNotFound + db_instance = DBPlugin(id=instance_id, type=plugin_type, enabled=data.get("enabled", True), + primary_user=primary_user, config=data.get("config", "")) + instance = PluginInstance(db_instance) + instance.load() + PluginInstance.db.add(db_instance) + PluginInstance.db.commit() + await instance.start() + return web.json_response(instance.to_dict()) -async def update_instance(client: Client, data: dict) -> web.Response: - return ErrNotImplemented +async def update_instance(instance: PluginInstance, data: dict) -> web.Response: + if not await instance.update_primary_user(data.get("primary_user")): + return ErrPrimaryUserNotFound + instance.update_id(data.get("id", None)) + instance.update_enabled(data.get("enabled", None)) + instance.update_config(data.get("config", None)) + await instance.update_started(data.get("started", None)) + instance.db.commit() + return web.json_response(instance.to_dict()) @routes.put("/instance/{id}") async def update_instance(request: web.Request) -> web.Response: - user_id = request.match_info.get("id", None) - client = Client.get(user_id, None) + instance_id = request.match_info.get("id", "").lower() + instance = PluginInstance.get(instance_id, None) try: data = await request.json() except JSONDecodeError: return ErrBodyNotJSON - if not client: - return await create_instance(user_id, data) + if not instance: + return await create_instance(instance_id, data) else: - return await update_instance(client, data) + return await update_instance(instance, data) @routes.delete("/instance/{id}") async def delete_instance(request: web.Request) -> web.Response: - user_id = request.match_info.get("id", None) - client = Client.get(user_id, None) - if not client: - return ErrClientNotFound - return ErrNotImplemented + instance_id = request.match_info.get("id", "").lower() + instance = PluginInstance.get(instance_id, None) + if not instance: + return ErrInstanceNotFound + if instance.started: + await instance.stop() + instance.delete() + return RespDeleted diff --git a/maubot/management/api/responses.py b/maubot/management/api/responses.py index ad3033b..16efecb 100644 --- a/maubot/management/api/responses.py +++ b/maubot/management/api/responses.py @@ -41,6 +41,31 @@ ErrClientNotFound = web.json_response({ "errcode": "client_not_found", }, status=HTTPStatus.NOT_FOUND) +ErrPrimaryUserNotFound = web.json_response({ + "error": "Client for given primary user not found", + "errcode": "primary_user_not_found", +}, status=HTTPStatus.NOT_FOUND) + +ErrInstanceNotFound = web.json_response({ + "error": "Plugin instance not found", + "errcode": "instance_not_found", +}, status=HTTPStatus.NOT_FOUND) + +ErrPluginTypeNotFound = web.json_response({ + "error": "Given plugin type not found", + "errcode": "plugin_type_not_found", +}, status=HTTPStatus.NOT_FOUND) + +ErrPluginTypeRequired = web.json_response({ + "error": "Plugin type is required when creating plugin instances", + "errcode": "plugin_type_required", +}, status=HTTPStatus.BAD_REQUEST) + +ErrPrimaryUserRequired = web.json_response({ + "error": "Primary user is required when creating plugin instances", + "errcode": "primary_user_required", +}, status=HTTPStatus.BAD_REQUEST) + ErrPathNotFound = web.json_response({ "error": "Resource not found", "errcode": "resource_not_found", diff --git a/maubot/management/api/spec.yaml b/maubot/management/api/spec.yaml index 8cfde21..7b7ebd9 100644 --- a/maubot/management/api/spec.yaml +++ b/maubot/management/api/spec.yaml @@ -346,6 +346,9 @@ components: primary_user: type: string example: '@putkiteippi:maunium.net' + config: + type: string + example: "YAML" MatrixClient: type: object properties: diff --git a/maubot/management/frontend/src/index.js b/maubot/management/frontend/src/index.js index 294a680..8f50972 100644 --- a/maubot/management/frontend/src/index.js +++ b/maubot/management/frontend/src/index.js @@ -15,7 +15,7 @@ // along with this program. If not, see . import React from "react" import ReactDOM from "react-dom" -import "./style/base" +import "./style/index.sass" import MaubotManager from "./MaubotManager" ReactDOM.render(, document.getElementById("root")) diff --git a/maubot/plugin_base.py b/maubot/plugin_base.py index 9b394f1..3925513 100644 --- a/maubot/plugin_base.py +++ b/maubot/plugin_base.py @@ -28,7 +28,6 @@ if TYPE_CHECKING: from .command_spec import CommandSpec from mautrix.util.config import BaseProxyConfig - DatabaseNotConfigured = ValueError("A database for this maubot instance has not been configured.") @@ -69,3 +68,7 @@ class Plugin(ABC): @classmethod def get_config_class(cls) -> Optional[Type['BaseProxyConfig']]: return None + + def on_external_config_update(self) -> None: + if self.config: + self.config.load_and_update() diff --git a/setup.py b/setup.py index 0d8dad0..2995b45 100644 --- a/setup.py +++ b/setup.py @@ -48,4 +48,8 @@ setuptools.setup( data_files=[ (".", ["example-config.yaml"]), ], + package_data={ + "maubot": ["management/frontend/build/*", "management/frontend/build/static/css/*", + "management/frontend/build/static/js/*"], + }, ) From 383c9ce5ec6c090112f28717ec59ad0058b3aa26 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 1 Nov 2018 23:31:30 +0200 Subject: [PATCH 024/101] Implement client API --- maubot/__main__.py | 11 ++-- maubot/client.py | 83 +++++++++++++++++++++--------- maubot/management/api/client.py | 66 +++++++++++++++++++++--- maubot/management/api/responses.py | 53 ++++++++++++++----- 4 files changed, 163 insertions(+), 50 deletions(-) diff --git a/maubot/__main__.py b/maubot/__main__.py index 3e18a68..3929095 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -74,18 +74,21 @@ try: log.info("Starting server") loop.run_until_complete(server.start()) log.info("Starting clients and plugins") - loop.run_until_complete(asyncio.gather(*[client.start() for client in clients])) + loop.run_until_complete(asyncio.gather(*[client.start() for client in clients], loop=loop)) log.info("Startup actions complete, running forever") periodic_commit_task = asyncio.ensure_future(periodic_commit(), loop=loop) loop.run_forever() except KeyboardInterrupt: - log.debug("Interrupt received, stopping HTTP clients/servers and saving database") + log.info("Interrupt received, stopping HTTP clients/servers and saving database") if periodic_commit_task is not None: periodic_commit_task.cancel() - for client in Client.cache.values(): - client.stop() + log.debug("Stopping clients") + loop.run_until_complete(asyncio.gather(*[client.stop() for client in Client.cache.values()], + loop=loop)) db_session.commit() + log.debug("Stopping server") loop.run_until_complete(server.stop()) + log.debug("Closing event loop") loop.close() log.debug("Everything stopped, shutting down") sys.exit(0) diff --git a/maubot/client.py b/maubot/client.py index 684eaea..91fcb0c 100644 --- a/maubot/client.py +++ b/maubot/client.py @@ -92,7 +92,7 @@ class Client: self.db_instance.enabled = False return if not self.filter_id: - self.filter_id = await self.client.create_filter(Filter( + self.db_instance.filter_id = await self.client.create_filter(Filter( room=RoomFilter( timeline=RoomEventFilter( limit=50, @@ -122,9 +122,18 @@ class Client: def stop_sync(self) -> None: self.client.stop() - def stop(self) -> None: - self.started = False - self.stop_sync() + async def stop(self) -> None: + if self.started: + self.started = False + await self.stop_plugins() + self.stop_sync() + + def delete(self) -> None: + try: + del self.cache[self.id] + except KeyError: + pass + self.db.delete(self.db_instance) def to_dict(self) -> dict: return { @@ -158,6 +167,44 @@ class Client: if evt.state_key == self.id and evt.content.membership == Membership.INVITE: await self.client.join_room(evt.room_id) + async def update_started(self, started: bool) -> None: + if started is None or started == self.started: + return + if started: + await self.start() + else: + await self.stop() + + async def update_displayname(self, displayname: str) -> None: + if not displayname or displayname == self.displayname: + return + self.db_instance.displayname = displayname + await self.client.set_displayname(self.displayname) + + async def update_avatar_url(self, avatar_url: ContentURI) -> None: + if not avatar_url or avatar_url == self.avatar_url: + return + self.db_instance.avatar_url = avatar_url + await self.client.set_avatar_url(self.avatar_url) + + async def update_access_details(self, access_token: str, homeserver: str) -> None: + if not access_token and not homeserver: + return + elif access_token == self.access_token and homeserver == self.homeserver: + return + new_client = MaubotMatrixClient(mxid=self.id, base_url=homeserver or self.homeserver, + token=access_token or self.access_token, loop=self.loop, + client_session=self.http_client, log=self.log) + mxid = await new_client.whoami() + if mxid != self.id: + raise ValueError("MXID mismatch") + new_client.store = self.db_instance + self.stop_sync() + self.client = new_client + self.db_instance.homeserver = homeserver + self.db_instance.access_token = access_token + self.start_sync() + # region Properties @property @@ -172,11 +219,6 @@ class Client: def access_token(self) -> str: return self.db_instance.access_token - @access_token.setter - def access_token(self, value: str) -> None: - self.client.api.token = value - self.db_instance.access_token = value - @property def enabled(self) -> bool: return self.db_instance.enabled @@ -189,25 +231,24 @@ class Client: def next_batch(self) -> SyncToken: return self.db_instance.next_batch - @next_batch.setter - def next_batch(self, value: SyncToken) -> None: - self.db_instance.next_batch = value - @property def filter_id(self) -> FilterID: return self.db_instance.filter_id - @filter_id.setter - def filter_id(self, value: FilterID) -> None: - self.db_instance.filter_id = value - @property def sync(self) -> bool: return self.db_instance.sync @sync.setter def sync(self, value: bool) -> None: + if value == self.db_instance.sync: + return self.db_instance.sync = value + if self.started: + if value: + self.start_sync() + else: + self.stop_sync() @property def autojoin(self) -> bool: @@ -227,18 +268,10 @@ class Client: def displayname(self) -> str: return self.db_instance.displayname - @displayname.setter - def displayname(self, value: str) -> None: - self.db_instance.displayname = value - @property def avatar_url(self) -> ContentURI: return self.db_instance.avatar_url - @avatar_url.setter - def avatar_url(self, value: ContentURI) -> None: - self.db_instance.avatar_url = value - # endregion diff --git a/maubot/management/api/client.py b/maubot/management/api/client.py index 8f5eed0..84d5b37 100644 --- a/maubot/management/api/client.py +++ b/maubot/management/api/client.py @@ -17,15 +17,20 @@ from json import JSONDecodeError from aiohttp import web -from mautrix.types import UserID +from mautrix.types import UserID, SyncToken, FilterID +from mautrix.errors import MatrixRequestError, MatrixInvalidToken +from mautrix.client import Client as MatrixClient +from ...db import DBClient from ...client import Client from .base import routes -from .responses import ErrNotImplemented, ErrClientNotFound, ErrBodyNotJSON +from .responses import (RespDeleted, ErrClientNotFound, ErrBodyNotJSON, ErrClientInUse, + ErrBadClientAccessToken, ErrBadClientAccessDetails, ErrMXIDMismatch, + ErrUserExists) @routes.get("/clients") -async def get_clients(request: web.Request) -> web.Response: +async def get_clients(_: web.Request) -> web.Response: return web.json_response([client.to_dict() for client in Client.cache.values()]) @@ -39,17 +44,59 @@ async def get_client(request: web.Request) -> web.Response: async def create_client(user_id: UserID, data: dict) -> web.Response: - return ErrNotImplemented + homeserver = data.get("homeserver", None) + access_token = data.get("access_token", None) + new_client = MatrixClient(base_url=homeserver, token=access_token, loop=Client.loop, + client_session=Client.http_client) + try: + mxid = await new_client.whoami() + except MatrixInvalidToken: + return ErrBadClientAccessToken + except MatrixRequestError: + return ErrBadClientAccessDetails + if user_id == "new": + existing_client = Client.get(mxid, None) + if existing_client is not None: + return ErrUserExists + elif mxid != user_id: + return ErrMXIDMismatch + db_instance = DBClient(id=user_id, homeserver=homeserver, access_token=access_token, + enabled=data.get("enabled", True), next_batch=SyncToken(""), + filter_id=FilterID(""), sync=data.get("sync", True), + autojoin=data.get("autojoin", True), + displayname=data.get("displayname", ""), + avatar_url=data.get("avatar_url", "")) + client = Client(db_instance) + Client.db.add(db_instance) + Client.db.commit() + await client.start() + return web.json_response(client.to_dict()) async def update_client(client: Client, data: dict) -> web.Response: - return ErrNotImplemented + try: + await client.update_access_details(data.get("access_token", None), + data.get("homeserver", None)) + except MatrixInvalidToken: + return ErrBadClientAccessToken + except MatrixRequestError: + return ErrBadClientAccessDetails + except ValueError: + return ErrMXIDMismatch + await client.update_avatar_url(data.get("avatar_url", None)) + await client.update_displayname(data.get("displayname", None)) + await client.update_started(data.get("started", None)) + client.enabled = data.get("enabled", client.enabled) + client.autojoin = data.get("autojoin", client.autojoin) + client.sync = data.get("sync", client.sync) + return web.json_response(client.to_dict()) @routes.put("/client/{id}") async def update_client(request: web.Request) -> web.Response: user_id = request.match_info.get("id", None) - client = Client.get(user_id, None) + # /client/new always creates a new client + client = Client.get(user_id, None) if user_id != "new" else None try: data = await request.json() except JSONDecodeError: @@ -66,4 +113,9 @@ async def delete_client(request: web.Request) -> web.Response: client = Client.get(user_id, None) if not client: return ErrClientNotFound - return ErrNotImplemented + if len(client.references) > 0: + return ErrClientInUse + if client.started: + await client.stop() + client.delete() + return RespDeleted diff --git a/maubot/management/api/responses.py b/maubot/management/api/responses.py index 16efecb..9fd4c40 100644 --- a/maubot/management/api/responses.py +++ b/maubot/management/api/responses.py @@ -16,6 +16,36 @@ from http import HTTPStatus from aiohttp import web +ErrBodyNotJSON = web.json_response({ + "error": "Request body is not JSON", + "errcode": "body_not_json", +}, status=HTTPStatus.BAD_REQUEST) + +ErrPluginTypeRequired = web.json_response({ + "error": "Plugin type is required when creating plugin instances", + "errcode": "plugin_type_required", +}, status=HTTPStatus.BAD_REQUEST) + +ErrPrimaryUserRequired = web.json_response({ + "error": "Primary user is required when creating plugin instances", + "errcode": "primary_user_required", +}, status=HTTPStatus.BAD_REQUEST) + +ErrBadClientAccessToken = web.json_response({ + "error": "Invalid access token", + "errcode": "bad_client_access_token", +}, status=HTTPStatus.BAD_REQUEST) + +ErrBadClientAccessDetails = web.json_response({ + "error": "Invalid homeserver or access token", + "errcode": "bad_client_access_details" +}, status=HTTPStatus.BAD_REQUEST) + +ErrMXIDMismatch = web.json_response({ + "error": "The Matrix user ID of the client and the user ID of the access token don't match", + "errcode": "mxid_mismatch", +}, status=HTTPStatus.BAD_REQUEST) + ErrBadAuth = web.json_response({ "error": "Invalid username or password", "errcode": "invalid_auth", @@ -56,16 +86,6 @@ ErrPluginTypeNotFound = web.json_response({ "errcode": "plugin_type_not_found", }, status=HTTPStatus.NOT_FOUND) -ErrPluginTypeRequired = web.json_response({ - "error": "Plugin type is required when creating plugin instances", - "errcode": "plugin_type_required", -}, status=HTTPStatus.BAD_REQUEST) - -ErrPrimaryUserRequired = web.json_response({ - "error": "Primary user is required when creating plugin instances", - "errcode": "primary_user_required", -}, status=HTTPStatus.BAD_REQUEST) - ErrPathNotFound = web.json_response({ "error": "Resource not found", "errcode": "resource_not_found", @@ -76,15 +96,20 @@ ErrMethodNotAllowed = web.json_response({ "errcode": "method_not_allowed", }, status=HTTPStatus.METHOD_NOT_ALLOWED) +ErrUserExists = web.json_response({ + "error": "There is already a client with the user ID of that token", + "errcode": "user_exists", +}, status=HTTPStatus.CONFLICT) + ErrPluginInUse = web.json_response({ "error": "Plugin instances of this type still exist", "errcode": "plugin_in_use", }, status=HTTPStatus.PRECONDITION_FAILED) -ErrBodyNotJSON = web.json_response({ - "error": "Request body is not JSON", - "errcode": "body_not_json", -}, status=HTTPStatus.BAD_REQUEST) +ErrClientInUse = web.json_response({ + "error": "Plugin instances with this client as their primary user still exist", + "errcode": "client_in_use", +}, status=HTTPStatus.PRECONDITION_FAILED) def plugin_import_error(error: str, stacktrace: str) -> web.Response: From 8b97134efd1e52ec41328eb683b6ea366587514e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 1 Nov 2018 23:57:24 +0200 Subject: [PATCH 025/101] Update spec and fix minor problems in implementation --- maubot/management/api/client.py | 26 +++++++--- maubot/management/api/instance.py | 11 ++-- maubot/management/api/plugin.py | 6 ++- maubot/management/api/spec.yaml | 86 ++++++++++++++++++++++++++++--- 4 files changed, 107 insertions(+), 22 deletions(-) diff --git a/maubot/management/api/client.py b/maubot/management/api/client.py index 84d5b37..9e6cac7 100644 --- a/maubot/management/api/client.py +++ b/maubot/management/api/client.py @@ -13,7 +13,9 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import Optional from json import JSONDecodeError +from http import HTTPStatus from aiohttp import web @@ -43,7 +45,7 @@ async def get_client(request: web.Request) -> web.Response: return web.json_response(client.to_dict()) -async def create_client(user_id: UserID, data: dict) -> web.Response: +async def _create_client(user_id: Optional[UserID], data: dict) -> web.Response: homeserver = data.get("homeserver", None) access_token = data.get("access_token", None) new_client = MatrixClient(base_url=homeserver, token=access_token, loop=Client.loop, @@ -54,7 +56,7 @@ async def create_client(user_id: UserID, data: dict) -> web.Response: return ErrBadClientAccessToken except MatrixRequestError: return ErrBadClientAccessDetails - if user_id == "new": + if user_id is None: existing_client = Client.get(mxid, None) if existing_client is not None: return ErrUserExists @@ -73,7 +75,7 @@ async def create_client(user_id: UserID, data: dict) -> web.Response: return web.json_response(client.to_dict()) -async def update_client(client: Client, data: dict) -> web.Response: +async def _update_client(client: Client, data: dict) -> web.Response: try: await client.update_access_details(data.get("access_token", None), data.get("homeserver", None)) @@ -89,22 +91,30 @@ async def update_client(client: Client, data: dict) -> web.Response: client.enabled = data.get("enabled", client.enabled) client.autojoin = data.get("autojoin", client.autojoin) client.sync = data.get("sync", client.sync) - return web.json_response(client.to_dict()) + return web.json_response(client.to_dict(), status=HTTPStatus.CREATED) + + +@routes.post("/client/new") +async def create_client(request: web.Request) -> web.Response: + try: + data = await request.json() + except JSONDecodeError: + return ErrBodyNotJSON + return await _create_client(None, data) @routes.put("/client/{id}") async def update_client(request: web.Request) -> web.Response: user_id = request.match_info.get("id", None) - # /client/new always creates a new client - client = Client.get(user_id, None) if user_id != "new" else None + client = Client.get(user_id, None) try: data = await request.json() except JSONDecodeError: return ErrBodyNotJSON if not client: - return await create_client(user_id, data) + return await _create_client(user_id, data) else: - return await update_client(client, data) + return await _update_client(client, data) @routes.delete("/client/{id}") diff --git a/maubot/management/api/instance.py b/maubot/management/api/instance.py index 166108a..2947247 100644 --- a/maubot/management/api/instance.py +++ b/maubot/management/api/instance.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from json import JSONDecodeError +from http import HTTPStatus from aiohttp import web @@ -40,7 +41,7 @@ async def get_instance(request: web.Request) -> web.Response: return web.json_response(instance.to_dict()) -async def create_instance(instance_id: str, data: dict) -> web.Response: +async def _create_instance(instance_id: str, data: dict) -> web.Response: plugin_type = data.get("type", None) primary_user = data.get("primary_user", None) if not plugin_type: @@ -60,10 +61,10 @@ async def create_instance(instance_id: str, data: dict) -> web.Response: PluginInstance.db.add(db_instance) PluginInstance.db.commit() await instance.start() - return web.json_response(instance.to_dict()) + return web.json_response(instance.to_dict(), status=HTTPStatus.CREATED) -async def update_instance(instance: PluginInstance, data: dict) -> web.Response: +async def _update_instance(instance: PluginInstance, data: dict) -> web.Response: if not await instance.update_primary_user(data.get("primary_user")): return ErrPrimaryUserNotFound instance.update_id(data.get("id", None)) @@ -83,9 +84,9 @@ async def update_instance(request: web.Request) -> web.Response: except JSONDecodeError: return ErrBodyNotJSON if not instance: - return await create_instance(instance_id, data) + return await _create_instance(instance_id, data) else: - return await update_instance(instance, data) + return await _update_instance(instance, data) @routes.delete("/instance/{id}") diff --git a/maubot/management/api/plugin.py b/maubot/management/api/plugin.py index 03fb609..87174ab 100644 --- a/maubot/management/api/plugin.py +++ b/maubot/management/api/plugin.py @@ -13,13 +13,15 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from aiohttp import web +from http import HTTPStatus from io import BytesIO from time import time import traceback import os.path import re +from aiohttp import web + from ...loader import PluginLoader, ZippedPluginLoader, MaubotZipImportError from .responses import (ErrPluginNotFound, ErrPluginInUse, plugin_import_error, plugin_reload_error, RespDeleted, RespOK, ErrUnsupportedPluginLoader) @@ -77,7 +79,7 @@ async def upload_new_plugin(content: bytes, pid: str, version: str) -> web.Respo except MaubotZipImportError as e: ZippedPluginLoader.trash(path) return plugin_import_error(str(e), traceback.format_exc()) - return web.json_response(plugin.to_dict()) + return web.json_response(plugin.to_dict(), status=HTTPStatus.CREATED) async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, new_version: str diff --git a/maubot/management/api/spec.yaml b/maubot/management/api/spec.yaml index 7b7ebd9..e89f18b 100644 --- a/maubot/management/api/spec.yaml +++ b/maubot/management/api/spec.yaml @@ -81,12 +81,10 @@ paths: application/json: schema: $ref: '#/components/schemas/Plugin' + 400: + $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' - 412: - description: >- - Instances of the uploaded plugin type are currently active, - therefore the plugin can't be updated requestBody: content: application/zip: @@ -131,6 +129,10 @@ paths: $ref: '#/components/responses/PluginNotFound' 412: description: One or more plugin instances of this type exist + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /plugin/{id}/reload: parameters: - name: id @@ -215,10 +217,16 @@ paths: description: Plugin instance edited 201: description: Plugin instance created + 400: + $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' 404: - $ref: '#/components/responses/InstanceNotFound' + description: The referenced client or plugin type could not be found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '/clients': get: @@ -236,6 +244,35 @@ paths: $ref: '#/components/schemas/MatrixClient' 401: $ref: '#/components/responses/Unauthorized' + /client/new: + post: + operationId: create_client + summary: Create a Matrix client + tags: [Clients] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClient' + responses: + 201: + description: Client created + content: + application/json: + schema: + $ref: '#/components/schemas/MatrixClient' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/ClientNotFound' + 409: + description: There is already a client with the user ID of that token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' '/client/{id}': parameters: - name: id @@ -281,10 +318,10 @@ paths: application/json: schema: $ref: '#/components/schemas/MatrixClient' + 400: + $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' - 404: - $ref: '#/components/responses/ClientNotFound' delete: operationId: delete_client summary: Delete a Matrix client @@ -298,23 +335,58 @@ paths: $ref: '#/components/responses/ClientNotFound' 412: description: One or more plugin instances with this as their primary client exist + content: + application/json: + schema: + $ref: '#/components/schemas/Error' components: responses: Unauthorized: description: Invalid or missing access token + content: + application/json: + schema: + $ref: '#/components/schemas/Error' PluginNotFound: description: Plugin not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' ClientNotFound: description: Client not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' InstanceNotFound: description: Plugin instance not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + BadRequest: + description: Bad request (e.g. bad request body) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' securitySchemes: bearer: type: http scheme: bearer description: Required authentication for all endpoints schemas: + Error: + type: object + properties: + error: + type: string + description: A human-readable error message + errcode: + type: string + description: A simple error code Plugin: type: object properties: From 1e37a56096d5ee8e5563b3d67ab5715d148a4308 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Nov 2018 00:09:42 +0200 Subject: [PATCH 026/101] Update version --- maubot/__meta__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maubot/__meta__.py b/maubot/__meta__.py index e305370..11b122d 100644 --- a/maubot/__meta__.py +++ b/maubot/__meta__.py @@ -1 +1 @@ -__version__ = "0.1.0.dev6" +__version__ = "0.1.0.dev6+management" From f5886ed34bb5763b0a6893519d0874899593be4e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Nov 2018 00:43:00 +0200 Subject: [PATCH 027/101] Fix creating clients --- maubot/management/api/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/maubot/management/api/client.py b/maubot/management/api/client.py index 9e6cac7..421ef51 100644 --- a/maubot/management/api/client.py +++ b/maubot/management/api/client.py @@ -48,8 +48,8 @@ async def get_client(request: web.Request) -> web.Response: async def _create_client(user_id: Optional[UserID], data: dict) -> web.Response: homeserver = data.get("homeserver", None) access_token = data.get("access_token", None) - new_client = MatrixClient(base_url=homeserver, token=access_token, loop=Client.loop, - client_session=Client.http_client) + new_client = MatrixClient(mxid="@not:a.mxid", base_url=homeserver, token=access_token, + loop=Client.loop, client_session=Client.http_client) try: mxid = await new_client.whoami() except MatrixInvalidToken: @@ -62,7 +62,7 @@ async def _create_client(user_id: Optional[UserID], data: dict) -> web.Response: return ErrUserExists elif mxid != user_id: return ErrMXIDMismatch - db_instance = DBClient(id=user_id, homeserver=homeserver, access_token=access_token, + db_instance = DBClient(id=mxid, homeserver=homeserver, access_token=access_token, enabled=data.get("enabled", True), next_batch=SyncToken(""), filter_id=FilterID(""), sync=data.get("sync", True), autojoin=data.get("autojoin", True), From 982623b677930708e38d17e676110fb52a661ecb Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Nov 2018 00:53:43 +0200 Subject: [PATCH 028/101] Fix patching instances --- maubot/instance.py | 2 ++ maubot/management/api/instance.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/maubot/instance.py b/maubot/instance.py index 6e3590f..11c53bd 100644 --- a/maubot/instance.py +++ b/maubot/instance.py @@ -170,6 +170,8 @@ class PluginInstance: self.plugin.on_external_config_update() async def update_primary_user(self, primary_user: UserID) -> bool: + if not primary_user or primary_user == self.primary_user: + return True client = Client.get(primary_user) if not client: return False diff --git a/maubot/management/api/instance.py b/maubot/management/api/instance.py index 2947247..2901b8c 100644 --- a/maubot/management/api/instance.py +++ b/maubot/management/api/instance.py @@ -65,7 +65,7 @@ async def _create_instance(instance_id: str, data: dict) -> web.Response: async def _update_instance(instance: PluginInstance, data: dict) -> web.Response: - if not await instance.update_primary_user(data.get("primary_user")): + if not await instance.update_primary_user(data.get("primary_user", None)): return ErrPrimaryUserNotFound instance.update_id(data.get("id", None)) instance.update_enabled(data.get("enabled", None)) From c70818fef2da408d743700c7f70e3bab1f5e555e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Nov 2018 01:21:22 +0200 Subject: [PATCH 029/101] Update plugin list and add link to API spec --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 97e4b33..78586d9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # maubot A plugin-based [Matrix](https://matrix.org) bot system written in Python. +Management API spec: [maubot.xyz/spec](https://maubot.xyz/spec) + ## Discussion Matrix room: [#maubot:maunium.net](https://matrix.to/#/#maubot:maunium.net) @@ -12,12 +14,12 @@ Matrix room: [#maubot:maunium.net](https://matrix.to/#/#maubot:maunium.net) * [media](https://github.com/maubot/media) - A bot that replies with the MXC URI of images you send it. * [dice](https://github.com/maubot/dice) - A combined dice rolling and calculator bot. * [karma](https://github.com/maubot/karma) - A user karma tracker bot. +* [echo](https://github.com/maubot/echo) - A bot that echoes pings and other stuff. ### Upcoming * rss - A bot that posts new RSS entries to rooms. * dictionary - A bot to get the dictionary definitions of words. * poll - A simple poll bot. -* echo - A very simple echo bot. * reminder - A bot to ping you about something after a certain amount of time. * github - A GitHub client and webhook receiver bot. * wolfram - A Wolfram Alpha bot From a584cba794b5a05d4e1423c5c679462f1816c8c5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Nov 2018 12:27:49 +0200 Subject: [PATCH 030/101] Return config with instances in management API --- maubot/instance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/maubot/instance.py b/maubot/instance.py index 11c53bd..06bc25c 100644 --- a/maubot/instance.py +++ b/maubot/instance.py @@ -65,6 +65,7 @@ class PluginInstance: "enabled": self.enabled, "started": self.started, "primary_user": self.primary_user, + "config": self.db_instance.config, } def load(self) -> None: From ec22e5eba7c57cb32ecbda6a6f2e3e8139ed2cf4 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Nov 2018 15:16:30 +0200 Subject: [PATCH 031/101] Add /auth/ping and prepare for frontend dev --- maubot/management/api/auth.py | 21 ++++++++++++++++-- maubot/management/api/middleware.py | 2 +- maubot/management/api/spec.yaml | 19 +++++++++++++++- maubot/management/frontend/package.json | 3 ++- maubot/management/frontend/public/favicon.ico | Bin 14846 -> 0 bytes maubot/management/frontend/public/index.html | 2 +- 6 files changed, 41 insertions(+), 6 deletions(-) delete mode 100644 maubot/management/frontend/public/favicon.ico diff --git a/maubot/management/api/auth.py b/maubot/management/api/auth.py index b813a7c..fe3fe40 100644 --- a/maubot/management/api/auth.py +++ b/maubot/management/api/auth.py @@ -22,7 +22,7 @@ from mautrix.types import UserID from mautrix.util.signed_token import sign_token, verify_token from .base import routes, get_config -from .responses import ErrBadAuth, ErrBodyNotJSON +from .responses import ErrBadAuth, ErrBodyNotJSON, ErrNoToken, ErrInvalidToken def is_valid_token(token: str) -> bool: @@ -38,7 +38,24 @@ def create_token(user: UserID) -> str: }) -@routes.post("/login") +@routes.post("/auth/ping") +async def ping(request: web.Request) -> web.Response: + token = request.headers.get("Authorization", "") + if not token or not token.startswith("Bearer "): + return ErrNoToken + + data = verify_token(get_config()["server.unshared_secret"], token[len("Bearer "):]) + if not data: + return ErrInvalidToken + user = data.get("user_id", None) + if not get_config().is_admin(user): + return ErrInvalidToken + return web.json_response({ + "username": user, + }) + + +@routes.post("/auth/login") async def login(request: web.Request) -> web.Response: try: data = await request.json() diff --git a/maubot/management/api/middleware.py b/maubot/management/api/middleware.py index fa5b93a..27185c0 100644 --- a/maubot/management/api/middleware.py +++ b/maubot/management/api/middleware.py @@ -24,7 +24,7 @@ Handler = Callable[[web.Request], Awaitable[web.Response]] @web.middleware async def auth(request: web.Request, handler: Handler) -> web.Response: - if request.path.endswith("/login"): + if "/auth/" in request.path: return await handler(request) token = request.headers.get("Authorization", "") if not token or not token.startswith("Bearer "): diff --git a/maubot/management/api/spec.yaml b/maubot/management/api/spec.yaml index e89f18b..75ec865 100644 --- a/maubot/management/api/spec.yaml +++ b/maubot/management/api/spec.yaml @@ -12,7 +12,7 @@ servers: - url: /_matrix/maubot/v1 paths: - /login: + /auth/login: post: operationId: login summary: Log in with the unshared secret or username+password @@ -45,6 +45,23 @@ paths: type: string 401: description: Invalid credentials + /auth/ping: + post: + operationId: ping + summary: Check if the given token is valid + tags: [Authentication] + responses: + 200: + description: Token is OK + content: + application/json: + schema: + type: object + properties: + username: + type: string + 401: + description: Token is not OK /plugins: get: diff --git a/maubot/management/frontend/package.json b/maubot/management/frontend/package.json index e02b7a0..c5cf653 100644 --- a/maubot/management/frontend/package.json +++ b/maubot/management/frontend/package.json @@ -21,5 +21,6 @@ "last 3 and_chr versions", "last 2 safari versions", "last 2 ios_saf versions" - ] + ], + "proxy": "http://localhost:29316" } diff --git a/maubot/management/frontend/public/favicon.ico b/maubot/management/frontend/public/favicon.ico deleted file mode 100644 index c74a967fe3a1a6d629541e1db50e062f9c7edbc3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14846 zcmeHN2Ut{B_W!9Uf?}l%7DUB{9RU>;X%0xh(Lq5<24TPi6dN$kP==25K4M1^EGSqI z1qB;+F&b;4l2{T;G}$D^B)i|Y$^YEBxwaz55m z*ck4KjS*fbjt#-u*iclZFGE$va+Ib;pdvFIRavXBKRX-;bHXW&!2X;F9N`ENnK$CJDMwRH8HIA3Jzk4*T&TnO6Lq+7@)$0hI)=-Q4QM)Z9JkNb;r6*Y+&Om? zcg`QfCls!pKaJDZ?%>q5PjLGBr)a!>7mYXW;S7bdHz~b&A7^jf$2l%EM*u zaI~K}-DaxwXxmxBxeJq0GjcQ5B(IL}63t0*n(Y}WQ$)*UvK3-?tNGc^zA`m@7MPDq zPn#4LwM-TszA93_e1iV_MR|6XZr&^3b8(&LF|=!MNs%h9Z!gpFL(B#b>e*4Z50w*) z^q|#6yK{$5I^9?~zF!X=HQJVSiPpW3AMp}{3UTv~>WduwpGI3CrTGEu)N8C$oNpn~SphV8j%C@(;1 zSs6-q?!t~ed$F@>KepE#N5#P!>^X82)fL-OTeTDQ2lwF6(R!5aYs8MqOW0Z0h`ls7 zR-L$j1IN$eV8dk`t-FPNCokYo<7L#JK7+b*S5SZX2F~o=h?9r+;nE?R^XhAG;Y2My zK7IgA2fjno>0`Kd?gTeyUA}f5mv7(4Cs!}v+VvaUoYZvpAwK@>OWb(yl$(=2d;S-E z{^C14x^opzzIuu0PafmZ*RSy8yC3oF`=9Xa)z5hS{a^6vhad3c>%Y;Q^h=wI@hW6jOSjF~uj`t;c|rV}QP9W!Rs$f2{RPO`OiahWsE%g4vZd!fhN zIj(bDTx>0^XX#puww&hT;yP#UToI87(=4rRt>2$wX>G$5HIcRTj1kjpB{N2I1Xm5& zW`=FQzHY8Wx8=n(6)q-y416UEW(=ApW=*%iq^p6cuf)&Q%v@weL>t@Dww5j~Gx~Sx z%;*w{y`G7{%GP$^w1GAQZA`j%)FQecqxb2o)kUwLp^1@kH|>sE9G&CW@2RELK})M6 zL5o47$AIWXbtJksBPFE0{l8%`CEu7!L`wRT!p;I0F=Z^Q63kBG-U&}$2>qzN-Rne!>~DN1vaP2u`}@l>`J8d zZA}>VrY%F+npN15wu<}+TIcd(ae(}Y>O2{03ZrPfi$!8e3bNP4BR3-%dFe?g%t^<_ z+*IV}<)EM-A9-unL$!WA64#a@bwfFFN_Hc+v=aGc`>?UJ92?7aW6Q3+Q0+Vb)vju6 z*;k8_{YSC0C zd-h@1-b!rSdl1_XHDLDTo44*H+-%v4gnA*2`11XuNV0 zXRqDn_V*XB-@&Cj&A9Z*=eT*P5x1{j!~N!Fur>43v#)90`~shS{s1>0K1K7xNBH#1 z7ifO|4L+y6{KH#U@Zj!kJpSSfJb6z0`Dah@;^j+zE&cX2x0XJqwe(xs+kgM^IbOZ| zJ6^q_HT9>T@zc*g+0a(U^jDG8_cw`vvY8L@BPKH4+I8=hDJt4{fvwZ4Gk&o$8tvfjHzU3sBb?{;7u#q z?_r?dxZ!=L@R(|CGi{oU&D5z=r&!xq+f1Hn-Cxi@R_G~Wm^*(_fHW{5Kq{36NCOtl z7l}n87op>F?rlqb= zN|-Uk)5t{ ztFzW4Dt9B41to}!uWH%96w{e!ez>37C-*^#!VlSzLCB0+iZwCeNKwj>7QGUAF|==q zU5pKJOHdl^kL^k+G9`DBC;k$tiLpqdP?(~iJ8d!Ud@cC=po~NG9NKcn#<4i>{E>^9_mBZz@Qnv?}_I{5m zCy(Gt<1ySgeHe|6XK?v)6Hb406DKd-!$~>=WoMl1T$Am8ZhdqKH*Va-^-r5|_wGGz z4|Id}KWy)F=kaqiKYD_@PhO(=8NdH|`uSbFc-)MKkH5gfC(rQZ^A~vb^eJA_9_aBm zuke)46P|znBVK>^4Y%+4iS|8z{qYC<{KMbh?RzxO+JA9c{9lZsq9RpcVGct!7Z>Cg zs#HXM!{(xb?BuwZ$jFF@2ybt0J@6v%ivbZLA|fMW<1%szm|Rizs(B(8XXjbYvk1;Y z=k^dX3elZ~LTAz31u;2sb9xvG+QPVPfy*-rPwipoQ8>?-ouK|Pxb<8aG)}N|*&oFy z({!7uy-i2Dxs7ylAK~UU!rgs@tGoMq?@@4fcXKDYtNSQdcd8xb>Nd(uu&mfN*lr>n zicFk1ew?MHrIm%1HEkEIr&%))!P3ggf&93>A84VBB~GDUn+>YA)I z47s^$)?}qpnwk=?pgKA6gak{(_6bUd&?p84Au2v4uRU^;qKVA?6;aU&sa0#h+Eqq9jDvVw$|RCuZd4cN=Z#!ladr4ui<5Bu)V!F zSP|?{yrM(D)gMTtp}`Izt@WIFZHjWSt&|m`ZHtTF_m|o_*xOT=qnN2}!1sy>!FEn^ zg}<#sRIWpDagbC(nk5oeOCp3QIUn;W)!C79@#vvJi&BVRCie4Fj{r5m1ZcRJgt{@g zlYMBE!goLqvkV9E2gSw74i5IgY#=x;V&X(7R+|$nCgI>Qri0B?$x6DVlc$J7xY6XS zkn>#ayAl;T$<%zfnTmUE7WvsrCDHNmYIR&g6t#uiw)Y4(HGhscRXW8!B{xZ-;94Ou zYDjH6FC=&9Eil*cE5%Vk6K163CMqP3T(#OF8Y|wYs8HKJ-Fmk0XE`{`G?^Hm8y{-R z>z7Grto@_p_Ecmx22bj(r^on*^Y$0yrwtkylAv(#<v_lm(Hj0ogVm3sfPGh?3 z>FKoUr#{CGGPMbD)TXPzVBa}JK3Q8Hsy$V%kV_`@=|&q%;%^zB;^MSHgZg&rW72n; zAG64&OQ>vemr#YawnVJmmy|PsHu{THX{JL=P1$>uUSp;?`c2mUFhaYtMAAh|OJ7Gv zy=x_YGxhiv7pD&zX+D?<=nT^7rQdh-z%Bzj(!n| z9z1{h{-zBwH|Go@7FMs}W$pc@e&bPYBh(eYo#?b)UmDMJ;vfFojB2WGkCyAJ=GW+F z_P-6?-_`%y{CZ5k`hMW;c~d_pr>l|v|Mhcbwer7Zzox$$7D{#LVlXk5d&~Y&f*u+O z^ytS$Jp_7c;-ELj$8A)5xVm!Xks7L-=K12r(>?4>xV}MJx^zU#{Z?Q$c;qk+BPpf7 z8U4*zrUfI04KrR!s(GX@rxl3>4$CRKTwS6>on0PYpkeK@V#PB}COzvT+#bvCV{1H}9Y{rVfO7cZ zq^}M|hGHp_qa%r&q}XQK$^Vrf4a)MoF+eE*PnCb;$Gh67lg$WXDG!D=CJ1 zC!LohM_GI@b|i&jSJDUEd+F_2G1!;2lKbX|`L;(??A>%M;uGSLuqFeUsfmcsU5E7C z0wfo0MjrXG2{~kM@(Co-Iep6da-?tE!TGYh4_n3iue*8wbyHOxwj4N$^87^XqI3GH z4Vm1xLCj~ZFUi2Rs>3Ly^Y{9y?WnFkgxcC8XlS5v@n2BB{Uh>EPhv;yDO4PzZ+FNy zJyg}q`KML%uDXVu!=Gv5&fwYkJLik;%*WZ?g*dyfgnZHsXsRy3xoYxDtGD9vk=60i_5qQ8^XSAx=uF=pIJ?Xi6MTGpeSQ7>=r8f39K(EHBKnAD&vuzT+u6lg z=;&xSeTwe9Sk*iinUcPBW1vK?Ow7p1%gf<@6y?$J4vW}XKu&6ck{Facb7UmFS(@#b zneN!~eH>q*OvvKSgt#cLHJ=)%mgL!}EqSC^n6Hxeah1xrDouSG4Xk|*$#F87R3pi! zQXj`25|wJ1Ru?;GAL^~IUkigWE+s2VZ9a)J!wDMwEHCvDIyn0HI?7dE9XfUzKFiU; z$;r`P7{KZ{DB}{784?pTJ=WBa<&k0sM=|lr#X?ouh>o4#m(yrSWm0FS786>z-^wZd z9Uc866$+}5+No68GZ)JIog5vVXdwTHxYX6bHBu=H@Ci^RCz5=Ti@(&tRT|)G|HhSg zt6`2Hb#e-jDW$W!&2W_FB$K>qp0Cu0s+7Lozb(;@>T6GzOBN}dY!ntD%v$Y8^1iMC zV*j>-+fJZeL8>-VsBJ6=%Ww=3N=bh8Ebm#f{z%VdGFPVnIslWq>KI!Hma10H3S`r2 zrlXIutMBg{puWAy5UIbE-Q@n=23uN_yn0fO@8v}AN!!`^I|cq$kWL?HA+k0mc^i{21U-&HneOjFY!rL`u4I{ z(tS=n^p^f3B~qo-S=&g!e}_vkIl$w`nY#ZIj)QSMD$N~x2Mkw9R@+Sl}d zk;5#o(%G%U;%{d{r?$qH)@|A^l&{VR)rWU-qAb#B zwx;<=dm&w@9?mkkj~=@_3GztSo&)GiSD&-LrF}MxVFm-8WD42r*5}W^hdtdV-R)%x zaaTu9TIOf3-B(~B5EvM9d z+;051_KhbDuyN4sZ_?9=W+8S+E2LKCE+nc5(9;Wu^mWoTG14c~1@v{5!Q1izD>I}1 zq~2(N?sO*~8at=1Lb=?zd+z{+w0Bp>={f_9m@tDGFW;{JO9j?e+TGdNp{I^Xe?zWqPTs)4?Ct&=TZ=>%tOdqrXxO)ZKY9nPqob{DV$#1a zeW1tpoTVE3-)x`BtH+1w(5FB9qJcWhwW^6&9e4k<+<*M|tJ^0r#?T5*7tvS;Sj6GC z?VrSnax7{-wLK)m^tM>{_WcuTY;7$fc{P|g1wNrxc}dAue4z5)ev4)xX43w%zk7Hr>o3)cS{tY8}ktOYZIS?z?FkfmH8?wj8jLo0Mn)#pW zKf8X`mXjt;vY2FH!BQ5eWMRo?S}2T4-EXsgFnQKKX=p`DVUB1H{h2^(CCjzW-@JTq zD8DnN-7dHBWf5V?T#ME%Gf(2. - + From 2736a1f47fb3a0656b38e6f967b9354de9050417 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 2 Nov 2018 18:45:07 +0200 Subject: [PATCH 032/101] Fix reusing management API responses and some other things --- maubot/client.py | 1 + maubot/instance.py | 12 +- maubot/loader/zip.py | 6 +- maubot/management/api/auth.py | 27 +-- maubot/management/api/client.py | 38 ++-- maubot/management/api/instance.py | 29 ++- maubot/management/api/middleware.py | 19 +- maubot/management/api/plugin.py | 33 ++-- maubot/management/api/responses.py | 293 ++++++++++++++++++---------- 9 files changed, 272 insertions(+), 186 deletions(-) diff --git a/maubot/client.py b/maubot/client.py index 91fcb0c..ea7db0d 100644 --- a/maubot/client.py +++ b/maubot/client.py @@ -134,6 +134,7 @@ class Client: except KeyError: pass self.db.delete(self.db_instance) + self.db.commit() def to_dict(self) -> dict: return { diff --git a/maubot/instance.py b/maubot/instance.py index 06bc25c..00f8be7 100644 --- a/maubot/instance.py +++ b/maubot/instance.py @@ -14,13 +14,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from typing import Dict, List, Optional -from sqlalchemy.orm import Session -from ruamel.yaml.comments import CommentedMap -from ruamel.yaml import YAML from asyncio import AbstractEventLoop import logging import io +from sqlalchemy.orm import Session +from ruamel.yaml.comments import CommentedMap +from ruamel.yaml import YAML + from mautrix.util.config import BaseProxyConfig, RecursiveDict from mautrix.types import UserID @@ -56,6 +57,10 @@ class PluginInstance: self.log = logging.getLogger(f"maubot.plugin.{self.id}") self.config = None self.started = False + self.loader = None + self.client = None + self.plugin = None + self.base_cfg = None self.cache[self.id] = self def to_dict(self) -> dict: @@ -94,6 +99,7 @@ class PluginInstance: except KeyError: pass self.db.delete(self.db_instance) + self.db.commit() # TODO delete plugin db def load_config(self) -> CommentedMap: diff --git a/maubot/loader/zip.py b/maubot/loader/zip.py index b8c54ea..d014aeb 100644 --- a/maubot/loader/zip.py +++ b/maubot/loader/zip.py @@ -192,8 +192,10 @@ class ZippedPluginLoader(PluginLoader): for module in self.modules: try: importer.load_module(module) - except ZipImportError as e: + except ZipImportError: raise MaubotZipLoadError(f"Module {module} not found in file") + except Exception: + raise MaubotZipLoadError(f"Failed to load module {module}") try: main_mod = sys.modules[self.main_module] except KeyError as e: @@ -235,7 +237,7 @@ class ZippedPluginLoader(PluginLoader): self._importer.remove_cache() self._importer = None self._loaded = None - os.remove(self.path) + self.trash(self.path, reason="delete") self.id = None self.path = None self.version = None diff --git a/maubot/management/api/auth.py b/maubot/management/api/auth.py index fe3fe40..34303d1 100644 --- a/maubot/management/api/auth.py +++ b/maubot/management/api/auth.py @@ -22,7 +22,7 @@ from mautrix.types import UserID from mautrix.util.signed_token import sign_token, verify_token from .base import routes, get_config -from .responses import ErrBadAuth, ErrBodyNotJSON, ErrNoToken, ErrInvalidToken +from .responses import resp def is_valid_token(token: str) -> bool: @@ -35,6 +35,7 @@ def is_valid_token(token: str) -> bool: def create_token(user: UserID) -> str: return sign_token(get_config()["server.unshared_secret"], { "user_id": user, + "created_at": int(time()), }) @@ -42,17 +43,15 @@ def create_token(user: UserID) -> str: async def ping(request: web.Request) -> web.Response: token = request.headers.get("Authorization", "") if not token or not token.startswith("Bearer "): - return ErrNoToken + return resp.no_token data = verify_token(get_config()["server.unshared_secret"], token[len("Bearer "):]) if not data: - return ErrInvalidToken + return resp.invalid_token user = data.get("user_id", None) if not get_config().is_admin(user): - return ErrInvalidToken - return web.json_response({ - "username": user, - }) + return resp.invalid_token + return resp.pong(user) @routes.post("/auth/login") @@ -60,21 +59,15 @@ async def login(request: web.Request) -> web.Response: try: data = await request.json() except json.JSONDecodeError: - return ErrBodyNotJSON + return resp.body_not_json secret = data.get("secret") if secret and get_config()["server.unshared_secret"] == secret: user = data.get("user") or "root" - return web.json_response({ - "token": create_token(user), - "created_at": int(time()), - }) + return resp.logged_in(create_token(user)) username = data.get("username") password = data.get("password") if get_config().check_password(username, password): - return web.json_response({ - "token": create_token(username), - "created_at": int(time()), - }) + return resp.logged_in(create_token(username)) - return ErrBadAuth + return resp.bad_auth diff --git a/maubot/management/api/client.py b/maubot/management/api/client.py index 421ef51..872c965 100644 --- a/maubot/management/api/client.py +++ b/maubot/management/api/client.py @@ -26,14 +26,12 @@ from mautrix.client import Client as MatrixClient from ...db import DBClient from ...client import Client from .base import routes -from .responses import (RespDeleted, ErrClientNotFound, ErrBodyNotJSON, ErrClientInUse, - ErrBadClientAccessToken, ErrBadClientAccessDetails, ErrMXIDMismatch, - ErrUserExists) +from .responses import resp @routes.get("/clients") async def get_clients(_: web.Request) -> web.Response: - return web.json_response([client.to_dict() for client in Client.cache.values()]) + return resp.found([client.to_dict() for client in Client.cache.values()]) @routes.get("/client/{id}") @@ -41,8 +39,8 @@ async def get_client(request: web.Request) -> web.Response: user_id = request.match_info.get("id", None) client = Client.get(user_id, None) if not client: - return ErrClientNotFound - return web.json_response(client.to_dict()) + return resp.client_not_found + return resp.found(client.to_dict()) async def _create_client(user_id: Optional[UserID], data: dict) -> web.Response: @@ -53,15 +51,15 @@ async def _create_client(user_id: Optional[UserID], data: dict) -> web.Response: try: mxid = await new_client.whoami() except MatrixInvalidToken: - return ErrBadClientAccessToken + return resp.bad_client_access_token except MatrixRequestError: - return ErrBadClientAccessDetails + return resp.bad_client_access_details if user_id is None: existing_client = Client.get(mxid, None) if existing_client is not None: - return ErrUserExists + return resp.user_exists elif mxid != user_id: - return ErrMXIDMismatch + return resp.mxid_mismatch db_instance = DBClient(id=mxid, homeserver=homeserver, access_token=access_token, enabled=data.get("enabled", True), next_batch=SyncToken(""), filter_id=FilterID(""), sync=data.get("sync", True), @@ -72,7 +70,7 @@ async def _create_client(user_id: Optional[UserID], data: dict) -> web.Response: Client.db.add(db_instance) Client.db.commit() await client.start() - return web.json_response(client.to_dict()) + return resp.created(client.to_dict()) async def _update_client(client: Client, data: dict) -> web.Response: @@ -80,18 +78,18 @@ async def _update_client(client: Client, data: dict) -> web.Response: await client.update_access_details(data.get("access_token", None), data.get("homeserver", None)) except MatrixInvalidToken: - return ErrBadClientAccessToken + return resp.bad_client_access_token except MatrixRequestError: - return ErrBadClientAccessDetails + return resp.bad_client_access_details except ValueError: - return ErrMXIDMismatch + return resp.mxid_mismatch await client.update_avatar_url(data.get("avatar_url", None)) await client.update_displayname(data.get("displayname", None)) await client.update_started(data.get("started", None)) client.enabled = data.get("enabled", client.enabled) client.autojoin = data.get("autojoin", client.autojoin) client.sync = data.get("sync", client.sync) - return web.json_response(client.to_dict(), status=HTTPStatus.CREATED) + return resp.updated(client.to_dict()) @routes.post("/client/new") @@ -99,7 +97,7 @@ async def create_client(request: web.Request) -> web.Response: try: data = await request.json() except JSONDecodeError: - return ErrBodyNotJSON + return resp.body_not_json return await _create_client(None, data) @@ -110,7 +108,7 @@ async def update_client(request: web.Request) -> web.Response: try: data = await request.json() except JSONDecodeError: - return ErrBodyNotJSON + return resp.body_not_json if not client: return await _create_client(user_id, data) else: @@ -122,10 +120,10 @@ async def delete_client(request: web.Request) -> web.Response: user_id = request.match_info.get("id", None) client = Client.get(user_id, None) if not client: - return ErrClientNotFound + return resp.client_not_found if len(client.references) > 0: - return ErrClientInUse + return resp.client_in_use if client.started: await client.stop() client.delete() - return RespDeleted + return resp.deleted diff --git a/maubot/management/api/instance.py b/maubot/management/api/instance.py index 2901b8c..57cf2f3 100644 --- a/maubot/management/api/instance.py +++ b/maubot/management/api/instance.py @@ -23,13 +23,12 @@ from ...instance import PluginInstance from ...loader import PluginLoader from ...client import Client from .base import routes -from .responses import (ErrInstanceNotFound, ErrBodyNotJSON, RespDeleted, ErrPrimaryUserNotFound, - ErrPluginTypeRequired, ErrPrimaryUserRequired, ErrPluginTypeNotFound) +from .responses import resp @routes.get("/instances") async def get_instances(_: web.Request) -> web.Response: - return web.json_response([instance.to_dict() for instance in PluginInstance.cache.values()]) + return resp.found([instance.to_dict() for instance in PluginInstance.cache.values()]) @routes.get("/instance/{id}") @@ -37,23 +36,23 @@ async def get_instance(request: web.Request) -> web.Response: instance_id = request.match_info.get("id", "").lower() instance = PluginInstance.get(instance_id, None) if not instance: - return ErrInstanceNotFound - return web.json_response(instance.to_dict()) + return resp.instance_not_found + return resp.found(instance.to_dict()) async def _create_instance(instance_id: str, data: dict) -> web.Response: plugin_type = data.get("type", None) primary_user = data.get("primary_user", None) if not plugin_type: - return ErrPluginTypeRequired + return resp.plugin_type_required elif not primary_user: - return ErrPrimaryUserRequired + return resp.primary_user_required elif not Client.get(primary_user): - return ErrPrimaryUserNotFound + return resp.primary_user_not_found try: PluginLoader.find(plugin_type) except KeyError: - return ErrPluginTypeNotFound + return resp.plugin_type_not_found db_instance = DBPlugin(id=instance_id, type=plugin_type, enabled=data.get("enabled", True), primary_user=primary_user, config=data.get("config", "")) instance = PluginInstance(db_instance) @@ -61,18 +60,18 @@ async def _create_instance(instance_id: str, data: dict) -> web.Response: PluginInstance.db.add(db_instance) PluginInstance.db.commit() await instance.start() - return web.json_response(instance.to_dict(), status=HTTPStatus.CREATED) + return resp.created(instance.to_dict()) async def _update_instance(instance: PluginInstance, data: dict) -> web.Response: if not await instance.update_primary_user(data.get("primary_user", None)): - return ErrPrimaryUserNotFound + return resp.primary_user_not_found instance.update_id(data.get("id", None)) instance.update_enabled(data.get("enabled", None)) instance.update_config(data.get("config", None)) await instance.update_started(data.get("started", None)) instance.db.commit() - return web.json_response(instance.to_dict()) + return resp.updated(instance.to_dict()) @routes.put("/instance/{id}") @@ -82,7 +81,7 @@ async def update_instance(request: web.Request) -> web.Response: try: data = await request.json() except JSONDecodeError: - return ErrBodyNotJSON + return resp.body_not_json if not instance: return await _create_instance(instance_id, data) else: @@ -94,8 +93,8 @@ async def delete_instance(request: web.Request) -> web.Response: instance_id = request.match_info.get("id", "").lower() instance = PluginInstance.get(instance_id, None) if not instance: - return ErrInstanceNotFound + return resp.instance_not_found if instance.started: await instance.stop() instance.delete() - return RespDeleted + return resp.deleted diff --git a/maubot/management/api/middleware.py b/maubot/management/api/middleware.py index 27185c0..2fefbe8 100644 --- a/maubot/management/api/middleware.py +++ b/maubot/management/api/middleware.py @@ -14,9 +14,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from typing import Callable, Awaitable +import logging + from aiohttp import web -from .responses import ErrNoToken, ErrInvalidToken, ErrPathNotFound, ErrMethodNotAllowed +from .responses import resp from .auth import is_valid_token Handler = Callable[[web.Request], Awaitable[web.Response]] @@ -28,25 +30,32 @@ async def auth(request: web.Request, handler: Handler) -> web.Response: return await handler(request) token = request.headers.get("Authorization", "") if not token or not token.startswith("Bearer "): - return ErrNoToken + return resp.no_token if not is_valid_token(token[len("Bearer "):]): - return ErrInvalidToken + return resp.invalid_token return await handler(request) +log = logging.getLogger("maubot.server") + + @web.middleware async def error(request: web.Request, handler: Handler) -> web.Response: try: return await handler(request) except web.HTTPException as ex: + print(ex) if ex.status_code == 404: - return ErrPathNotFound + return resp.path_not_found elif ex.status_code == 405: - return ErrMethodNotAllowed + return resp.method_not_allowed return web.json_response({ "error": f"Unhandled HTTP {ex.status}", "errcode": f"unhandled_http_{ex.status}", }, status=ex.status) + except Exception: + log.exception("Error in handler") + return resp.internal_server_error req_no = 0 diff --git a/maubot/management/api/plugin.py b/maubot/management/api/plugin.py index 87174ab..4fbb209 100644 --- a/maubot/management/api/plugin.py +++ b/maubot/management/api/plugin.py @@ -23,14 +23,13 @@ import re from aiohttp import web from ...loader import PluginLoader, ZippedPluginLoader, MaubotZipImportError -from .responses import (ErrPluginNotFound, ErrPluginInUse, plugin_import_error, - plugin_reload_error, RespDeleted, RespOK, ErrUnsupportedPluginLoader) +from .responses import resp from .base import routes, get_config @routes.get("/plugins") async def get_plugins(_) -> web.Response: - return web.json_response([plugin.to_dict() for plugin in PluginLoader.id_cache.values()]) + return resp.found([plugin.to_dict() for plugin in PluginLoader.id_cache.values()]) @routes.get("/plugin/{id}") @@ -38,8 +37,8 @@ async def get_plugin(request: web.Request) -> web.Response: plugin_id = request.match_info.get("id", None) plugin = PluginLoader.id_cache.get(plugin_id, None) if not plugin: - return ErrPluginNotFound - return web.json_response(plugin.to_dict()) + return resp.plugin_not_found + return resp.found(plugin.to_dict()) @routes.delete("/plugin/{id}") @@ -47,11 +46,11 @@ async def delete_plugin(request: web.Request) -> web.Response: plugin_id = request.match_info.get("id", None) plugin = PluginLoader.id_cache.get(plugin_id, None) if not plugin: - return ErrPluginNotFound + return resp.plugin_not_found elif len(plugin.references) > 0: - return ErrPluginInUse + return resp.plugin_in_use await plugin.delete() - return RespDeleted + return resp.deleted @routes.post("/plugin/{id}/reload") @@ -59,15 +58,15 @@ async def reload_plugin(request: web.Request) -> web.Response: plugin_id = request.match_info.get("id", None) plugin = PluginLoader.id_cache.get(plugin_id, None) if not plugin: - return ErrPluginNotFound + return resp.plugin_not_found await plugin.stop_instances() try: await plugin.reload() except MaubotZipImportError as e: - return plugin_reload_error(str(e), traceback.format_exc()) + return resp.plugin_reload_error(str(e), traceback.format_exc()) await plugin.start_instances() - return RespOK + return resp.ok async def upload_new_plugin(content: bytes, pid: str, version: str) -> web.Response: @@ -78,8 +77,8 @@ async def upload_new_plugin(content: bytes, pid: str, version: str) -> web.Respo plugin = ZippedPluginLoader.get(path) except MaubotZipImportError as e: ZippedPluginLoader.trash(path) - return plugin_import_error(str(e), traceback.format_exc()) - return web.json_response(plugin.to_dict(), status=HTTPStatus.CREATED) + return resp.plugin_import_error(str(e), traceback.format_exc()) + return resp.created(plugin.to_dict()) async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, new_version: str @@ -107,10 +106,10 @@ async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, await plugin.start_instances() except MaubotZipImportError: pass - return plugin_import_error(str(e), traceback.format_exc()) + return resp.plugin_import_error(str(e), traceback.format_exc()) await plugin.start_instances() ZippedPluginLoader.trash(old_path, reason="update") - return web.json_response(plugin.to_dict()) + return resp.updated(plugin.to_dict()) @routes.post("/plugins/upload") @@ -120,11 +119,11 @@ async def upload_plugin(request: web.Request) -> web.Response: try: pid, version = ZippedPluginLoader.verify_meta(file) except MaubotZipImportError as e: - return plugin_import_error(str(e), traceback.format_exc()) + return resp.plugin_import_error(str(e), traceback.format_exc()) plugin = PluginLoader.id_cache.get(pid, None) if not plugin: return await upload_new_plugin(content, pid, version) elif isinstance(plugin, ZippedPluginLoader): return await upload_replacement_plugin(plugin, content, version) else: - return ErrUnsupportedPluginLoader + return resp.unsupported_plugin_loader diff --git a/maubot/management/api/responses.py b/maubot/management/api/responses.py index 9fd4c40..9c815c2 100644 --- a/maubot/management/api/responses.py +++ b/maubot/management/api/responses.py @@ -14,132 +14,211 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from http import HTTPStatus + from aiohttp import web -ErrBodyNotJSON = web.json_response({ - "error": "Request body is not JSON", - "errcode": "body_not_json", -}, status=HTTPStatus.BAD_REQUEST) -ErrPluginTypeRequired = web.json_response({ - "error": "Plugin type is required when creating plugin instances", - "errcode": "plugin_type_required", -}, status=HTTPStatus.BAD_REQUEST) +class _Response: + @property + def body_not_json(self) -> web.Response: + return web.json_response({ + "error": "Request body is not JSON", + "errcode": "body_not_json", + }, status=HTTPStatus.BAD_REQUEST) -ErrPrimaryUserRequired = web.json_response({ - "error": "Primary user is required when creating plugin instances", - "errcode": "primary_user_required", -}, status=HTTPStatus.BAD_REQUEST) + @property + def plugin_type_required(self) -> web.Response: + return web.json_response({ + "error": "Plugin type is required when creating plugin instances", + "errcode": "plugin_type_required", + }, status=HTTPStatus.BAD_REQUEST) -ErrBadClientAccessToken = web.json_response({ - "error": "Invalid access token", - "errcode": "bad_client_access_token", -}, status=HTTPStatus.BAD_REQUEST) + @property + def primary_user_required(self) -> web.Response: + return web.json_response({ + "error": "Primary user is required when creating plugin instances", + "errcode": "primary_user_required", + }, status=HTTPStatus.BAD_REQUEST) -ErrBadClientAccessDetails = web.json_response({ - "error": "Invalid homeserver or access token", - "errcode": "bad_client_access_details" -}, status=HTTPStatus.BAD_REQUEST) + @property + def bad_client_access_token(self) -> web.Response: + return web.json_response({ + "error": "Invalid access token", + "errcode": "bad_client_access_token", + }, status=HTTPStatus.BAD_REQUEST) -ErrMXIDMismatch = web.json_response({ - "error": "The Matrix user ID of the client and the user ID of the access token don't match", - "errcode": "mxid_mismatch", -}, status=HTTPStatus.BAD_REQUEST) + @property + def bad_client_access_details(self) -> web.Response: + return web.json_response({ + "error": "Invalid homeserver or access token", + "errcode": "bad_client_access_details" + }, status=HTTPStatus.BAD_REQUEST) -ErrBadAuth = web.json_response({ - "error": "Invalid username or password", - "errcode": "invalid_auth", -}, status=HTTPStatus.UNAUTHORIZED) + @property + def mxid_mismatch(self) -> web.Response: + return web.json_response({ + "error": "The Matrix user ID of the client and the user ID of the access token don't match", + "errcode": "mxid_mismatch", + }, status=HTTPStatus.BAD_REQUEST) -ErrNoToken = web.json_response({ - "error": "Authorization token missing", - "errcode": "auth_token_missing", -}, status=HTTPStatus.UNAUTHORIZED) + @property + def bad_auth(self) -> web.Response: + return web.json_response({ + "error": "Invalid username or password", + "errcode": "invalid_auth", + }, status=HTTPStatus.UNAUTHORIZED) -ErrInvalidToken = web.json_response({ - "error": "Invalid authorization token", - "errcode": "auth_token_invalid", -}, status=HTTPStatus.UNAUTHORIZED) + @property + def no_token(self) -> web.Response: + return web.json_response({ + "error": "Authorization token missing", + "errcode": "auth_token_missing", + }, status=HTTPStatus.UNAUTHORIZED) -ErrPluginNotFound = web.json_response({ - "error": "Plugin not found", - "errcode": "plugin_not_found", -}, status=HTTPStatus.NOT_FOUND) + @property + def invalid_token(self) -> web.Response: + return web.json_response({ + "error": "Invalid authorization token", + "errcode": "auth_token_invalid", + }, status=HTTPStatus.UNAUTHORIZED) -ErrClientNotFound = web.json_response({ - "error": "Client not found", - "errcode": "client_not_found", -}, status=HTTPStatus.NOT_FOUND) + @property + def plugin_not_found(self) -> web.Response: + return web.json_response({ + "error": "Plugin not found", + "errcode": "plugin_not_found", + }, status=HTTPStatus.NOT_FOUND) -ErrPrimaryUserNotFound = web.json_response({ - "error": "Client for given primary user not found", - "errcode": "primary_user_not_found", -}, status=HTTPStatus.NOT_FOUND) + @property + def client_not_found(self) -> web.Response: + return web.json_response({ + "error": "Client not found", + "errcode": "client_not_found", + }, status=HTTPStatus.NOT_FOUND) -ErrInstanceNotFound = web.json_response({ - "error": "Plugin instance not found", - "errcode": "instance_not_found", -}, status=HTTPStatus.NOT_FOUND) + @property + def primary_user_not_found(self) -> web.Response: + return web.json_response({ + "error": "Client for given primary user not found", + "errcode": "primary_user_not_found", + }, status=HTTPStatus.NOT_FOUND) -ErrPluginTypeNotFound = web.json_response({ - "error": "Given plugin type not found", - "errcode": "plugin_type_not_found", -}, status=HTTPStatus.NOT_FOUND) + @property + def instance_not_found(self) -> web.Response: + return web.json_response({ + "error": "Plugin instance not found", + "errcode": "instance_not_found", + }, status=HTTPStatus.NOT_FOUND) -ErrPathNotFound = web.json_response({ - "error": "Resource not found", - "errcode": "resource_not_found", -}, status=HTTPStatus.NOT_FOUND) + @property + def plugin_type_not_found(self) -> web.Response: + return web.json_response({ + "error": "Given plugin type not found", + "errcode": "plugin_type_not_found", + }, status=HTTPStatus.NOT_FOUND) -ErrMethodNotAllowed = web.json_response({ - "error": "Method not allowed", - "errcode": "method_not_allowed", -}, status=HTTPStatus.METHOD_NOT_ALLOWED) + @property + def path_not_found(self) -> web.Response: + return web.json_response({ + "error": "Resource not found", + "errcode": "resource_not_found", + }, status=HTTPStatus.NOT_FOUND) -ErrUserExists = web.json_response({ - "error": "There is already a client with the user ID of that token", - "errcode": "user_exists", -}, status=HTTPStatus.CONFLICT) + @property + def method_not_allowed(self) -> web.Response: + return web.json_response({ + "error": "Method not allowed", + "errcode": "method_not_allowed", + }, status=HTTPStatus.METHOD_NOT_ALLOWED) -ErrPluginInUse = web.json_response({ - "error": "Plugin instances of this type still exist", - "errcode": "plugin_in_use", -}, status=HTTPStatus.PRECONDITION_FAILED) + @property + def user_exists(self) -> web.Response: + return web.json_response({ + "error": "There is already a client with the user ID of that token", + "errcode": "user_exists", + }, status=HTTPStatus.CONFLICT) -ErrClientInUse = web.json_response({ - "error": "Plugin instances with this client as their primary user still exist", - "errcode": "client_in_use", -}, status=HTTPStatus.PRECONDITION_FAILED) + @property + def plugin_in_use(self) -> web.Response: + return web.json_response({ + "error": "Plugin instances of this type still exist", + "errcode": "plugin_in_use", + }, status=HTTPStatus.PRECONDITION_FAILED) + + @property + def client_in_use(self) -> web.Response: + return web.json_response({ + "error": "Plugin instances with this client as their primary user still exist", + "errcode": "client_in_use", + }, status=HTTPStatus.PRECONDITION_FAILED) + + @staticmethod + def plugin_import_error(error: str, stacktrace: str) -> web.Response: + return web.json_response({ + "error": error, + "stacktrace": stacktrace, + "errcode": "plugin_invalid", + }, status=HTTPStatus.BAD_REQUEST) + + @staticmethod + def plugin_reload_error(error: str, stacktrace: str) -> web.Response: + return web.json_response({ + "error": error, + "stacktrace": stacktrace, + "errcode": "plugin_reload_fail", + }, status=HTTPStatus.INTERNAL_SERVER_ERROR) + + @property + def internal_server_error(self) -> web.Response: + return web.json_response({ + "error": "Internal server error", + "errcode": "internal_server_error", + }, status=HTTPStatus.INTERNAL_SERVER_ERROR) + + @property + def unsupported_plugin_loader(self) -> web.Response: + return web.json_response({ + "error": "Existing plugin with same ID uses unsupported plugin loader", + "errcode": "unsupported_plugin_loader", + }, status=HTTPStatus.BAD_REQUEST) + + @property + def not_implemented(self) -> web.Response: + return web.json_response({ + "error": "Not implemented", + "errcode": "not_implemented", + }, status=HTTPStatus.NOT_IMPLEMENTED) + + @property + def ok(self) -> web.Response: + return web.json_response({ + "success": True, + }, status=HTTPStatus.OK) + + @property + def deleted(self) -> web.Response: + return web.Response(status=HTTPStatus.NO_CONTENT) + + @staticmethod + def found(data: dict) -> web.Response: + return web.json_response(data, status=HTTPStatus.OK) + + def updated(self, data: dict) -> web.Response: + return self.found(data) + + def logged_in(self, token: str) -> web.Response: + return self.found({ + "token": token, + }) + + def pong(self, user: str) -> web.Response: + return self.found({ + "username": user, + }) + + @staticmethod + def created(data: dict) -> web.Response: + return web.json_response(data, status=HTTPStatus.CREATED) -def plugin_import_error(error: str, stacktrace: str) -> web.Response: - return web.json_response({ - "error": error, - "stacktrace": stacktrace, - "errcode": "plugin_invalid", - }, status=HTTPStatus.BAD_REQUEST) - - -def plugin_reload_error(error: str, stacktrace: str) -> web.Response: - return web.json_response({ - "error": error, - "stacktrace": stacktrace, - "errcode": "plugin_reload_fail", - }, status=HTTPStatus.INTERNAL_SERVER_ERROR) - - -ErrUnsupportedPluginLoader = web.json_response({ - "error": "Existing plugin with same ID uses unsupported plugin loader", - "errcode": "unsupported_plugin_loader", -}, status=HTTPStatus.BAD_REQUEST) - -ErrNotImplemented = web.json_response({ - "error": "Not implemented", - "errcode": "not_implemented", -}, status=HTTPStatus.NOT_IMPLEMENTED) - -RespOK = web.json_response({ - "success": True, -}, status=HTTPStatus.OK) - -RespDeleted = web.Response(status=HTTPStatus.NO_CONTENT) +resp = _Response() From 8cd8f525666d0ef43fee35a6a51431520d35dd72 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 4 Nov 2018 22:55:58 +0200 Subject: [PATCH 033/101] Base frontend stuff and login view --- maubot/management/frontend/package.json | 1 + .../src/{MaubotManager.js => Home.js} | 15 +-- maubot/management/frontend/src/Login.js | 47 +++++++++ .../management/frontend/src/PrivateRoute.js | 16 ++++ maubot/management/frontend/src/Router.js | 41 ++++++++ maubot/management/frontend/src/Spinner.js | 9 ++ maubot/management/frontend/src/api.js | 86 +++++++++++++++++ maubot/management/frontend/src/index.js | 4 +- .../frontend/src/style/base/body.sass | 7 ++ .../frontend/src/style/base/elements.sass | 95 +++++++++++++++++++ .../management/frontend/src/style/index.sass | 5 + .../frontend/src/style/lib/spinner.sass | 60 ++++++++++++ .../frontend/src/style/pages/login.sass | 44 +++++++++ maubot/management/frontend/yarn.lock | 78 ++++++++++++++- 14 files changed, 492 insertions(+), 16 deletions(-) rename maubot/management/frontend/src/{MaubotManager.js => Home.js} (75%) create mode 100644 maubot/management/frontend/src/Login.js create mode 100644 maubot/management/frontend/src/PrivateRoute.js create mode 100644 maubot/management/frontend/src/Router.js create mode 100644 maubot/management/frontend/src/Spinner.js create mode 100644 maubot/management/frontend/src/api.js create mode 100644 maubot/management/frontend/src/style/base/elements.sass create mode 100644 maubot/management/frontend/src/style/lib/spinner.sass create mode 100644 maubot/management/frontend/src/style/pages/login.sass diff --git a/maubot/management/frontend/package.json b/maubot/management/frontend/package.json index c5cf653..9ecf46a 100644 --- a/maubot/management/frontend/package.json +++ b/maubot/management/frontend/package.json @@ -6,6 +6,7 @@ "node-sass": "^4.9.4", "react": "^16.6.0", "react-dom": "^16.6.0", + "react-router-dom": "^4.3.1", "react-scripts": "2.0.5" }, "scripts": { diff --git a/maubot/management/frontend/src/MaubotManager.js b/maubot/management/frontend/src/Home.js similarity index 75% rename from maubot/management/frontend/src/MaubotManager.js rename to maubot/management/frontend/src/Home.js index 314f47a..53a8466 100644 --- a/maubot/management/frontend/src/MaubotManager.js +++ b/maubot/management/frontend/src/Home.js @@ -15,19 +15,12 @@ // along with this program. If not, see . import React, { Component } from "react" -class MaubotManager extends Component { +class Home extends Component { render() { - return ( -
-
+ return
-
-
- -
-
- ) + } } -export default MaubotManager +export default Home diff --git a/maubot/management/frontend/src/Login.js b/maubot/management/frontend/src/Login.js new file mode 100644 index 0000000..6788168 --- /dev/null +++ b/maubot/management/frontend/src/Login.js @@ -0,0 +1,47 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" + +class Login extends Component { + constructor(props, context) { + super(props, context) + this.state = { + username: "", + password: "", + } + } + + inputChanged = event => this.setState({ [event.target.name]: event.target.value }) + + login = () => { + + } + + render() { + return
+
+

Maubot Manager

+ + + +
+
+ } +} + +export default Login diff --git a/maubot/management/frontend/src/PrivateRoute.js b/maubot/management/frontend/src/PrivateRoute.js new file mode 100644 index 0000000..2180e96 --- /dev/null +++ b/maubot/management/frontend/src/PrivateRoute.js @@ -0,0 +1,16 @@ +import React, { Component } from "react" +import { Route, Redirect } from "react-router-dom" + +const PrivateRoute = ({ component, authed, ...rest }) => ( + authed === true + ? + : } + /> +) + +export default PrivateRoute diff --git a/maubot/management/frontend/src/Router.js b/maubot/management/frontend/src/Router.js new file mode 100644 index 0000000..3eb4faf --- /dev/null +++ b/maubot/management/frontend/src/Router.js @@ -0,0 +1,41 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" +import { BrowserRouter as Router, Route, Redirect } from "react-router-dom" +import PrivateRoute from "./PrivateRoute" +import Home from "./Home" +import Login from "./Login" + +class MaubotRouter extends Component { + constructor(props) { + super(props) + this.state = { + authed: localStorage.accessToken !== undefined, + } + } + + render() { + return +
+ }/> + + +
+
+ } +} + +export default MaubotRouter diff --git a/maubot/management/frontend/src/Spinner.js b/maubot/management/frontend/src/Spinner.js new file mode 100644 index 0000000..4f227d4 --- /dev/null +++ b/maubot/management/frontend/src/Spinner.js @@ -0,0 +1,9 @@ +import React from "react" + +const Spinner = () => ( +
+ + + +
+) diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js new file mode 100644 index 0000000..712eb0d --- /dev/null +++ b/maubot/management/frontend/src/api.js @@ -0,0 +1,86 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const BASE_PATH = "/_matrix/maubot/v1" + +export function login(username, password) { + return fetch(`${BASE_PATH}/auth/login`, { + method: "POST", + body: JSON.stringify({ + username, + password, + }), + }) +} + +function getHeaders(contentType = "application/json") { + return { + "Content-Type": contentType, + "Authorization": `Bearer ${localStorage.accessToken}`, + } +} + +export async function ping() { + const response = await fetch(`${BASE_PATH}/auth/ping`, { + method: "POST", + headers: getHeaders(), + }) + const json = await response.json() + if (json.username) { + return json.username + } else if (json.errcode === "auth_token_missing" || json.errcode === "auth_token_invalid") { + return null + } + throw json +} + +export async function getInstances() { + const resp = await fetch(`${BASE_PATH}/instances`, { headers: getHeaders() }) + return await resp.json() +} + +export async function getInstance(id) { + const resp = await fetch(`${BASE_PATH}/instance/${id}`, { headers: getHeaders() }) + return await resp.json() +} + +export async function getPlugins() { + const resp = await fetch(`${BASE_PATH}/plugins`, { headers: getHeaders() }) + return await resp.json() +} + +export async function getPlugin(id) { + const resp = await fetch(`${BASE_PATH}/plugin/${id}`, { headers: getHeaders() }) + return await resp.json() +} + +export async function uploadPlugin(data) { + const resp = await fetch(`${BASE_PATH}/plugins/upload`, { + headers: getHeaders("application/zip"), + body: data, + }) + return await resp.json() +} + +export async function getClients() { + const resp = await fetch(`${BASE_PATH}/clients`, { headers: getHeaders() }) + return await resp.json() +} + +export async function getClient(id) { + const resp = await fetch(`${BASE_PATH}/client/${id}`, { headers: getHeaders() }) + return await resp.json() +} diff --git a/maubot/management/frontend/src/index.js b/maubot/management/frontend/src/index.js index 8f50972..9e9203f 100644 --- a/maubot/management/frontend/src/index.js +++ b/maubot/management/frontend/src/index.js @@ -16,6 +16,6 @@ import React from "react" import ReactDOM from "react-dom" import "./style/index.sass" -import MaubotManager from "./MaubotManager" +import App from "./Router" -ReactDOM.render(, document.getElementById("root")) +ReactDOM.render(, document.getElementById("root")) diff --git a/maubot/management/frontend/src/style/base/body.sass b/maubot/management/frontend/src/style/base/body.sass index b45c068..01d90e9 100644 --- a/maubot/management/frontend/src/style/base/body.sass +++ b/maubot/management/frontend/src/style/base/body.sass @@ -27,6 +27,13 @@ body right: 0 left: 0 +.maubot-wrapper + position: absolute + top: 0 + bottom: 0 + left: 0 + right: 0 + //.lindeb > header position: absolute diff --git a/maubot/management/frontend/src/style/base/elements.sass b/maubot/management/frontend/src/style/base/elements.sass new file mode 100644 index 0000000..10a183e --- /dev/null +++ b/maubot/management/frontend/src/style/base/elements.sass @@ -0,0 +1,95 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +=button() + font-family: $font-stack + padding: .375rem 1rem + background-color: $background-color + border: none + border-radius: .25rem + color: $inverted-text-color + box-sizing: border-box + font-size: 1rem + cursor: pointer + + &:hover + background-color: darken($background-color, 10%) + +=link-button() + display: inline-block + text-align: center + text-decoration: none + +=main-color-button() + background-color: $main-color + &:hover + background-color: $dark-color + +button, .button + +button + + &.main-color + +main-color-button + +=button-group() + width: 100% + display: flex + > button, > .button + flex: 1 + border-radius: 0 + + &:first-of-type + border-radius: .25rem 0 0 .25rem + + &:last-of-type + border-radius: 0 .25rem .25rem 0 + + &:first-of-type:last-of-type + border-radius: .25rem + +=vertical-button-group() + display: flex + flex-direction: column + > button, > .button + flex: 1 + border-radius: 0 + + &:first-of-type + border-radius: .25rem .25rem 0 0 + + &:last-of-type + border-radius: 0 0 .25rem .25rem + + &:first-of-type:last-of-type + border-radius: .25rem + +input, textarea + font-family: $font-stack + border: 1px solid $border-color + background-color: $background-color + color: $text-color + box-sizing: border-box + border-radius: .25rem + padding: .375rem 1rem + font-size: 1rem + resize: vertical + + &:hover, &:focus + border-color: $main-color + + &:focus + border-width: 2px + padding: calc(.375rem - 1px) 1rem diff --git a/maubot/management/frontend/src/style/index.sass b/maubot/management/frontend/src/style/index.sass index d3d2eaa..20d193d 100644 --- a/maubot/management/frontend/src/style/index.sass +++ b/maubot/management/frontend/src/style/index.sass @@ -15,3 +15,8 @@ // along with this program. If not, see . @import base/vars @import base/body +@import base/elements + +@import lib/spinner + +@import pages/login diff --git a/maubot/management/frontend/src/style/lib/spinner.sass b/maubot/management/frontend/src/style/lib/spinner.sass new file mode 100644 index 0000000..3fb4cbc --- /dev/null +++ b/maubot/management/frontend/src/style/lib/spinner.sass @@ -0,0 +1,60 @@ +$green: #008744 +$blue: #0057e7 +$red: #d62d20 +$yellow: #ffa700 +$white: #eee + +$width: 100px + +.loader + position: relative + margin: 0 auto + width: $width + + &:before + content: "" + display: block + padding-top: 100% + + svg + animation: rotate 2s linear infinite + height: 100% + transform-origin: center center + width: 100% + position: absolute + top: 0 + bottom: 0 + left: 0 + right: 0 + margin: auto + + circle + stroke-dasharray: 1, 200 + stroke-dashoffset: 0 + animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite + stroke-linecap: round + +@keyframes rotate + 100% + transform: rotate(360deg) + +@keyframes dash + 0% + stroke-dasharray: 1, 200 + stroke-dashoffset: 0 + 50% + stroke-dasharray: 89, 200 + stroke-dashoffset: -35px + 100% + stroke-dasharray: 89, 200 + stroke-dashoffset: -124px + +@keyframes color + 100%, 0% + stroke: $red + 40% + stroke: $blue + 66% + stroke: $green + 80%, 90% + stroke: $yellow diff --git a/maubot/management/frontend/src/style/pages/login.sass b/maubot/management/frontend/src/style/pages/login.sass new file mode 100644 index 0000000..f3d04fa --- /dev/null +++ b/maubot/management/frontend/src/style/pages/login.sass @@ -0,0 +1,44 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +.maubot-wrapper:not(.authenticated) + background-color: $main-color + + text-align: center + +.login + width: 25rem + height: 23.5rem + display: inline-block + box-sizing: border-box + background-color: white + border-radius: .25rem + margin-top: 3rem + + .title + color: $main-color + margin: 3rem 0 + + input, button + width: calc(100% - 5rem) + margin: .5rem 2.5rem + padding: 1rem + + input:focus + padding: calc(1rem - 1px) + + button + +main-color-button diff --git a/maubot/management/frontend/yarn.lock b/maubot/management/frontend/yarn.lock index 64d5190..9b58f83 100644 --- a/maubot/management/frontend/yarn.lock +++ b/maubot/management/frontend/yarn.lock @@ -4316,6 +4316,17 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== +history@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b" + integrity sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA== + dependencies: + invariant "^2.2.1" + loose-envify "^1.2.0" + resolve-pathname "^2.2.0" + value-equal "^0.4.0" + warning "^3.0.0" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -4330,6 +4341,11 @@ hoek@4.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA== +hoist-non-react-statics@^2.5.0: + version "2.5.5" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" + integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -4643,7 +4659,7 @@ internal-ip@^3.0.1: default-gateway "^2.6.0" ipaddr.js "^1.5.2" -invariant@^2.2.0, invariant@^2.2.2, invariant@^2.2.4: +invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -5851,7 +5867,7 @@ loglevel@^1.4.1: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" integrity sha1-4PyVEztu8nbNyIh82vJKpvFW+Po= -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -6895,6 +6911,13 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + integrity sha1-Wf3g9DW62suhA6hOnTvGTpa5k30= + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -7698,7 +7721,7 @@ prompts@^0.1.9: kleur "^2.0.1" sisteransi "^0.1.1" -prop-types@^15.6.2: +prop-types@^15.6.1, prop-types@^15.6.2: version "15.6.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== @@ -7916,6 +7939,31 @@ react-error-overlay@^5.0.5: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.0.5.tgz#716ff1a92fda76bb89a39adf9ce046a5d740e71a" integrity sha512-ab0HWBgxdIsngHtMGU8+8gYFdTBXpUGd4AE4lN2VZvOIlBmWx9dtaWEViihuGSIGosCKPeHCnzFoRWB42UtnLg== +react-router-dom@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6" + integrity sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA== + dependencies: + history "^4.7.2" + invariant "^2.2.4" + loose-envify "^1.3.1" + prop-types "^15.6.1" + react-router "^4.3.1" + warning "^4.0.1" + +react-router@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e" + integrity sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg== + dependencies: + history "^4.7.2" + hoist-non-react-statics "^2.5.0" + invariant "^2.2.4" + loose-envify "^1.3.1" + path-to-regexp "^1.7.0" + prop-types "^15.6.1" + warning "^4.0.1" + react-scripts@2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-2.0.5.tgz#74b8e9fa6a7c5f0f11221dd18c10df2ae3df3d69" @@ -8303,6 +8351,11 @@ resolve-from@^3.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" integrity sha1-six699nWiBvItuZTM17rywoYh0g= +resolve-pathname@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879" + integrity sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg== + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -9559,6 +9612,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +value-equal@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7" + integrity sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw== + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -9599,6 +9657,20 @@ walker@~1.0.5: dependencies: makeerror "1.0.x" +warning@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c" + integrity sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w= + dependencies: + loose-envify "^1.0.0" + +warning@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.2.tgz#aa6876480872116fa3e11d434b0d0d8d91e44607" + integrity sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug== + dependencies: + loose-envify "^1.0.0" + watch@~0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" From 4bc78996c1e60c9200c4a92fffe6c9265d03c67b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 4 Nov 2018 23:05:39 +0200 Subject: [PATCH 034/101] Add UID/GID option and custom config base for docker Closes #13 --- Dockerfile | 6 +++- docker-run.sh | 25 --------------- docker/example-config.yaml | 66 ++++++++++++++++++++++++++++++++++++++ docker/run.sh | 21 ++++++++++++ 4 files changed, 92 insertions(+), 26 deletions(-) delete mode 100755 docker-run.sh create mode 100644 docker/example-config.yaml create mode 100755 docker/run.sh diff --git a/Dockerfile b/Dockerfile index d9a43f9..710b778 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ FROM alpine:3.8 +ENV UID=1337 \ + GID=1337 + COPY . /opt/maubot WORKDIR /opt/maubot RUN apk add --no-cache \ @@ -9,8 +12,9 @@ RUN apk add --no-cache \ py3-bcrypt \ py3-cffi \ ca-certificates \ + su-exec \ && pip3 install -r requirements.txt VOLUME /data -CMD ["/opt/maubot/docker-run.sh"] +CMD ["/opt/maubot/docker/run.sh"] diff --git a/docker-run.sh b/docker-run.sh deleted file mode 100755 index 424953a..0000000 --- a/docker-run.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh - -cd /opt/maubot - -# Replace database path in config. -sed -i "s#sqlite:///maubot.db#sqlite:////data/maubot.db#" /data/config.yaml -sed -i "s#- ./plugins#- /data/plugins#" /data/config.yaml -sed -i "s#upload: ./plugins#upload: /data/plugins#" /data/config.yaml -sed -i "s#trash: ./trash#trash: /data/trash#" /data/config.yaml -sed -i "s#db: ./plugins#trash: /data/dbs#" /data/config.yaml -sed -i "s#./logs/maubot.log#/var/log/maubot/maubot.log#" /data/config.yaml - -mkdir -p /var/log/maubot /data/plugins /data/trash /data/dbs - -# Check that database is in the right state -alembic -x config=/data/config.yaml upgrade head - -if [ ! -f /data/config.yaml ]; then - cp example-config.yaml /data/config.yaml - echo "Config file not found. Example config copied to /data/config.yaml" - echo "Please modify the config file to your liking and restart the container." - exit -fi - -python3 -m maubot -c /data/config.yaml diff --git a/docker/example-config.yaml b/docker/example-config.yaml new file mode 100644 index 0000000..77f97f0 --- /dev/null +++ b/docker/example-config.yaml @@ -0,0 +1,66 @@ +# The full URI to the database. SQLite and Postgres are fully supported. +# Other DBMSes supported by SQLAlchemy may or may not work. +# Format examples: +# SQLite: sqlite:///filename.db +# Postgres: postgres://username:password@hostname/dbname +database: sqlite:////data/maubot.db + +plugin_directories: + # The directory where uploaded new plugins should be stored. + upload: /data/plugins + # The directories from which plugins should be loaded. + # Duplicate plugin IDs will be moved to the trash. + load: + - /data/plugins + # The directory where old plugin versions and conflicting plugins should be moved. + # Set to "delete" to delete files immediately. + trash: /data/trash + # The directory where plugin databases should be stored. + db: /data/plugins + +server: + # The IP and port to listen to. + hostname: 0.0.0.0 + port: 29316 + # The base management API path. + base_path: /_matrix/maubot/v1 + # The base appservice API path. Use / for legacy appservice API and /_matrix/app/v1 for v1. + appservice_base_path: /_matrix/app/v1 + # The shared secret to sign API access tokens. + # Set to "generate" to generate and save a new token at startup. + unshared_secret: generate + +# List of administrator users. Plaintext passwords will be bcrypted on startup. Set empty password +# to prevent normal login. Root is a special user that can't have a password and will always exist. +admins: + root: "" + +# Python logging configuration. +# +# See section 16.7.2 of the Python documentation for more info: +# https://docs.python.org/3.6/library/logging.config.html#configuration-dictionary-schema +logging: + version: 1 + formatters: + precise: + format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s" + handlers: + file: + class: logging.handlers.RotatingFileHandler + formatter: precise + filename: /var/log/maubot.log + maxBytes: 10485760 + backupCount: 10 + console: + class: logging.StreamHandler + formatter: precise + loggers: + maubot: + level: DEBUG + mautrix: + level: DEBUG + aiohttp: + level: INFO + root: + level: DEBUG + handlers: [file, console] diff --git a/docker/run.sh b/docker/run.sh new file mode 100755 index 0000000..24c8f60 --- /dev/null +++ b/docker/run.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +function fixperms { + chown -R $UID:$GID /var/log /data /opt/maubot +} + +cd /opt/maubot + +if [ ! -f /data/config.yaml ]; then + cp docker/example-config.yaml /data/config.yaml + mkdir -p /var/log /data/plugins /data/trash /data/dbs + echo "Config file not found. Example config copied to /data/config.yaml" + echo "Please modify the config file to your liking and restart the container." + fixperms + exit +fi + +mkdir -p /var/log/maubot /data/plugins /data/trash /data/dbs +#alembic -x config=/data/config.yaml upgrade head +fixperms +exec su-exec $UID:$GID python3 -m maubot -c /data/config.yaml -b docker/example-config.yaml From f3a0b7bc4fa3f1511ebc2ec89a8b88591c79307f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 6 Nov 2018 23:27:17 +0200 Subject: [PATCH 035/101] Implement login --- maubot/management/frontend/src/Home.js | 2 +- maubot/management/frontend/src/Login.js | 26 +++++++++++--- .../src/{Router.js => MaubotRouter.js} | 35 +++++++++++++++++-- .../management/frontend/src/PrivateRoute.js | 28 ++++++++++----- maubot/management/frontend/src/Spinner.js | 6 ++-- maubot/management/frontend/src/api.js | 12 +++++-- maubot/management/frontend/src/index.js | 2 +- .../frontend/src/style/base/body.sass | 4 +++ .../frontend/src/style/base/elements.sass | 27 ++++++++++---- .../management/frontend/src/style/index.sass | 4 +-- .../frontend/src/style/lib/spinner.sass | 15 +++++--- .../frontend/src/style/pages/login.sass | 25 +++++++++---- 12 files changed, 145 insertions(+), 41 deletions(-) rename maubot/management/frontend/src/{Router.js => MaubotRouter.js} (61%) diff --git a/maubot/management/frontend/src/Home.js b/maubot/management/frontend/src/Home.js index 53a8466..464294d 100644 --- a/maubot/management/frontend/src/Home.js +++ b/maubot/management/frontend/src/Home.js @@ -18,7 +18,7 @@ import React, { Component } from "react" class Home extends Component { render() { return
- + Hello, {localStorage.username}
} } diff --git a/maubot/management/frontend/src/Login.js b/maubot/management/frontend/src/Login.js index 6788168..af95d07 100644 --- a/maubot/management/frontend/src/Login.js +++ b/maubot/management/frontend/src/Login.js @@ -14,6 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" +import Spinner from "./Spinner" +import api from "./api" class Login extends Component { constructor(props, context) { @@ -21,24 +23,38 @@ class Login extends Component { this.state = { username: "", password: "", + loading: false, + error: "", } } inputChanged = event => this.setState({ [event.target.name]: event.target.value }) - login = () => { - + login = async () => { + this.setState({ loading: true }) + const resp = await api.login(this.state.username, this.state.password) + if (resp.token) { + await this.props.onLogin(resp.token) + } else if (resp.error) { + this.setState({ error: resp.error, loading: false }) + } else { + this.setState({ error: "Unknown error", loading: false }) + console.log("Unknown error:", resp) + } } render() { return
-
-

Maubot Manager

+
+

Maubot Manager

- + + {this.state.error &&
{this.state.error}
}
} diff --git a/maubot/management/frontend/src/Router.js b/maubot/management/frontend/src/MaubotRouter.js similarity index 61% rename from maubot/management/frontend/src/Router.js rename to maubot/management/frontend/src/MaubotRouter.js index 3eb4faf..891f613 100644 --- a/maubot/management/frontend/src/Router.js +++ b/maubot/management/frontend/src/MaubotRouter.js @@ -18,21 +18,52 @@ import { BrowserRouter as Router, Route, Redirect } from "react-router-dom" import PrivateRoute from "./PrivateRoute" import Home from "./Home" import Login from "./Login" +import Spinner from "./Spinner" +import api from "./api" class MaubotRouter extends Component { constructor(props) { super(props) this.state = { - authed: localStorage.accessToken !== undefined, + pinged: false, + authed: false, } } + async componentWillMount() { + if (localStorage.accessToken) { + await this.ping() + } + this.setState({ pinged: true }) + } + + async ping() { + try { + const username = await api.ping() + if (username) { + localStorage.username = username + this.setState({ authed: true }) + } + } catch (err) { + console.error(err) + } + } + + login = async (token) => { + localStorage.accessToken = token + await this.ping() + } + render() { + if (!this.state.pinged) { + return + } return
}/> - + } + authed={!this.state.authed} to="/dashboard"/>
} diff --git a/maubot/management/frontend/src/PrivateRoute.js b/maubot/management/frontend/src/PrivateRoute.js index 2180e96..745078d 100644 --- a/maubot/management/frontend/src/PrivateRoute.js +++ b/maubot/management/frontend/src/PrivateRoute.js @@ -1,15 +1,27 @@ -import React, { Component } from "react" +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" import { Route, Redirect } from "react-router-dom" -const PrivateRoute = ({ component, authed, ...rest }) => ( +const PrivateRoute = ({ component, render, authed, to = "/login", ...args }) => ( authed === true - ? - : } + ? (component ? React.createElement(component, props) : render()) + : } /> ) diff --git a/maubot/management/frontend/src/Spinner.js b/maubot/management/frontend/src/Spinner.js index 4f227d4..77735f5 100644 --- a/maubot/management/frontend/src/Spinner.js +++ b/maubot/management/frontend/src/Spinner.js @@ -1,9 +1,11 @@ import React from "react" -const Spinner = () => ( -
+const Spinner = (props) => ( +
) + +export default Spinner diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index 712eb0d..7e1ef7f 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -16,14 +16,15 @@ const BASE_PATH = "/_matrix/maubot/v1" -export function login(username, password) { - return fetch(`${BASE_PATH}/auth/login`, { +export async function login(username, password) { + const resp = await fetch(`${BASE_PATH}/auth/login`, { method: "POST", body: JSON.stringify({ username, password, }), }) + return await resp.json() } function getHeaders(contentType = "application/json") { @@ -84,3 +85,10 @@ export async function getClient(id) { const resp = await fetch(`${BASE_PATH}/client/${id}`, { headers: getHeaders() }) return await resp.json() } + +export default { + login, ping, + getInstances, getInstance, + getPlugins, getPlugin, uploadPlugin, + getClients, getClient, +} diff --git a/maubot/management/frontend/src/index.js b/maubot/management/frontend/src/index.js index 9e9203f..cbe10ed 100644 --- a/maubot/management/frontend/src/index.js +++ b/maubot/management/frontend/src/index.js @@ -16,6 +16,6 @@ import React from "react" import ReactDOM from "react-dom" import "./style/index.sass" -import App from "./Router" +import App from "./MaubotRouter" ReactDOM.render(, document.getElementById("root")) diff --git a/maubot/management/frontend/src/style/base/body.sass b/maubot/management/frontend/src/style/base/body.sass index 01d90e9..462fe8c 100644 --- a/maubot/management/frontend/src/style/base/body.sass +++ b/maubot/management/frontend/src/style/base/body.sass @@ -34,6 +34,10 @@ body left: 0 right: 0 +.maubot-loading + margin-top: 10rem + width: 10rem + //.lindeb > header position: absolute diff --git a/maubot/management/frontend/src/style/base/elements.sass b/maubot/management/frontend/src/style/base/elements.sass index 10a183e..48fb045 100644 --- a/maubot/management/frontend/src/style/base/elements.sass +++ b/maubot/management/frontend/src/style/base/elements.sass @@ -14,9 +14,11 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -=button() +=button($width: null, $height: null, $padding: .375rem 1rem) font-family: $font-stack - padding: .375rem 1rem + padding: $padding + width: $width + height: $height background-color: $background-color border: none border-radius: .25rem @@ -38,7 +40,7 @@ &:hover background-color: $dark-color -button, .button +.button +button &.main-color @@ -76,15 +78,17 @@ button, .button &:first-of-type:last-of-type border-radius: .25rem -input, textarea +=input($width: null, $height: null, $vertical-padding: .375rem, $horizontal-padding: 1rem, $font-size: 1rem) font-family: $font-stack border: 1px solid $border-color background-color: $background-color color: $text-color + width: $width + height: $height box-sizing: border-box border-radius: .25rem - padding: .375rem 1rem - font-size: 1rem + padding: $vertical-padding $horizontal-padding + font-size: $font-size resize: vertical &:hover, &:focus @@ -92,4 +96,13 @@ input, textarea &:focus border-width: 2px - padding: calc(.375rem - 1px) 1rem + padding: calc(#{$vertical-padding} - 1px) calc(#{$horizontal-padding} - 1px) + +.input, .textarea + +input + +=notification($color: $error-color) + padding: 1rem + border-radius: .25rem + border: 2px solid $color + background-color: lighten($color, 25%) diff --git a/maubot/management/frontend/src/style/index.sass b/maubot/management/frontend/src/style/index.sass index 20d193d..8486157 100644 --- a/maubot/management/frontend/src/style/index.sass +++ b/maubot/management/frontend/src/style/index.sass @@ -13,10 +13,10 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +@import lib/spinner + @import base/vars @import base/body @import base/elements -@import lib/spinner - @import pages/login diff --git a/maubot/management/frontend/src/style/lib/spinner.sass b/maubot/management/frontend/src/style/lib/spinner.sass index 3fb4cbc..d164586 100644 --- a/maubot/management/frontend/src/style/lib/spinner.sass +++ b/maubot/management/frontend/src/style/lib/spinner.sass @@ -2,14 +2,11 @@ $green: #008744 $blue: #0057e7 $red: #d62d20 $yellow: #ffa700 -$white: #eee -$width: 100px - -.loader +.spinner position: relative margin: 0 auto - width: $width + width: 5rem &:before content: "" @@ -34,6 +31,14 @@ $width: 100px animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite stroke-linecap: round +=white-spinner() + circle + stroke: white !important + +=thick-spinner($thickness: 5) + svg > circle + stroke-width: $thickness + @keyframes rotate 100% transform: rotate(360deg) diff --git a/maubot/management/frontend/src/style/pages/login.sass b/maubot/management/frontend/src/style/pages/login.sass index f3d04fa..62b0891 100644 --- a/maubot/management/frontend/src/style/pages/login.sass +++ b/maubot/management/frontend/src/style/pages/login.sass @@ -21,24 +21,37 @@ .login width: 25rem - height: 23.5rem + height: 23rem display: inline-block box-sizing: border-box background-color: white border-radius: .25rem margin-top: 3rem - .title + h1 color: $main-color margin: 3rem 0 input, button - width: calc(100% - 5rem) margin: .5rem 2.5rem - padding: 1rem + height: 3rem + width: 20rem - input:focus - padding: calc(1rem - 1px) + input + +input button + +button($width: 20rem, $height: 3rem, $padding: 0) +main-color-button + + .spinner + +white-spinner + +thick-spinner + width: 2rem + + &.errored + height: 26.5rem + + .error + +notification($error-color) + margin: .5rem 2.5rem From 2ba1f46149b9ea2b10ba499ca929a48d8239e531 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 8 Nov 2018 01:02:46 +0200 Subject: [PATCH 036/101] Reorganize things, add sidebar and topbar --- maubot/management/frontend/src/Login.js | 2 +- .../management/frontend/src/MaubotRouter.js | 19 ++-- .../src/{ => components}/PrivateRoute.js | 0 .../frontend/src/{ => components}/Spinner.js | 0 .../src/dashboard/client/ListEntry.js | 30 +++++ .../src/{Home.js => dashboard/client/View.js} | 8 +- .../frontend/src/dashboard/index.js | 105 ++++++++++++++++++ .../src/dashboard/instance/ListEntry.js | 27 +++++ .../frontend/src/dashboard/instance/View.js | 24 ++++ .../src/dashboard/plugin/ListEntry.js | 27 +++++ .../frontend/src/dashboard/plugin/View.js | 24 ++++ .../frontend/src/res/chevron-right.svg | 1 + .../frontend/src/style/base/elements.sass | 12 +- .../frontend/src/style/base/vars.sass | 20 ++-- .../management/frontend/src/style/index.sass | 1 + .../src/style/pages/dashboard-grid.css | 6 + .../frontend/src/style/pages/dashboard.sass | 57 ++++++++++ .../frontend/src/style/pages/login.sass | 6 +- .../frontend/src/style/pages/sidebar.sass | 53 +++++++++ 19 files changed, 390 insertions(+), 32 deletions(-) rename maubot/management/frontend/src/{ => components}/PrivateRoute.js (100%) rename maubot/management/frontend/src/{ => components}/Spinner.js (100%) create mode 100644 maubot/management/frontend/src/dashboard/client/ListEntry.js rename maubot/management/frontend/src/{Home.js => dashboard/client/View.js} (86%) create mode 100644 maubot/management/frontend/src/dashboard/index.js create mode 100644 maubot/management/frontend/src/dashboard/instance/ListEntry.js create mode 100644 maubot/management/frontend/src/dashboard/instance/View.js create mode 100644 maubot/management/frontend/src/dashboard/plugin/ListEntry.js create mode 100644 maubot/management/frontend/src/dashboard/plugin/View.js create mode 100644 maubot/management/frontend/src/res/chevron-right.svg create mode 100644 maubot/management/frontend/src/style/pages/dashboard-grid.css create mode 100644 maubot/management/frontend/src/style/pages/dashboard.sass create mode 100644 maubot/management/frontend/src/style/pages/sidebar.sass diff --git a/maubot/management/frontend/src/Login.js b/maubot/management/frontend/src/Login.js index af95d07..e342abe 100644 --- a/maubot/management/frontend/src/Login.js +++ b/maubot/management/frontend/src/Login.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" -import Spinner from "./Spinner" +import Spinner from "./components/Spinner" import api from "./api" class Login extends Component { diff --git a/maubot/management/frontend/src/MaubotRouter.js b/maubot/management/frontend/src/MaubotRouter.js index 891f613..b3c7561 100644 --- a/maubot/management/frontend/src/MaubotRouter.js +++ b/maubot/management/frontend/src/MaubotRouter.js @@ -14,11 +14,11 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" -import { BrowserRouter as Router, Route, Redirect } from "react-router-dom" -import PrivateRoute from "./PrivateRoute" -import Home from "./Home" +import { BrowserRouter as Router, Switch } from "react-router-dom" +import PrivateRoute from "./components/PrivateRoute" +import Dashboard from "./dashboard" import Login from "./Login" -import Spinner from "./Spinner" +import Spinner from "./components/Spinner" import api from "./api" class MaubotRouter extends Component { @@ -43,6 +43,8 @@ class MaubotRouter extends Component { if (username) { localStorage.username = username this.setState({ authed: true }) + } else { + localStorage.accessToken = undefined } } catch (err) { console.error(err) @@ -60,10 +62,11 @@ class MaubotRouter extends Component { } return
- }/> - - } - authed={!this.state.authed} to="/dashboard"/> + + } + authed={!this.state.authed} to="/"/> + +
} diff --git a/maubot/management/frontend/src/PrivateRoute.js b/maubot/management/frontend/src/components/PrivateRoute.js similarity index 100% rename from maubot/management/frontend/src/PrivateRoute.js rename to maubot/management/frontend/src/components/PrivateRoute.js diff --git a/maubot/management/frontend/src/Spinner.js b/maubot/management/frontend/src/components/Spinner.js similarity index 100% rename from maubot/management/frontend/src/Spinner.js rename to maubot/management/frontend/src/components/Spinner.js diff --git a/maubot/management/frontend/src/dashboard/client/ListEntry.js b/maubot/management/frontend/src/dashboard/client/ListEntry.js new file mode 100644 index 0000000..e19055f --- /dev/null +++ b/maubot/management/frontend/src/dashboard/client/ListEntry.js @@ -0,0 +1,30 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" +import { Link } from "react-router-dom" +import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" + +const ClientListEntry = ({ client }) => ( + + {client.id.substr(1, + {client.displayname || client.id} + + +) + +export default ClientListEntry diff --git a/maubot/management/frontend/src/Home.js b/maubot/management/frontend/src/dashboard/client/View.js similarity index 86% rename from maubot/management/frontend/src/Home.js rename to maubot/management/frontend/src/dashboard/client/View.js index 464294d..5256549 100644 --- a/maubot/management/frontend/src/Home.js +++ b/maubot/management/frontend/src/dashboard/client/View.js @@ -15,12 +15,10 @@ // along with this program. If not, see . import React, { Component } from "react" -class Home extends Component { +class ClientView extends Component { render() { - return
- Hello, {localStorage.username} -
+ return
{this.props.client.displayname}
} } -export default Home +export default ClientView diff --git a/maubot/management/frontend/src/dashboard/index.js b/maubot/management/frontend/src/dashboard/index.js new file mode 100644 index 0000000..6e40a8f --- /dev/null +++ b/maubot/management/frontend/src/dashboard/index.js @@ -0,0 +1,105 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" +import { Route, Redirect } from "react-router-dom" +import api from "../api" +import InstanceListEntry from "./instance/ListEntry" +import InstanceView from "./instance/View" +import ClientListEntry from "./client/ListEntry" +import ClientView from "./client/View" +import PluginListEntry from "./plugin/ListEntry" +import PluginView from "./plugin/View" + +class Dashboard extends Component { + constructor(props) { + super(props) + this.state = { + instances: {}, + clients: {}, + plugins: {}, + } + global.maubot = this + } + + async componentWillMount() { + const [instanceList, clientList, pluginList] = await Promise.all([ + api.getInstances(), api.getClients(), api.getPlugins()]) + const instances = {} + for (const instance of instanceList) { + instances[instance.id] = instance + } + const clients = {} + for (const client of clientList) { + clients[client.id] = client + } + const plugins = {} + for (const plugin of pluginList) { + plugins[plugin.id] = plugin + } + this.setState({ instances, clients, plugins }) + } + + renderList(field, type) { + return Object.values(this.state[field + "s"]).map(entry => + React.createElement(type, { key: entry.id, [field]: entry })) + } + + renderView(field, type, id) { + const entry = this.state[field + "s"][id] + if (!entry) { + return "Not found :(" + } + return React.createElement(type, { [field]: entry }) + } + + render() { + return
+
+ + Maubot Manager +
+
+ {localStorage.username} +
+ +
+ "Hello, World!"}/> + + this.renderView("instance", InstanceView, match.params.id)}/> + + this.renderView("client", ClientView, match.params.id)}/> + + this.renderView("plugin", PluginView, match.params.id)}/> + }/> +
+
+ } +} + +export default Dashboard diff --git a/maubot/management/frontend/src/dashboard/instance/ListEntry.js b/maubot/management/frontend/src/dashboard/instance/ListEntry.js new file mode 100644 index 0000000..0603e4d --- /dev/null +++ b/maubot/management/frontend/src/dashboard/instance/ListEntry.js @@ -0,0 +1,27 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" +import { Link } from "react-router-dom" +import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" + +const InstanceListEntry = ({ instance }) => ( + + {instance.id} + + +) + +export default InstanceListEntry diff --git a/maubot/management/frontend/src/dashboard/instance/View.js b/maubot/management/frontend/src/dashboard/instance/View.js new file mode 100644 index 0000000..69bdf9f --- /dev/null +++ b/maubot/management/frontend/src/dashboard/instance/View.js @@ -0,0 +1,24 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" + +class InstanceView extends Component { + render() { + return
{this.props.instance.id}
+ } +} + +export default InstanceView diff --git a/maubot/management/frontend/src/dashboard/plugin/ListEntry.js b/maubot/management/frontend/src/dashboard/plugin/ListEntry.js new file mode 100644 index 0000000..6facdbf --- /dev/null +++ b/maubot/management/frontend/src/dashboard/plugin/ListEntry.js @@ -0,0 +1,27 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" +import { Link } from "react-router-dom" +import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" + +const PluginListEntry = ({ plugin }) => ( + + {plugin.id} + + +) + +export default PluginListEntry diff --git a/maubot/management/frontend/src/dashboard/plugin/View.js b/maubot/management/frontend/src/dashboard/plugin/View.js new file mode 100644 index 0000000..5b8ccbc --- /dev/null +++ b/maubot/management/frontend/src/dashboard/plugin/View.js @@ -0,0 +1,24 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" + +class PluginView extends Component { + render() { + return
{this.props.plugin.id}
+ } +} + +export default PluginView diff --git a/maubot/management/frontend/src/res/chevron-right.svg b/maubot/management/frontend/src/res/chevron-right.svg new file mode 100644 index 0000000..58ee688 --- /dev/null +++ b/maubot/management/frontend/src/res/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/maubot/management/frontend/src/style/base/elements.sass b/maubot/management/frontend/src/style/base/elements.sass index 48fb045..93d10f0 100644 --- a/maubot/management/frontend/src/style/base/elements.sass +++ b/maubot/management/frontend/src/style/base/elements.sass @@ -36,9 +36,9 @@ text-decoration: none =main-color-button() - background-color: $main-color + background-color: $primary &:hover - background-color: $dark-color + background-color: $primary-dark .button +button @@ -92,7 +92,7 @@ resize: vertical &:hover, &:focus - border-color: $main-color + border-color: $primary &:focus border-width: 2px @@ -101,8 +101,8 @@ .input, .textarea +input -=notification($color: $error-color) +=notification($border: $error-dark, $background: transparentize($error-light, 0.5)) padding: 1rem border-radius: .25rem - border: 2px solid $color - background-color: lighten($color, 25%) + border: 2px solid $border + background-color: $background diff --git a/maubot/management/frontend/src/style/base/vars.sass b/maubot/management/frontend/src/style/base/vars.sass index 9dc77dd..6e9a6c3 100644 --- a/maubot/management/frontend/src/style/base/vars.sass +++ b/maubot/management/frontend/src/style/base/vars.sass @@ -13,16 +13,18 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -$main-color: darken(#50D367, 10%) -$dark-color: darken($main-color, 10%) -$light-color: lighten($main-color, 10%) -$alt-color: darken(#47B9D7, 10%) -$dark-alt-color: darken($alt-color, 10%) -$border-color: #CCC -$error-color: #D35067 +$primary: #00C853 +$primary-dark: #009624 +$primary-light: #5EFC82 +$secondary: #00B8D4 +$secondary-dark: #0088A3 +$secondary-light: #62EBFF +$error: #B71C1C +$error-dark: #7F0000 +$error-light: #F05545 + +$border-color: #DDD $text-color: #212121 $background-color: #FAFAFA $inverted-text-color: $background-color $font-stack: sans-serif -$max-width: 42.5rem -$header-height: 3.5rem diff --git a/maubot/management/frontend/src/style/index.sass b/maubot/management/frontend/src/style/index.sass index 8486157..c2e9a16 100644 --- a/maubot/management/frontend/src/style/index.sass +++ b/maubot/management/frontend/src/style/index.sass @@ -20,3 +20,4 @@ @import base/elements @import pages/login +@import pages/dashboard diff --git a/maubot/management/frontend/src/style/pages/dashboard-grid.css b/maubot/management/frontend/src/style/pages/dashboard-grid.css new file mode 100644 index 0000000..26906db --- /dev/null +++ b/maubot/management/frontend/src/style/pages/dashboard-grid.css @@ -0,0 +1,6 @@ +.dashboard { + grid-template: + [row1-start] "title topbar" 3.5rem [row1-end] + [row2-start] "sidebar main" auto [row2-end] + / 15rem auto; +} diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass new file mode 100644 index 0000000..4be4111 --- /dev/null +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -0,0 +1,57 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +@import "dashboard-grid" + +.dashboard + display: grid + height: 100% + + > .title + grid-area: title + display: flex + align-items: center + justify-content: center + + font-size: 1.35rem + font-weight: bold + + color: $text-color + + z-index: 1 + + background-color: $background-color + border-right: 1px solid $primary + border-bottom: 1px solid $border-color + + > img + max-width: 2rem + margin-right: .5rem + + > .topbar + grid-area: topbar + display: flex + align-items: center + justify-content: center + background-color: $primary + width: 110% + margin: 0 -5% + box-shadow: 0 .25rem .25rem rgba(0, 0, 0, .25) + + + @import "sidebar" + + > .dashboard + grid-area: main diff --git a/maubot/management/frontend/src/style/pages/login.sass b/maubot/management/frontend/src/style/pages/login.sass index 62b0891..6447f54 100644 --- a/maubot/management/frontend/src/style/pages/login.sass +++ b/maubot/management/frontend/src/style/pages/login.sass @@ -15,7 +15,7 @@ // along with this program. If not, see . .maubot-wrapper:not(.authenticated) - background-color: $main-color + background-color: $primary text-align: center @@ -29,7 +29,7 @@ margin-top: 3rem h1 - color: $main-color + color: $primary margin: 3rem 0 input, button @@ -53,5 +53,5 @@ height: 26.5rem .error - +notification($error-color) + +notification($error) margin: .5rem 2.5rem diff --git a/maubot/management/frontend/src/style/pages/sidebar.sass b/maubot/management/frontend/src/style/pages/sidebar.sass new file mode 100644 index 0000000..a96a2cd --- /dev/null +++ b/maubot/management/frontend/src/style/pages/sidebar.sass @@ -0,0 +1,53 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> .sidebar + grid-area: sidebar + background-color: $background-color + + border-right: 1px solid $border-color + padding: .5rem + + div.list + margin-bottom: 1.5rem + + h3.title + margin: 0 + + a.entry + display: block + color: $text-color + text-decoration: none + + &:not(:hover) > svg + display: none + + > svg + float: right + + padding: .25rem + + &.client + padding: .25rem + + img.avatar + max-height: 1.5rem + border-radius: 100% + vertical-align: middle + + span.displayname + margin-left: .25rem + vertical-align: middle From 9a21c6ade8854f73d56b09536f973408259228df Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 8 Nov 2018 17:25:00 +0200 Subject: [PATCH 037/101] Add endpoint to replace specific plugin --- maubot/management/api/plugin.py | 64 ++++++++++++++++++++---------- maubot/management/api/responses.py | 14 +++++++ maubot/management/api/spec.yaml | 43 ++++++++++++++++++++ 3 files changed, 100 insertions(+), 21 deletions(-) diff --git a/maubot/management/api/plugin.py b/maubot/management/api/plugin.py index 4fbb209..3113124 100644 --- a/maubot/management/api/plugin.py +++ b/maubot/management/api/plugin.py @@ -69,6 +69,45 @@ async def reload_plugin(request: web.Request) -> web.Response: return resp.ok +@routes.put("/plugin/{id}") +async def put_plugin(request: web.Request) -> web.Response: + plugin_id = request.match_info.get("id", None) + content = await request.read() + file = BytesIO(content) + try: + pid, version = ZippedPluginLoader.verify_meta(file) + except MaubotZipImportError as e: + return resp.plugin_import_error(str(e), traceback.format_exc()) + if pid != plugin_id: + return resp.pid_mismatch + plugin = PluginLoader.id_cache.get(plugin_id, None) + if not plugin: + return await upload_new_plugin(content, pid, version) + elif isinstance(plugin, ZippedPluginLoader): + return await upload_replacement_plugin(plugin, content, version) + else: + return resp.unsupported_plugin_loader + + +@routes.post("/plugins/upload") +async def upload_plugin(request: web.Request) -> web.Response: + content = await request.read() + file = BytesIO(content) + try: + pid, version = ZippedPluginLoader.verify_meta(file) + except MaubotZipImportError as e: + return resp.plugin_import_error(str(e), traceback.format_exc()) + plugin = PluginLoader.id_cache.get(pid, None) + if not plugin: + return await upload_new_plugin(content, pid, version) + elif not request.query.get("allow_override"): + return resp.plugin_exists + elif isinstance(plugin, ZippedPluginLoader): + return await upload_replacement_plugin(plugin, content, version) + else: + return resp.unsupported_plugin_loader + + async def upload_new_plugin(content: bytes, pid: str, version: str) -> web.Response: path = os.path.join(get_config()["plugin_directories.upload"], f"{pid}-v{version}.mbp") with open(path, "wb") as p: @@ -86,10 +125,10 @@ async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, dirname = os.path.dirname(plugin.path) old_filename = os.path.basename(plugin.path) if plugin.version in old_filename: - filename = old_filename.replace(plugin.version, new_version) - if filename == old_filename: - filename = re.sub(f"{re.escape(plugin.version)}(-ts[0-9]+)?", - f"{new_version}-ts{int(time())}", old_filename) + replacement = (new_version if plugin.version != new_version + else f"{new_version}-ts{int(time())}") + filename = re.sub(f"{re.escape(plugin.version)}(-ts[0-9]+)?", + replacement, old_filename) else: filename = old_filename.rstrip(".mbp") filename = f"{filename}-v{new_version}.mbp" @@ -110,20 +149,3 @@ async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, await plugin.start_instances() ZippedPluginLoader.trash(old_path, reason="update") return resp.updated(plugin.to_dict()) - - -@routes.post("/plugins/upload") -async def upload_plugin(request: web.Request) -> web.Response: - content = await request.read() - file = BytesIO(content) - try: - pid, version = ZippedPluginLoader.verify_meta(file) - except MaubotZipImportError as e: - return resp.plugin_import_error(str(e), traceback.format_exc()) - plugin = PluginLoader.id_cache.get(pid, None) - if not plugin: - return await upload_new_plugin(content, pid, version) - elif isinstance(plugin, ZippedPluginLoader): - return await upload_replacement_plugin(plugin, content, version) - else: - return resp.unsupported_plugin_loader diff --git a/maubot/management/api/responses.py b/maubot/management/api/responses.py index 9c815c2..be9b3b0 100644 --- a/maubot/management/api/responses.py +++ b/maubot/management/api/responses.py @@ -61,6 +61,13 @@ class _Response: "errcode": "mxid_mismatch", }, status=HTTPStatus.BAD_REQUEST) + @property + def pid_mismatch(self) -> web.Response: + return web.json_response({ + "error": "The ID in the path does not match the ID of the uploaded plugin", + "errcode": "pid_mismatch", + }, status=HTTPStatus.BAD_REQUEST) + @property def bad_auth(self) -> web.Response: return web.json_response({ @@ -138,6 +145,13 @@ class _Response: "errcode": "user_exists", }, status=HTTPStatus.CONFLICT) + @property + def plugin_exists(self) -> web.Response: + return web.json_response({ + "error": "A plugin with the same ID as the uploaded plugin already exists", + "errcode": "plugin_exists" + }, status=HTTPStatus.CONFLICT) + @property def plugin_in_use(self) -> web.Response: return web.json_response({ diff --git a/maubot/management/api/spec.yaml b/maubot/management/api/spec.yaml index 75ec865..efe759e 100644 --- a/maubot/management/api/spec.yaml +++ b/maubot/management/api/spec.yaml @@ -85,6 +85,14 @@ paths: summary: Upload a new plugin description: Upload a new plugin. If the plugin already exists, enabled instances will be restarted. tags: [Plugins] + parameters: + - name: allow_override + in: query + description: Set to allow overriding existing plugins + required: false + schema: + type: boolean + default: false responses: 200: description: Plugin uploaded and replaced current version successfully @@ -102,6 +110,8 @@ paths: $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' + 409: + description: Plugin requestBody: content: application/zip: @@ -150,6 +160,39 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + put: + operationId: put_plugin + summary: Upload a new or replacement plugin + description: | + Upload a new or replacement plugin with the specified ID. + A HTTP 400 will be returned if the ID of the uploaded plugin + doesn't match the ID in the path. If the plugin already + exists, enabled instances will be restarted. + tags: [Plugins] + responses: + 200: + description: Plugin uploaded and replaced current version successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Plugin' + 201: + description: New plugin uploaded successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Plugin' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + requestBody: + content: + application/zip: + schema: + type: string + format: binary + example: The plugin maubot archive (.mbp) /plugin/{id}/reload: parameters: - name: id From 3e661aa88716b670e5cca754ef2349b19ed81140 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 8 Nov 2018 17:25:22 +0200 Subject: [PATCH 038/101] New buttons in sidebar --- maubot/management/frontend/src/api.js | 20 ++++++-- .../frontend/src/dashboard/client/View.js | 2 +- .../frontend/src/dashboard/index.js | 47 ++++++++++++------- .../frontend/src/dashboard/instance/View.js | 2 +- .../frontend/src/dashboard/plugin/View.js | 2 +- maubot/management/frontend/src/res/plus.svg | 5 ++ .../frontend/src/style/pages/dashboard.sass | 7 +-- .../frontend/src/style/pages/sidebar.sass | 12 ++++- 8 files changed, 68 insertions(+), 29 deletions(-) create mode 100644 maubot/management/frontend/src/res/plus.svg diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index 7e1ef7f..d0acbb6 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -68,11 +68,21 @@ export async function getPlugin(id) { return await resp.json() } -export async function uploadPlugin(data) { - const resp = await fetch(`${BASE_PATH}/plugins/upload`, { - headers: getHeaders("application/zip"), - body: data, - }) +export async function uploadPlugin(data, id) { + let resp + if (id) { + resp = await fetch(`${BASE_PATH}/plugin/${id}`, { + headers: getHeaders("applcation/zip"), + body: data, + method: "PUT", + }) + } else { + resp = await fetch(`${BASE_PATH}/plugins/upload`, { + headers: getHeaders("application/zip"), + body: data, + method: "POST", + }) + } return await resp.json() } diff --git a/maubot/management/frontend/src/dashboard/client/View.js b/maubot/management/frontend/src/dashboard/client/View.js index 5256549..b63d58f 100644 --- a/maubot/management/frontend/src/dashboard/client/View.js +++ b/maubot/management/frontend/src/dashboard/client/View.js @@ -17,7 +17,7 @@ import React, { Component } from "react" class ClientView extends Component { render() { - return
{this.props.client.displayname}
+ return
{this.props.displayname}
} } diff --git a/maubot/management/frontend/src/dashboard/index.js b/maubot/management/frontend/src/dashboard/index.js index 6e40a8f..77ea69b 100644 --- a/maubot/management/frontend/src/dashboard/index.js +++ b/maubot/management/frontend/src/dashboard/index.js @@ -14,8 +14,9 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" -import { Route, Redirect } from "react-router-dom" +import { Route, Switch, Link } from "react-router-dom" import api from "../api" +import { ReactComponent as Plus } from "../res/plus.svg" import InstanceListEntry from "./instance/ListEntry" import InstanceView from "./instance/View" import ClientListEntry from "./client/ListEntry" @@ -62,41 +63,55 @@ class Dashboard extends Component { if (!entry) { return "Not found :(" } - return React.createElement(type, { [field]: entry }) + return React.createElement(type, entry) } render() { return
-
+ Maubot Manager -
+
{localStorage.username}
-
- "Hello, World!"}/> - - this.renderView("instance", InstanceView, match.params.id)}/> - - this.renderView("client", ClientView, match.params.id)}/> - - this.renderView("plugin", PluginView, match.params.id)}/> - }/> +
+ + "Hello, World!"}/> + }/> + }/> + }/> + + this.renderView("instance", InstanceView, match.params.id)}/> + + this.renderView("client", ClientView, match.params.id)}/> + + this.renderView("plugin", PluginView, match.params.id)}/> + "Not found :("}/> +
} diff --git a/maubot/management/frontend/src/dashboard/instance/View.js b/maubot/management/frontend/src/dashboard/instance/View.js index 69bdf9f..9a82cba 100644 --- a/maubot/management/frontend/src/dashboard/instance/View.js +++ b/maubot/management/frontend/src/dashboard/instance/View.js @@ -17,7 +17,7 @@ import React, { Component } from "react" class InstanceView extends Component { render() { - return
{this.props.instance.id}
+ return
{this.props.id}
} } diff --git a/maubot/management/frontend/src/dashboard/plugin/View.js b/maubot/management/frontend/src/dashboard/plugin/View.js index 5b8ccbc..fbcf2c3 100644 --- a/maubot/management/frontend/src/dashboard/plugin/View.js +++ b/maubot/management/frontend/src/dashboard/plugin/View.js @@ -17,7 +17,7 @@ import React, { Component } from "react" class PluginView extends Component { render() { - return
{this.props.plugin.id}
+ return
{this.props.id}
} } diff --git a/maubot/management/frontend/src/res/plus.svg b/maubot/management/frontend/src/res/plus.svg new file mode 100644 index 0000000..8030204 --- /dev/null +++ b/maubot/management/frontend/src/res/plus.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 4be4111..dd001a0 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -19,7 +19,7 @@ display: grid height: 100% - > .title + > a.title grid-area: title display: flex align-items: center @@ -29,6 +29,7 @@ font-weight: bold color: $text-color + text-decoration: none z-index: 1 @@ -40,7 +41,7 @@ max-width: 2rem margin-right: .5rem - > .topbar + > div.topbar grid-area: topbar display: flex align-items: center @@ -53,5 +54,5 @@ @import "sidebar" - > .dashboard + > main.dashboard grid-area: main diff --git a/maubot/management/frontend/src/style/pages/sidebar.sass b/maubot/management/frontend/src/style/pages/sidebar.sass index a96a2cd..47bbfd7 100644 --- a/maubot/management/frontend/src/style/pages/sidebar.sass +++ b/maubot/management/frontend/src/style/pages/sidebar.sass @@ -24,8 +24,16 @@ div.list margin-bottom: 1.5rem - h3.title - margin: 0 + div.title + h2 + margin: 0 + display: inline-block + + font-size: 1.25rem + + a + display: inline-block + float: right a.entry display: block From ed16ee88607d103586f17c90e8ccee9b5eee9daf Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 9 Nov 2018 00:54:42 +0200 Subject: [PATCH 039/101] Add initial parts of client view --- .../frontend/src/components/Switch.js | 54 +++++++++ .../src/dashboard/client/ListEntry.js | 30 ----- maubot/management/frontend/src/index.js | 2 +- .../frontend/src/{ => pages}/Login.js | 4 +- .../src/{MaubotRouter.js => pages/Main.js} | 10 +- .../frontend/src/pages/dashboard/Client.js | 114 ++++++++++++++++++ .../src/{ => pages}/dashboard/index.js | 15 ++- .../dashboard/instance/ListEntry.js | 2 +- .../{ => pages}/dashboard/instance/View.js | 0 .../{ => pages}/dashboard/plugin/ListEntry.js | 2 +- .../src/{ => pages}/dashboard/plugin/View.js | 0 maubot/management/frontend/src/res/upload.svg | 5 + .../frontend/src/style/base/body.sass | 5 +- .../frontend/src/style/base/elements.sass | 6 +- .../frontend/src/style/base/vars.sass | 6 +- .../management/frontend/src/style/index.sass | 2 +- .../frontend/src/style/lib/switch.sass | 79 ++++++++++++ .../frontend/src/style/pages/client.sass | 109 +++++++++++++++++ .../frontend/src/style/pages/dashboard.sass | 17 +-- .../View.js => style/pages/instance.sass} | 10 +- .../frontend/src/style/pages/plugin.sass | 18 +++ .../frontend/src/style/pages/sidebar.sass | 7 +- 22 files changed, 425 insertions(+), 72 deletions(-) create mode 100644 maubot/management/frontend/src/components/Switch.js delete mode 100644 maubot/management/frontend/src/dashboard/client/ListEntry.js rename maubot/management/frontend/src/{ => pages}/Login.js (97%) rename maubot/management/frontend/src/{MaubotRouter.js => pages/Main.js} (92%) create mode 100644 maubot/management/frontend/src/pages/dashboard/Client.js rename maubot/management/frontend/src/{ => pages}/dashboard/index.js (92%) rename maubot/management/frontend/src/{ => pages}/dashboard/instance/ListEntry.js (92%) rename maubot/management/frontend/src/{ => pages}/dashboard/instance/View.js (100%) rename maubot/management/frontend/src/{ => pages}/dashboard/plugin/ListEntry.js (92%) rename maubot/management/frontend/src/{ => pages}/dashboard/plugin/View.js (100%) create mode 100644 maubot/management/frontend/src/res/upload.svg create mode 100644 maubot/management/frontend/src/style/lib/switch.sass create mode 100644 maubot/management/frontend/src/style/pages/client.sass rename maubot/management/frontend/src/{dashboard/client/View.js => style/pages/instance.sass} (80%) create mode 100644 maubot/management/frontend/src/style/pages/plugin.sass diff --git a/maubot/management/frontend/src/components/Switch.js b/maubot/management/frontend/src/components/Switch.js new file mode 100644 index 0000000..975b47c --- /dev/null +++ b/maubot/management/frontend/src/components/Switch.js @@ -0,0 +1,54 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" + +class Switch extends Component { + constructor(props) { + super(props) + this.state = { + active: props.active, + } + } + + componentWillReceiveProps(nextProps) { + this.setState({ + active: nextProps.active, + }) + } + + toggle = () => { + if (this.props.onToggle) { + this.props.onToggle(!this.state.active) + } else { + this.setState({ active: !this.state.active }) + } + } + + render() { + return ( +
+
+ + {this.props.onText || "On"} + {this.props.offText || "Off"} + +
+
+ ) + } +} + +export default Switch diff --git a/maubot/management/frontend/src/dashboard/client/ListEntry.js b/maubot/management/frontend/src/dashboard/client/ListEntry.js deleted file mode 100644 index e19055f..0000000 --- a/maubot/management/frontend/src/dashboard/client/ListEntry.js +++ /dev/null @@ -1,30 +0,0 @@ -// maubot - A plugin-based Matrix bot system. -// Copyright (C) 2018 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -import React from "react" -import { Link } from "react-router-dom" -import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" - -const ClientListEntry = ({ client }) => ( - - {client.id.substr(1, - {client.displayname || client.id} - - -) - -export default ClientListEntry diff --git a/maubot/management/frontend/src/index.js b/maubot/management/frontend/src/index.js index cbe10ed..12dd05c 100644 --- a/maubot/management/frontend/src/index.js +++ b/maubot/management/frontend/src/index.js @@ -16,6 +16,6 @@ import React from "react" import ReactDOM from "react-dom" import "./style/index.sass" -import App from "./MaubotRouter" +import App from "./pages/Main" ReactDOM.render(, document.getElementById("root")) diff --git a/maubot/management/frontend/src/Login.js b/maubot/management/frontend/src/pages/Login.js similarity index 97% rename from maubot/management/frontend/src/Login.js rename to maubot/management/frontend/src/pages/Login.js index e342abe..5b97f14 100644 --- a/maubot/management/frontend/src/Login.js +++ b/maubot/management/frontend/src/pages/Login.js @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" -import Spinner from "./components/Spinner" -import api from "./api" +import Spinner from "../components/Spinner" +import api from "../api" class Login extends Component { constructor(props, context) { diff --git a/maubot/management/frontend/src/MaubotRouter.js b/maubot/management/frontend/src/pages/Main.js similarity index 92% rename from maubot/management/frontend/src/MaubotRouter.js rename to maubot/management/frontend/src/pages/Main.js index b3c7561..efb9ac3 100644 --- a/maubot/management/frontend/src/MaubotRouter.js +++ b/maubot/management/frontend/src/pages/Main.js @@ -15,13 +15,13 @@ // along with this program. If not, see . import React, { Component } from "react" import { BrowserRouter as Router, Switch } from "react-router-dom" -import PrivateRoute from "./components/PrivateRoute" +import PrivateRoute from "../components/PrivateRoute" +import Spinner from "../components/Spinner" +import api from "../api" import Dashboard from "./dashboard" import Login from "./Login" -import Spinner from "./components/Spinner" -import api from "./api" -class MaubotRouter extends Component { +class Main extends Component { constructor(props) { super(props) this.state = { @@ -72,4 +72,4 @@ class MaubotRouter extends Component { } } -export default MaubotRouter +export default Main diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js new file mode 100644 index 0000000..d352b31 --- /dev/null +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -0,0 +1,114 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" +import { Link } from "react-router-dom" +import Switch from "../../components/Switch" +import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" +import { ReactComponent as UploadButton } from "../../res/upload.svg" + +function getAvatarURL(client) { + const id = client.avatar_url.substr("mxc://".length) + return `${client.homeserver}/_matrix/media/r0/download/${id}` +} + +const ClientListEntry = ({ client }) => { + const classes = ["client", "entry"] + if (!client.enabled) { + classes.push("disabled") + } else if (!client.started) { + classes.push("stopped") + } + return ( + + {client.id.substr(1, + {client.displayname || client.id} + + + ) +} + +class Client extends Component { + static ListEntry = ClientListEntry + + constructor(props) { + super(props) + this.state = props + } + + componentWillReceiveProps(nextProps) { + this.setState(nextProps) + } + + inputChange = event => { + this.setState({ [event.target.name]: event.target.value }) + } + + render() { + return
+
+ Avatar + +
+
+
+
User ID
+
+ +
+
+
+
Display name
+
+ +
+
+
+
Homeserver
+
+ +
+
+
+
Access token
+
+ +
+
+
+
Sync
+
+ this.setState({ sync })}/> +
+
+
+
Enabled
+
+ this.setState({ enabled })}/> +
+
+
+ +
+ } +} + +export default Client diff --git a/maubot/management/frontend/src/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js similarity index 92% rename from maubot/management/frontend/src/dashboard/index.js rename to maubot/management/frontend/src/pages/dashboard/index.js index 77ea69b..34dbb0c 100644 --- a/maubot/management/frontend/src/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -15,12 +15,11 @@ // along with this program. If not, see . import React, { Component } from "react" import { Route, Switch, Link } from "react-router-dom" -import api from "../api" -import { ReactComponent as Plus } from "../res/plus.svg" +import api from "../../api" +import { ReactComponent as Plus } from "../../res/plus.svg" import InstanceListEntry from "./instance/ListEntry" import InstanceView from "./instance/View" -import ClientListEntry from "./client/ListEntry" -import ClientView from "./client/View" +import Client from "./Client" import PluginListEntry from "./plugin/ListEntry" import PluginView from "./plugin/View" @@ -88,7 +87,7 @@ class Dashboard extends Component {

Clients

- {this.renderList("client", ClientListEntry)} + {this.renderList("client", Client.ListEntry)}
@@ -98,16 +97,16 @@ class Dashboard extends Component { {this.renderList("plugin", PluginListEntry)}
-
+
"Hello, World!"}/> }/> - }/> + }/> }/> this.renderView("instance", InstanceView, match.params.id)}/> - this.renderView("client", ClientView, match.params.id)}/> + this.renderView("client", Client, match.params.id)}/> this.renderView("plugin", PluginView, match.params.id)}/> "Not found :("}/> diff --git a/maubot/management/frontend/src/dashboard/instance/ListEntry.js b/maubot/management/frontend/src/pages/dashboard/instance/ListEntry.js similarity index 92% rename from maubot/management/frontend/src/dashboard/instance/ListEntry.js rename to maubot/management/frontend/src/pages/dashboard/instance/ListEntry.js index 0603e4d..9b36817 100644 --- a/maubot/management/frontend/src/dashboard/instance/ListEntry.js +++ b/maubot/management/frontend/src/pages/dashboard/instance/ListEntry.js @@ -15,7 +15,7 @@ // along with this program. If not, see . import React from "react" import { Link } from "react-router-dom" -import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" +import { ReactComponent as ChevronRight } from "../../../res/chevron-right.svg" const InstanceListEntry = ({ instance }) => ( diff --git a/maubot/management/frontend/src/dashboard/instance/View.js b/maubot/management/frontend/src/pages/dashboard/instance/View.js similarity index 100% rename from maubot/management/frontend/src/dashboard/instance/View.js rename to maubot/management/frontend/src/pages/dashboard/instance/View.js diff --git a/maubot/management/frontend/src/dashboard/plugin/ListEntry.js b/maubot/management/frontend/src/pages/dashboard/plugin/ListEntry.js similarity index 92% rename from maubot/management/frontend/src/dashboard/plugin/ListEntry.js rename to maubot/management/frontend/src/pages/dashboard/plugin/ListEntry.js index 6facdbf..d7563df 100644 --- a/maubot/management/frontend/src/dashboard/plugin/ListEntry.js +++ b/maubot/management/frontend/src/pages/dashboard/plugin/ListEntry.js @@ -15,7 +15,7 @@ // along with this program. If not, see . import React from "react" import { Link } from "react-router-dom" -import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" +import { ReactComponent as ChevronRight } from "../../../res/chevron-right.svg" const PluginListEntry = ({ plugin }) => ( diff --git a/maubot/management/frontend/src/dashboard/plugin/View.js b/maubot/management/frontend/src/pages/dashboard/plugin/View.js similarity index 100% rename from maubot/management/frontend/src/dashboard/plugin/View.js rename to maubot/management/frontend/src/pages/dashboard/plugin/View.js diff --git a/maubot/management/frontend/src/res/upload.svg b/maubot/management/frontend/src/res/upload.svg new file mode 100644 index 0000000..f1deea6 --- /dev/null +++ b/maubot/management/frontend/src/res/upload.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/maubot/management/frontend/src/style/base/body.sass b/maubot/management/frontend/src/style/base/body.sass index 462fe8c..ba0406b 100644 --- a/maubot/management/frontend/src/style/base/body.sass +++ b/maubot/management/frontend/src/style/base/body.sass @@ -18,7 +18,6 @@ body margin: 0 padding: 0 font-size: 16px - background-color: $background-color #root position: fixed @@ -33,6 +32,10 @@ body bottom: 0 left: 0 right: 0 + background-color: $background-dark + + > * + background-color: $background .maubot-loading margin-top: 10rem diff --git a/maubot/management/frontend/src/style/base/elements.sass b/maubot/management/frontend/src/style/base/elements.sass index 93d10f0..76f3a4e 100644 --- a/maubot/management/frontend/src/style/base/elements.sass +++ b/maubot/management/frontend/src/style/base/elements.sass @@ -19,7 +19,7 @@ padding: $padding width: $width height: $height - background-color: $background-color + background-color: $background border: none border-radius: .25rem color: $inverted-text-color @@ -28,7 +28,7 @@ cursor: pointer &:hover - background-color: darken($background-color, 10%) + background-color: darken($background, 10%) =link-button() display: inline-block @@ -81,7 +81,7 @@ =input($width: null, $height: null, $vertical-padding: .375rem, $horizontal-padding: 1rem, $font-size: 1rem) font-family: $font-stack border: 1px solid $border-color - background-color: $background-color + background-color: $background color: $text-color width: $width height: $height diff --git a/maubot/management/frontend/src/style/base/vars.sass b/maubot/management/frontend/src/style/base/vars.sass index 6e9a6c3..e179396 100644 --- a/maubot/management/frontend/src/style/base/vars.sass +++ b/maubot/management/frontend/src/style/base/vars.sass @@ -13,6 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . + $primary: #00C853 $primary-dark: #009624 $primary-light: #5EFC82 @@ -25,6 +26,7 @@ $error-light: #F05545 $border-color: #DDD $text-color: #212121 -$background-color: #FAFAFA -$inverted-text-color: $background-color +$background: #FAFAFA +$background-dark: #E7E7E7 +$inverted-text-color: $background $font-stack: sans-serif diff --git a/maubot/management/frontend/src/style/index.sass b/maubot/management/frontend/src/style/index.sass index c2e9a16..f6b6033 100644 --- a/maubot/management/frontend/src/style/index.sass +++ b/maubot/management/frontend/src/style/index.sass @@ -14,10 +14,10 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . @import lib/spinner - @import base/vars @import base/body @import base/elements +@import lib/switch @import pages/login @import pages/dashboard diff --git a/maubot/management/frontend/src/style/lib/switch.sass b/maubot/management/frontend/src/style/lib/switch.sass new file mode 100644 index 0000000..c087da4 --- /dev/null +++ b/maubot/management/frontend/src/style/lib/switch.sass @@ -0,0 +1,79 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +.switch + display: flex + + width: 100% + height: 2rem + + cursor: pointer + + border: 1px solid $primary + border-radius: .25rem + background-color: $background + + box-sizing: border-box + + > .box + box-sizing: border-box + width: 50% + height: 100% + + transition: .5s + text-align: center + + color: $inverted-text-color + border-radius: .15rem 0 0 .15rem + background-color: $primary + + align-items: center + + > .text + box-sizing: border-box + width: 100% + + text-align: center + vertical-align: middle + + color: $inverted-text-color + font-size: 1rem + + user-select: none + + .on + display: none + + .off + display: inline + + + &[data-active=true] + > .box + transform: translateX(100%) + + border-radius: 0 .15rem .15rem 0 + background-color: $primary + + .on + display: inline + + .off + display: none + + + + diff --git a/maubot/management/frontend/src/style/pages/client.sass b/maubot/management/frontend/src/style/pages/client.sass new file mode 100644 index 0000000..a6a6f6a --- /dev/null +++ b/maubot/management/frontend/src/style/pages/client.sass @@ -0,0 +1,109 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> .client + margin: 1rem + + div.avatar-container + position: relative + display: inline-block + width: 8rem + height: 8rem + border-radius: 100% + cursor: pointer + vertical-align: top + + > img.avatar + display: block + max-width: 8rem + max-height: 8rem + border-radius: 100% + position: absolute + left: 50% + top: 50% + -webkit-transform: translateY(-50%) translateX(-50%) + + > svg.upload + position: absolute + display: block + visibility: hidden + + width: 6rem + height: 6rem + + padding: 1rem + + &:hover + > img.avatar + opacity: .25 + + > svg.upload + visibility: visible + + div.info-container + display: inline-table + vertical-align: top + + margin: 1rem 2rem + + > .row + display: table-row + + > .key, > .value + display: table-cell + padding-bottom: .5rem + + > .key + width: 6.5rem + + > .value + margin: .5rem + + > .value > .switch + width: auto + height: 2rem + + > .value > input + border: none + height: 2rem + width: 100% + + box-sizing: border-box + + padding: .375rem 0 + background-color: $background + + font-size: 1rem + + border-bottom: 1px solid transparent + + &:hover:not(:disabled) + border-bottom: 1px solid $primary + + &:focus:not(:disabled) + border-bottom: 2px solid $primary + +//> .client + display: table + + > .field + display: table-row + width: 100% + + > .name, > .value + display: table-cell + width: 50% + text-align: center diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index dd001a0..801fc86 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -18,6 +18,9 @@ .dashboard display: grid height: 100% + max-width: 60rem + margin: auto + box-shadow: 0 .5rem .5rem rgba(0, 0, 0, 0.5) > a.title grid-area: title @@ -31,9 +34,7 @@ color: $text-color text-decoration: none - z-index: 1 - - background-color: $background-color + background-color: white border-right: 1px solid $primary border-bottom: 1px solid $border-color @@ -47,12 +48,14 @@ align-items: center justify-content: center background-color: $primary - width: 110% - margin: 0 -5% - box-shadow: 0 .25rem .25rem rgba(0, 0, 0, .25) + box-shadow: 0 .25rem .25rem rgba(0, 0, 0, .2) @import "sidebar" - > main.dashboard + > main.view grid-area: main + + @import "client" + @import "instance" + @import "plugin" diff --git a/maubot/management/frontend/src/dashboard/client/View.js b/maubot/management/frontend/src/style/pages/instance.sass similarity index 80% rename from maubot/management/frontend/src/dashboard/client/View.js rename to maubot/management/frontend/src/style/pages/instance.sass index b63d58f..7847402 100644 --- a/maubot/management/frontend/src/dashboard/client/View.js +++ b/maubot/management/frontend/src/style/pages/instance.sass @@ -13,12 +13,6 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { Component } from "react" -class ClientView extends Component { - render() { - return
{this.props.displayname}
- } -} - -export default ClientView +> .instance + margin: 1rem diff --git a/maubot/management/frontend/src/style/pages/plugin.sass b/maubot/management/frontend/src/style/pages/plugin.sass new file mode 100644 index 0000000..e1376b5 --- /dev/null +++ b/maubot/management/frontend/src/style/pages/plugin.sass @@ -0,0 +1,18 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> .plugin + margin: 1rem diff --git a/maubot/management/frontend/src/style/pages/sidebar.sass b/maubot/management/frontend/src/style/pages/sidebar.sass index 47bbfd7..cb48b12 100644 --- a/maubot/management/frontend/src/style/pages/sidebar.sass +++ b/maubot/management/frontend/src/style/pages/sidebar.sass @@ -16,13 +16,16 @@ > .sidebar grid-area: sidebar - background-color: $background-color + background-color: white border-right: 1px solid $border-color padding: .5rem + overflow-y: auto + div.list - margin-bottom: 1.5rem + &:not(:last-of-type) + margin-bottom: 1.5rem div.title h2 From ef3f4a20f29bb1f9e3b9408980257fbf2e26c295 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 9 Nov 2018 20:03:08 +0200 Subject: [PATCH 040/101] Add endpoint to upload avatars --- maubot/management/api/client.py | 13 +++++++ maubot/management/api/spec.yaml | 69 ++++++++++++++++++++++++++------- 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/maubot/management/api/client.py b/maubot/management/api/client.py index 872c965..051f9be 100644 --- a/maubot/management/api/client.py +++ b/maubot/management/api/client.py @@ -127,3 +127,16 @@ async def delete_client(request: web.Request) -> web.Response: await client.stop() client.delete() return resp.deleted + + +@routes.post("/client/{id}/avatar") +async def upload_avatar(request: web.Request) -> web.Response: + user_id = request.match_info.get("id", None) + client = Client.get(user_id, None) + if not client: + return resp.client_not_found + content = await request.read() + return web.json_response({ + "content_uri": await client.client.upload_media( + content, request.headers.get("Content-Type", None)), + }) diff --git a/maubot/management/api/spec.yaml b/maubot/management/api/spec.yaml index efe759e..af93a65 100644 --- a/maubot/management/api/spec.yaml +++ b/maubot/management/api/spec.yaml @@ -93,6 +93,13 @@ paths: schema: type: boolean default: false + requestBody: + content: + application/zip: + schema: + type: string + format: binary + example: The plugin maubot archive (.mbp) responses: 200: description: Plugin uploaded and replaced current version successfully @@ -111,14 +118,7 @@ paths: 401: $ref: '#/components/responses/Unauthorized' 409: - description: Plugin - requestBody: - content: - application/zip: - schema: - type: string - format: binary - example: The plugin maubot archive (.mbp) + description: Plugin already exists and allow_override was not specified. '/plugin/{id}': parameters: - name: id @@ -169,6 +169,13 @@ paths: doesn't match the ID in the path. If the plugin already exists, enabled instances will be restarted. tags: [Plugins] + requestBody: + content: + application/zip: + schema: + type: string + format: binary + example: The plugin maubot archive (.mbp) responses: 200: description: Plugin uploaded and replaced current version successfully @@ -186,13 +193,6 @@ paths: $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/Unauthorized' - requestBody: - content: - application/zip: - schema: - type: string - format: binary - example: The plugin maubot archive (.mbp) /plugin/{id}/reload: parameters: - name: id @@ -399,6 +399,45 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + '/client/{id}/avatar': + parameters: + - name: id + in: path + description: The Matrix user ID of the client to get + required: true + schema: + type: string + post: + operationId: upload_avatar + summary: Upload a profile picture for a bot + tags: [Clients] + requestBody: + content: + image/png: + schema: + type: string + format: binary + example: The avatar to upload + image/jpeg: + schema: + type: string + format: binary + example: The avatar to upload + responses: + 200: + description: The avatar was uploaded successfully + content: + application/json: + schema: + type: object + properties: + content_uri: + type: string + description: The MXC URI of the uploaded avatar + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' components: responses: From 29adf50ae0f308093f32820d9251d7e879faf7fe Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 9 Nov 2018 20:03:26 +0200 Subject: [PATCH 041/101] Finish initial client main view --- maubot/management/frontend/src/api.js | 24 ++- .../src/components/PreferenceTable.js | 55 ++++++ .../frontend/src/pages/dashboard/Client.js | 161 +++++++++++++----- .../frontend/src/pages/dashboard/index.js | 16 +- .../frontend/src/style/base/elements.sass | 10 +- .../management/frontend/src/style/index.sass | 3 + .../src/style/lib/preferencetable.sass | 57 +++++++ .../frontend/src/style/pages/client.sass | 143 ++++++++++------ .../frontend/src/style/pages/dashboard.sass | 12 +- 9 files changed, 370 insertions(+), 111 deletions(-) create mode 100644 maubot/management/frontend/src/components/PreferenceTable.js create mode 100644 maubot/management/frontend/src/style/lib/preferencetable.sass diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index d0acbb6..8721685 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -72,7 +72,7 @@ export async function uploadPlugin(data, id) { let resp if (id) { resp = await fetch(`${BASE_PATH}/plugin/${id}`, { - headers: getHeaders("applcation/zip"), + headers: getHeaders("application/zip"), body: data, method: "PUT", }) @@ -96,9 +96,27 @@ export async function getClient(id) { return await resp.json() } +export async function uploadAvatar(id, data, mime) { + const resp = await fetch(`${BASE_PATH}/client/${id}/avatar`, { + headers: getHeaders(mime), + body: data, + method: "POST", + }) + return await resp.json() +} + +export async function putClient(client) { + const resp = await fetch(`${BASE_PATH}/client/${client.id}`, { + headers: getHeaders(), + body: JSON.stringify(client), + method: "PUT", + }) + return await resp.json() +} + export default { login, ping, getInstances, getInstance, - getPlugins, getPlugin, uploadPlugin, - getClients, getClient, + getPlugins, getPlugin, uploadPlugin, + getClients, getClient, uploadAvatar, putClient, } diff --git a/maubot/management/frontend/src/components/PreferenceTable.js b/maubot/management/frontend/src/components/PreferenceTable.js new file mode 100644 index 0000000..28ddfab --- /dev/null +++ b/maubot/management/frontend/src/components/PreferenceTable.js @@ -0,0 +1,55 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" +import Switch from "./Switch" + +export const PrefTable = ({ children, wrapperClass }) => { + if (wrapperClass) { + return ( +
+
+ {children} +
+
+ ) + } + return ( +
+ {children} +
+ ) +} + +export const PrefRow = ({ name, children }) => ( +
+
{name}
+
{children}
+
+) + +export const PrefInput = ({ rowName, ...args }) => ( + + + +) + +export const PrefSwitch = ({ rowName, ...args }) => ( + + + +) + +export default PrefTable diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index d352b31..2019444 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -15,11 +15,16 @@ // along with this program. If not, see . import React, { Component } from "react" import { Link } from "react-router-dom" -import Switch from "../../components/Switch" import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" import { ReactComponent as UploadButton } from "../../res/upload.svg" +import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTable" +import Spinner from "../../components/Spinner" +import api from "../../api" function getAvatarURL(client) { + if (!client.avatar_url) { + return "" + } const id = client.avatar_url.substr("mxc://".length) return `${client.homeserver}/_matrix/media/r0/download/${id}` } @@ -45,68 +50,136 @@ class Client extends Component { constructor(props) { super(props) - this.state = props + this.state = Object.assign(this.initialState, props.client) + } + + get initialState() { + return { + id: "", + displayname: "", + homeserver: "", + avatar_url: "", + access_token: "", + sync: true, + autojoin: true, + enabled: true, + started: false, + + uploadingAvatar: false, + saving: false, + startingOrStopping: false, + } } componentWillReceiveProps(nextProps) { - this.setState(nextProps) + this.setState(Object.assign(this.initialState, nextProps.client)) } inputChange = event => { + if (!event.target.name) { + return + } this.setState({ [event.target.name]: event.target.value }) } + async readFile(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.readAsArrayBuffer(file) + reader.onload = evt => resolve(evt.target.result) + reader.onerror = err => reject(err) + }) + } + + avatarUpload = async event => { + const file = event.target.files[0] + this.setState({ + uploadingAvatar: true, + }) + const data = await this.readFile(file) + const resp = await api.uploadAvatar(this.state.id, data, file.type) + this.setState({ + uploadingAvatar: false, + avatar_url: resp.content_uri, + }) + } + + save = async () => { + this.setState({ saving: true }) + const resp = await api.putClient(this.state) + if (resp.id) { + resp.saving = false + this.setState(resp) + } else { + console.error(resp) + } + } + + startOrStop = async () => { + this.setState({ startingOrStopping: true }) + const resp = await api.putClient({ + id: this.state.id, + started: !this.state.started, + }) + if (resp.id) { + resp.startingOrStopping = false + this.setState(resp) + } else { + console.error(resp) + } + } + render() { return
-
- Avatar - +
+
+ Avatar + + + {this.state.uploadingAvatar && } +
+ {this.props.client && (<> +
+ + {this.state.started ? "Started" : "Stopped"} +
+ + )}
-
-
User ID
-
- + -
-
-
-
Display name
-
- -
-
-
-
Homeserver
-
- -
-
-
-
Access token
-
- -
-
-
-
Sync
-
- + this.setState({ sync })}/> -
-
-
-
Enabled
-
- this.setState({ autojoin })}/> + this.setState({ enabled })}/> -
-
-
+ + +
} } diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index 34dbb0c..5d9a38d 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -58,11 +58,19 @@ class Dashboard extends Component { } renderView(field, type, id) { - const entry = this.state[field + "s"][id] + const stateField = field + "s" + const entry = this.state[stateField][id] if (!entry) { return "Not found :(" } - return React.createElement(type, entry) + return React.createElement(type, { + [field]: entry, + onChange: newEntry => this.setState({ + [stateField]: Object.assign({}, this.state[stateField], { + [id]: newEntry, + }), + }), + }) } render() { @@ -72,7 +80,9 @@ class Dashboard extends Component { Maubot Manager
- {localStorage.username} +
+ {localStorage.username} +
"Hello, World!"}/> - }/> + this.add("instances", newEntry)}/>}/> this.add("clients", newEntry)}/>}/> - }/> + this.add("plugins", newEntry)}/>}/> - this.renderView("instance", InstanceView, match.params.id)}/> + this.renderView("instance", Instance, match.params.id)}/> this.renderView("client", Client, match.params.id)}/> - this.renderView("plugin", PluginView, match.params.id)}/> + this.renderView("plugin", Plugin, match.params.id)}/> "Not found :("}/>
diff --git a/maubot/management/frontend/src/style/lib/switch.sass b/maubot/management/frontend/src/style/lib/switch.sass index c087da4..570320c 100644 --- a/maubot/management/frontend/src/style/lib/switch.sass +++ b/maubot/management/frontend/src/style/lib/switch.sass @@ -29,6 +29,7 @@ box-sizing: border-box > .box + display: flex box-sizing: border-box width: 50% height: 100% diff --git a/maubot/management/frontend/src/style/pages/client.sass b/maubot/management/frontend/src/style/pages/client.sass deleted file mode 100644 index e0ffb66..0000000 --- a/maubot/management/frontend/src/style/pages/client.sass +++ /dev/null @@ -1,145 +0,0 @@ -// maubot - A plugin-based Matrix bot system. -// Copyright (C) 2018 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -> .client - margin: 1rem - display: flex - - > div.sidebar - vertical-align: top - text-align: center - width: 8rem - - > div - margin-bottom: 1rem - - > div.avatar-container - position: relative - width: 8rem - height: 8rem - border-radius: 50% - overflow: hidden - - display: flex - align-items: center - justify-content: center - - > img.avatar - position: absolute - display: block - max-width: 8rem - max-height: 8rem - user-select: none - - > svg.upload - position: absolute - display: block - visibility: hidden - - width: 6rem - height: 6rem - - padding: 1rem - user-select: none - - > input.file-selector - position: absolute - width: 8rem - height: 8rem - user-select: none - opacity: 0 - - > div.spinner - +thick-spinner - - &:not(.uploading) - > input.file-selector - cursor: pointer - - &:hover, &.drag - > img.avatar - opacity: .25 - - > svg.upload - visibility: visible - - &.no-avatar - > img.avatar - visibility: hidden - - > svg.upload - visibility: visible - opacity: .5 - - &.uploading - > img.avatar - opacity: .25 - - > div.started-container - display: inline-flex - - > span.started - display: inline-block - height: 0 - width: 0 - border-radius: 50% - margin: .5rem - - &.true - background-color: $primary - box-shadow: 0 0 .75rem .75rem $primary - - &.false - background-color: $error-light - box-shadow: 0 0 .75rem .75rem $error-light - - &.disabled - background-color: $border-color - box-shadow: 0 0 .75rem .75rem $border-color - - > span.text - display: inline-block - margin-left: 1rem - - > div.info-container - vertical-align: top - - margin: 0 1rem - flex: 1 - - > .buttons - display: flex - - +button-group - - > .error - margin-top: 1rem - +notification($error) - - &:empty - display: none - - button.save, button.delete - +button - +main-color-button - width: 100% - height: 2.5rem - padding: 0 - - > .spinner - +thick-spinner - +white-spinner - width: 2rem diff --git a/maubot/management/frontend/src/style/pages/client/avatar.sass b/maubot/management/frontend/src/style/pages/client/avatar.sass new file mode 100644 index 0000000..9174ba8 --- /dev/null +++ b/maubot/management/frontend/src/style/pages/client/avatar.sass @@ -0,0 +1,77 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> div.avatar-container + position: relative + width: 8rem + height: 8rem + border-radius: 50% + overflow: hidden + + display: flex + align-items: center + justify-content: center + + > img.avatar + position: absolute + display: block + max-width: 8rem + max-height: 8rem + user-select: none + + > svg.upload + position: absolute + display: block + visibility: hidden + + width: 6rem + height: 6rem + + padding: 1rem + user-select: none + + > input.file-selector + position: absolute + width: 8rem + height: 8rem + user-select: none + opacity: 0 + + > div.spinner + +thick-spinner + + &:not(.uploading) + > input.file-selector + cursor: pointer + + &:hover, &.drag + > img.avatar + opacity: .25 + + > svg.upload + visibility: visible + + &.no-avatar + > img.avatar + visibility: hidden + + > svg.upload + visibility: visible + opacity: .5 + + &.uploading + > img.avatar + opacity: .25 diff --git a/maubot/management/frontend/src/style/pages/client/index.sass b/maubot/management/frontend/src/style/pages/client/index.sass new file mode 100644 index 0000000..f34e43a --- /dev/null +++ b/maubot/management/frontend/src/style/pages/client/index.sass @@ -0,0 +1,61 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +> div.client + display: flex + margin: 2rem + + + > div.sidebar + vertical-align: top + text-align: center + width: 8rem + + > div + margin-bottom: 1rem + + @import avatar + @import started + + > div.info + vertical-align: top + margin-left: 1rem + flex: 1 + + @import instances + + > .buttons + display: flex + +button-group + + > .error + margin-top: 1rem + +notification($error) + + &:empty + display: none + + button.save, button.delete + +button + +main-color-button + width: 100% + height: 2.5rem + padding: 0 + + > .spinner + +thick-spinner + +white-spinner + width: 2rem diff --git a/maubot/management/frontend/src/pages/dashboard/instance/View.js b/maubot/management/frontend/src/style/pages/client/instances.sass similarity index 62% rename from maubot/management/frontend/src/pages/dashboard/instance/View.js rename to maubot/management/frontend/src/style/pages/client/instances.sass index 9a82cba..7d5fb3c 100644 --- a/maubot/management/frontend/src/pages/dashboard/instance/View.js +++ b/maubot/management/frontend/src/style/pages/client/instances.sass @@ -13,12 +13,24 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { Component } from "react" -class InstanceView extends Component { - render() { - return
{this.props.id}
- } -} +> div.instances + margin-top: 1rem -export default InstanceView + > h3 + margin-bottom: .5rem + + > a.instance + display: block + width: 100% + padding: .375rem .5rem + background-color: white + border-radius: .25rem + color: $text-color + text-decoration: none + box-sizing: border-box + border: 1px solid $primary + + &:hover + color: $inverted-text-color + background-color: $primary diff --git a/maubot/management/frontend/src/pages/dashboard/plugin/View.js b/maubot/management/frontend/src/style/pages/client/started.sass similarity index 55% rename from maubot/management/frontend/src/pages/dashboard/plugin/View.js rename to maubot/management/frontend/src/style/pages/client/started.sass index fbcf2c3..0f63095 100644 --- a/maubot/management/frontend/src/pages/dashboard/plugin/View.js +++ b/maubot/management/frontend/src/style/pages/client/started.sass @@ -13,12 +13,29 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { Component } from "react" -class PluginView extends Component { - render() { - return
{this.props.id}
- } -} +> div.started-container + display: inline-flex -export default PluginView + > span.started + display: inline-block + height: 0 + width: 0 + border-radius: 50% + margin: .5rem + + &.true + background-color: $primary + box-shadow: 0 0 .75rem .75rem $primary + + &.false + background-color: $error-light + box-shadow: 0 0 .75rem .75rem $error-light + + &.disabled + background-color: $border-color + box-shadow: 0 0 .75rem .75rem $border-color + + > span.text + display: inline-block + margin-left: 1rem diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 184c4db..8ec9563 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -61,11 +61,11 @@ border-radius: .25rem - @import "sidebar" + @import sidebar > main.view grid-area: main - @import "client" - @import "instance" - @import "plugin" + @import client/index + @import instance + @import plugin diff --git a/maubot/management/frontend/src/style/pages/sidebar.sass b/maubot/management/frontend/src/style/pages/sidebar.sass index cb48b12..8015e1a 100644 --- a/maubot/management/frontend/src/style/pages/sidebar.sass +++ b/maubot/management/frontend/src/style/pages/sidebar.sass @@ -42,6 +42,10 @@ display: block color: $text-color text-decoration: none + padding: .25rem + border-radius: .25rem + height: 2rem + box-sizing: border-box &:not(:hover) > svg display: none @@ -49,16 +53,18 @@ > svg float: right - padding: .25rem + &:hover + background-color: $primary-light + + &.active + background-color: $primary &.client - padding: .25rem - img.avatar max-height: 1.5rem border-radius: 100% vertical-align: middle - span.displayname - margin-left: .25rem - vertical-align: middle + span.displayname, span.id + margin-left: .25rem + vertical-align: middle diff --git a/maubot/management/frontend/yarn.lock b/maubot/management/frontend/yarn.lock index 9b58f83..47ad628 100644 --- a/maubot/management/frontend/yarn.lock +++ b/maubot/management/frontend/yarn.lock @@ -824,6 +824,58 @@ resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== +"@sentry/core@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-4.3.0.tgz#f51f86b380637b5f2348cd35fdb96c224023c103" + integrity sha512-VEMRshyF2J2IJkGTF9fnAd2/a/wR+42qrKwCLVeNxHzZm53VAnSfJd8ZA5jnN5RiydQVGDOylqvdfij/LmmsuQ== + dependencies: + "@sentry/hub" "4.3.0" + "@sentry/minimal" "4.3.0" + "@sentry/types" "4.3.0" + "@sentry/utils" "4.3.0" + +"@sentry/hub@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-4.3.0.tgz#ed7731583d81664057de73e3221a393116362b04" + integrity sha512-9nh4Hcx2tZQVHr5JGy1XZd1RgwC2C+1tNFXu06rcxYB20nhKOKtOAH9STrv64vpnK+70AC4miRdlCgqHOKLdxA== + dependencies: + "@sentry/types" "4.3.0" + "@sentry/utils" "4.3.0" + +"@sentry/minimal@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-4.3.0.tgz#d83826e713d54c95d4986187d223ef1fabc4e1ed" + integrity sha512-yRN+L3O9mlcjqpWiivqJsm3Cy/YNh0ejQKpxxxPD186tl5nzEkAqRq5jCYelYIAdH1eBvg87H5VNNFr/shHxaQ== + dependencies: + "@sentry/hub" "4.3.0" + "@sentry/types" "4.3.0" + +"@sentry/node@^4.0.0-beta.12": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-4.3.0.tgz#5a966d6d791d19a064eeede2cb265cf057414a3f" + integrity sha512-2TZHi0XYpSVzKGT/M7mMlXidya4+ZmSZxZCMJ8KlWPljuQxzPoUeGwmYGXOHEsktOGG2eGajDv1hjGtHfamlnA== + dependencies: + "@sentry/core" "4.3.0" + "@sentry/hub" "4.3.0" + "@sentry/types" "4.3.0" + "@sentry/utils" "4.3.0" + cookie "0.3.1" + lsmod "1.0.0" + md5 "2.2.1" + stack-trace "0.0.10" + +"@sentry/types@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-4.3.0.tgz#a5cec425764a72a0817329ac212c5ce085ecdd05" + integrity sha512-oBAusA+/JFJJlNfHgikyBPTyvmCr/YxZsLWL2FNOBNJkMClW+m7tQ63x/ZAqRSEd8lnJQxJSU7LfAYDH6LcChQ== + +"@sentry/utils@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-4.3.0.tgz#4d278b55deb6175ada2a78b56716aa2df97cd389" + integrity sha512-cEGygJbyc1SUGxbanvk3+JYadiywJupdEkp6L8rRfxur/nNQZq1Jd7tkdm4I2rz5YFVg+yayadAquvr4DIu9nA== + dependencies: + "@sentry/types" "4.3.0" + "@svgr/core@^2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@svgr/core/-/core-2.4.1.tgz#03a407c28c4a1d84305ae95021e8eabfda8fa731" @@ -1044,6 +1096,13 @@ acorn-globals@^4.1.0, acorn-globals@^4.3.0: acorn "^6.0.1" acorn-walk "^6.0.1" +acorn-jsx@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" + integrity sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s= + dependencies: + acorn "^3.0.4" + acorn-jsx@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-4.1.1.tgz#e8e41e48ea2fe0c896740610ab6a4ffd8add225e" @@ -1056,7 +1115,12 @@ acorn-walk@^6.0.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.0.tgz#c957f4a1460da46af4a0388ce28b4c99355b0cbc" integrity sha512-ugTb7Lq7u4GfWSqqpwE0bGyoBZNMTok/zDBXxfEG0QM50jNlGhIWjRC1pPN7bvV1anhF+bs+/gNcRw+o55Evbg== -acorn@^5.0.0, acorn@^5.0.3, acorn@^5.5.3, acorn@^5.6.0, acorn@^5.6.2: +acorn@^3.0.4: + version "3.3.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" + integrity sha1-ReN/s56No/JbruP/U2niu18iAXo= + +acorn@^5.0.0, acorn@^5.0.3, acorn@^5.5.0, acorn@^5.5.3, acorn@^5.6.0, acorn@^5.6.2: version "5.7.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== @@ -1076,11 +1140,24 @@ ajv-errors@^1.0.0: resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.0.tgz#ecf021fa108fd17dfb5e6b383f2dd233e31ffc59" integrity sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk= +ajv-keywords@^1.0.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" + integrity sha1-MU3QpLM2j609/NxU7eYXG4htrzw= + ajv-keywords@^3.0.0, ajv-keywords@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo= +ajv@^4.7.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + integrity sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY= + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + ajv@^5.3.0: version "5.5.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" @@ -1116,6 +1193,11 @@ ansi-colors@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.1.0.tgz#dcfaacc90ef9187de413ec3ef8d5eb981a98808f" integrity sha512-hTv1qPdi+sVEk3jYsdjox5nQI0C9HTbjKShbCdYLKb1LOfNbb7wsF4d7OEKIZoxIHx02tSp3m94jcPW2EfMjmA== +ansi-escapes@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + integrity sha1-06ioOzGapneTZisT52HHkRQiMG4= + ansi-escapes@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" @@ -2082,7 +2164,7 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@2.4.1, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1: +chalk@2.4.1, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.2, chalk@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== @@ -2091,7 +2173,7 @@ chalk@2.4.1, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^1.1.1, chalk@^1.1.3: +chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= @@ -2107,6 +2189,11 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + check-types@^7.3.0: version "7.4.0" resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.4.0.tgz#0378ec1b9616ec71f774931a3c6516fad8c152f4" @@ -2179,6 +2266,13 @@ clean-css@4.2.x: dependencies: source-map "~0.6.0" +cli-cursor@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc= + dependencies: + restore-cursor "^1.0.1" + cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" @@ -2312,7 +2406,7 @@ commander@2.17.x, commander@~2.17.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== -commander@^2.11.0: +commander@^2.11.0, commander@^2.15.1, commander@^2.8.1: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -2362,7 +2456,7 @@ 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: +concat-stream@^1.4.6, 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== @@ -2536,6 +2630,11 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -2763,6 +2862,13 @@ cyclist@~0.2.2: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + integrity sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8= + dependencies: + es5-ext "^0.10.9" + damerau-levenshtein@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514" @@ -2789,7 +2895,7 @@ date-now@^0.1.4: resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= -debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9: +debug@2.6.9, debug@^2.1.1, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -3003,7 +3109,7 @@ dns-txt@^2.0.2: dependencies: buffer-indexof "^1.0.0" -doctrine@1.5.0: +doctrine@1.5.0, doctrine@^1.2.2: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= @@ -3218,6 +3324,65 @@ es-to-primitive@^1.1.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: + version "0.10.46" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.46.tgz#efd99f67c5a7ec789baa3daa7f79870388f7f572" + integrity sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw== + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.1" + next-tick "1" + +es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-map@^0.1.3: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" + integrity sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA= + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-set "~0.1.5" + es6-symbol "~3.1.1" + event-emitter "~0.3.5" + +es6-set@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" + integrity sha1-0rPsXU2ADO2BjbU40ol02wpzzLE= + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-symbol "3.1.1" + event-emitter "~0.3.5" + +es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc= + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-weak-map@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" + integrity sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8= + dependencies: + d "1" + es5-ext "^0.10.14" + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -3240,6 +3405,16 @@ escodegen@^1.11.0, escodegen@^1.9.1: optionalDependencies: source-map "~0.6.1" +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + integrity sha1-4Bl16BJ4GhY6ba392AOY3GTIicM= + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + eslint-config-react-app@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-3.0.4.tgz#83f394d765e7d5af623d773e64609e9c9f2cbeb5" @@ -3392,6 +3567,53 @@ eslint@5.6.0: table "^4.0.3" text-table "^0.2.0" +eslint@^2.7.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-2.13.1.tgz#e4cc8fa0f009fb829aaae23855a29360be1f6c11" + integrity sha1-5MyPoPAJ+4KaquI4VaKTYL4fbBE= + dependencies: + chalk "^1.1.3" + concat-stream "^1.4.6" + debug "^2.1.1" + doctrine "^1.2.2" + es6-map "^0.1.3" + escope "^3.6.0" + espree "^3.1.6" + estraverse "^4.2.0" + esutils "^2.0.2" + file-entry-cache "^1.1.1" + glob "^7.0.3" + globals "^9.2.0" + ignore "^3.1.2" + imurmurhash "^0.1.4" + inquirer "^0.12.0" + is-my-json-valid "^2.10.0" + is-resolvable "^1.0.0" + js-yaml "^3.5.1" + json-stable-stringify "^1.0.0" + levn "^0.3.0" + lodash "^4.0.0" + mkdirp "^0.5.0" + optionator "^0.8.1" + path-is-absolute "^1.0.0" + path-is-inside "^1.0.1" + pluralize "^1.2.1" + progress "^1.1.8" + require-uncached "^1.0.2" + shelljs "^0.6.0" + strip-json-comments "~1.0.1" + table "^3.7.8" + text-table "~0.2.0" + user-home "^2.0.0" + +espree@^3.1.6: + version "3.5.4" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" + integrity sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A== + dependencies: + acorn "^5.5.0" + acorn-jsx "^3.0.0" + espree@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/espree/-/espree-4.0.0.tgz#253998f20a0f82db5d866385799d912a83a36634" @@ -3439,6 +3661,14 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +event-emitter@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= + dependencies: + d "1" + es5-ext "~0.10.14" + eventemitter3@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" @@ -3497,6 +3727,11 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g= + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -3695,6 +3930,14 @@ figgy-pudding@^3.1.0, figgy-pudding@^3.5.1: resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -3702,6 +3945,14 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" +file-entry-cache@^1.1.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-1.3.1.tgz#44c61ea607ae4be9c1402f41f44270cbfe334ff8" + integrity sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g= + dependencies: + flat-cache "^1.2.1" + object-assign "^4.0.1" + file-entry-cache@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" @@ -3912,6 +4163,13 @@ from2@^2.1.0: inherits "^2.0.1" readable-stream "^2.0.0" +front-matter@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/front-matter/-/front-matter-2.1.2.tgz#f75983b9f2f413be658c93dfd7bd8ce4078f5cdb" + integrity sha1-91mDufL0E75ljJPf172M5AePXNs= + dependencies: + js-yaml "^3.4.6" + fs-extra@7.0.0, fs-extra@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.0.tgz#8cc3f47ce07ef7b3593a11b9fb245f7e34c041d6" @@ -3921,6 +4179,15 @@ fs-extra@7.0.0, fs-extra@^7.0.0: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291" + integrity sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE= + dependencies: + graceful-fs "^4.1.2" + jsonfile "^3.0.0" + universalify "^0.1.0" + fs-extra@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" @@ -4001,6 +4268,20 @@ gaze@^1.0.0: dependencies: globule "^1.0.0" +generate-function@^2.0.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" + integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ== + dependencies: + is-property "^1.0.2" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + integrity sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA= + dependencies: + is-property "^1.0.0" + get-caller-file@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" @@ -4093,7 +4374,7 @@ globals@^11.1.0, globals@^11.7.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.8.0.tgz#c1ef45ee9bed6badf0663c5cb90e8d1adec1321d" integrity sha512-io6LkyPVuzCHBSQV9fmOwxZkUk6nIaGmxheLDgmuFv89j0fm2aqDbIXKAGfzCMHqz3HLF2Zf8WSG6VqMh2qFmA== -globals@^9.18.0: +globals@^9.18.0, globals@^9.2.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== @@ -4130,6 +4411,19 @@ globule@^1.0.0: lodash "~4.17.10" minimatch "~3.0.2" +gonzales-pe-sl@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/gonzales-pe-sl/-/gonzales-pe-sl-4.2.3.tgz#6a868bc380645f141feeb042c6f97fcc71b59fe6" + integrity sha1-aoaLw4BkXxQf7rBCxvl/zHG1n+Y= + dependencies: + minimist "1.1.x" + +"gonzales-pe-sl@github:srowhani/gonzales-pe#dev": + version "4.2.3" + resolved "https://codeload.github.com/srowhani/gonzales-pe/tar.gz/3b052416074edc280f7d04bbe40b2e410693c4a3" + dependencies: + minimist "1.1.x" + graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -4547,6 +4841,11 @@ ignore-walk@^3.0.1: dependencies: minimatch "^3.0.4" +ignore@^3.1.2: + version "3.3.10" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" + integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== + ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -4651,6 +4950,25 @@ inquirer@6.2.0, inquirer@^6.1.0: strip-ansi "^4.0.0" through "^2.3.6" +inquirer@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" + integrity sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34= + dependencies: + ansi-escapes "^1.1.0" + ansi-regex "^2.0.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + figures "^1.3.5" + lodash "^4.3.0" + readline2 "^1.0.1" + run-async "^0.1.0" + rx-lite "^3.1.2" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + internal-ip@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-3.0.1.tgz#df5c99876e1d2eb2ea2d74f520e3f669a00ece27" @@ -4732,7 +5050,7 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" -is-buffer@^1.0.2, is-buffer@^1.1.5: +is-buffer@^1.0.2, is-buffer@^1.1.5, is-buffer@~1.1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -4889,6 +5207,22 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" +is-my-ip-valid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" + integrity sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ== + +is-my-json-valid@^2.10.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz#8fd6e40363cd06b963fa877d444bfb5eddc62175" + integrity sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q== + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + is-my-ip-valid "^1.0.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + is-number@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" @@ -4954,6 +5288,11 @@ is-promise@^2.1.0: resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= +is-property@^1.0.0, is-property@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= + is-regex@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" @@ -5475,7 +5814,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0: +js-yaml@^3.11.0, js-yaml@^3.12.0, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0, js-yaml@^3.9.0: version "3.12.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== @@ -5591,7 +5930,7 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= -json-stable-stringify@^1.0.1: +json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8= @@ -5613,6 +5952,13 @@ json5@^0.5.0, json5@^0.5.1: resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= +jsonfile@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66" + integrity sha1-pezG9l9T9mLEQVx2daAzHQmS7GY= + optionalDependencies: + graceful-fs "^4.1.6" + jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -5625,6 +5971,11 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= +jsonpointer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" + integrity sha1-T9kss04OnbPInIYi7PUfm5eMbLk= + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -5683,6 +6034,11 @@ kleur@^2.0.1: resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300" integrity sha512-77XF9iTllATmG9lSlIv0qdQ2BQ/h9t0bJllHlbvsQ0zUWfU7Yi0S8L5JXzPZgkefIiajLmBJJ4BsMJmqcf7oxQ== +known-css-properties@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.3.0.tgz#a3d135bbfc60ee8c6eacf2f7e7e6f2d4755e49a4" + integrity sha512-QMQcnKAiQccfQTqtBh/qwquGZ2XK/DXND1jrcN9M8gMMy99Gwla7GQjndVUsEqIaRyP6bsFRuhwRj5poafBGJQ== + last-call-webpack-plugin@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555" @@ -5807,6 +6163,11 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= +lodash.capitalize@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9" + integrity sha1-+CbJtOKoUR2E46yinbBeGk87cqk= + lodash.clonedeep@^4.3.2: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -5817,6 +6178,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.kebabcase@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" + integrity sha1-hImxyw0p/4gZXM7KRI/21swpXDY= + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -5857,7 +6223,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@~4.17.10: +"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@~4.17.10: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== @@ -5895,6 +6261,11 @@ lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.3: pseudomap "^1.0.2" yallist "^2.1.2" +lsmod@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lsmod/-/lsmod-1.0.0.tgz#9a00f76dca36eb23fa05350afe1b585d4299e64b" + integrity sha1-mgD3bco26yP6BTUK/htYXUKZ5ks= + make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -5952,6 +6323,15 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +md5@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + mdn-data@~1.1.0: version "1.1.4" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01" @@ -6137,6 +6517,11 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= +minimist@1.1.x: + version "1.1.3" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" + integrity sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag= + minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -6252,6 +6637,11 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" +mute-stream@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" + integrity sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA= + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -6303,6 +6693,11 @@ neo-async@^2.5.0: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.2.tgz#489105ce7bc54e709d736b195f82135048c50fcc" integrity sha512-vdqTKI9GBIYcAEbFAcpKPErKINfPF5zIuz3/niBfq8WUZjpT2tytLlFVrBgWdOtqI4uaA/Rb6No0hux39XXDuw== +next-tick@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" + integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -6627,6 +7022,11 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k= + onetime@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" @@ -7000,6 +7400,11 @@ pkg-up@2.0.0: dependencies: find-up "^2.1.0" +pluralize@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" + integrity sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU= + pluralize@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" @@ -7696,6 +8101,11 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= +progress@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74= + progress@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" @@ -8094,6 +8504,15 @@ readdirp@^2.0.0: micromatch "^3.1.10" readable-stream "^2.0.2" +readline2@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" + integrity sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + mute-stream "0.0.5" + realpath-native@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.2.tgz#cd51ce089b513b45cf9b1516c82989b51ccc6560" @@ -8313,7 +8732,7 @@ require-main-filename@^1.0.1: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= -require-uncached@^1.0.3: +require-uncached@^1.0.2, require-uncached@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" integrity sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM= @@ -8373,6 +8792,14 @@ resolve@1.8.1, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.6.0, resolve@^1.8.1: dependencies: path-parse "^1.0.5" +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE= + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -8416,6 +8843,13 @@ rsvp@^3.3.3: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw== +run-async@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" + integrity sha1-yK1KXhEGYeQCp9IbUw4AnyX444k= + dependencies: + once "^1.3.0" + run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" @@ -8430,6 +8864,11 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" +rx-lite@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" + integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI= + rxjs@^6.1.0: version "6.3.3" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz#3c6a7fa420e844a81390fb1158a9ec614f4bad55" @@ -8480,6 +8919,39 @@ sass-graph@^2.2.4: scss-tokenizer "^0.2.3" yargs "^7.0.0" +sass-lint-auto-fix@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/sass-lint-auto-fix/-/sass-lint-auto-fix-0.15.0.tgz#503d1b73aaf5e3e033d4fe3b80f6c7ad80bb7a46" + integrity sha512-Q1WFpm9Ro1S3xPEavqWLIMx8KRTmkoHZh83+aBoqemiMtRB1KwD7VtLz4jreiZP6fZfSXee1Ln3BjHpKaC6hbA== + dependencies: + "@sentry/node" "^4.0.0-beta.12" + chalk "^2.3.2" + commander "^2.15.1" + glob "^7.1.2" + gonzales-pe-sl srowhani/gonzales-pe#dev + js-yaml "^3.11.0" + sass-lint "^1.12.1" + +sass-lint@^1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/sass-lint/-/sass-lint-1.12.1.tgz#630f69c216aa206b8232fb2aa907bdf3336b6d83" + integrity sha1-Yw9pwhaqIGuCMvsqqQe98zNrbYM= + dependencies: + commander "^2.8.1" + eslint "^2.7.0" + front-matter "2.1.2" + fs-extra "^3.0.1" + glob "^7.0.0" + globule "^1.0.0" + gonzales-pe-sl "^4.2.3" + js-yaml "^3.5.4" + known-css-properties "^0.3.0" + lodash.capitalize "^4.1.0" + lodash.kebabcase "^4.0.0" + merge "^1.2.0" + path-is-absolute "^1.0.0" + util "^0.10.3" + sass-loader@7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.1.0.tgz#16fd5138cb8b424bf8a759528a1972d72aad069d" @@ -8690,6 +9162,11 @@ shell-quote@1.6.1: array-reduce "~0.0.0" jsonify "~0.0.0" +shelljs@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8" + integrity sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg= + shellwords@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" @@ -8717,6 +9194,11 @@ slash@^1.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= + slice-ansi@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" @@ -8924,6 +9406,11 @@ stable@~0.1.6: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== +stack-trace@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + stack-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" @@ -9088,6 +9575,11 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= +strip-json-comments@~1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" + integrity sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E= + style-loader@0.23.0: version "0.23.0" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.0.tgz#8377fefab68416a2e05f1cabd8c3a3acfcce74f1" @@ -9149,6 +9641,18 @@ symbol-tree@^3.2.2: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY= +table@^3.7.8: + version "3.8.3" + resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" + integrity sha1-K7xULw/amGGnVdOUf+/Ys/UThV8= + dependencies: + ajv "^4.7.0" + ajv-keywords "^1.0.0" + chalk "^1.1.1" + lodash "^4.0.0" + slice-ansi "0.0.4" + string-width "^2.0.0" + table@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc" @@ -9222,7 +9726,7 @@ test-exclude@^4.2.1: read-pkg-up "^1.0.1" require-main-filename "^1.0.1" -text-table@0.2.0, text-table@^0.2.0: +text-table@0.2.0, text-table@^0.2.0, text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= @@ -9562,6 +10066,13 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +user-home@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" + integrity sha1-nHC/2Babwdy/SGBODwS4tJzenp8= + dependencies: + os-homedir "^1.0.0" + util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" From 53d2264351ca932830742275db859bcddb9ab37d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 10 Nov 2018 12:57:06 +0200 Subject: [PATCH 044/101] Proxy avatar requests through server and improve css --- maubot/management/api/auth.py | 25 +++++++- maubot/management/api/client.py | 13 ++++ maubot/management/api/middleware.py | 9 +-- maubot/management/frontend/src/api.js | 9 ++- .../frontend/src/components/Switch.js | 5 +- maubot/management/frontend/src/pages/Main.js | 2 +- .../frontend/src/pages/dashboard/Client.js | 62 ++++++++----------- .../frontend/src/style/base/body.sass | 28 --------- .../frontend/src/style/base/elements.sass | 2 +- .../frontend/src/style/lib/switch.sass | 9 +-- .../src/style/pages/client/instances.sass | 1 - .../frontend/src/style/pages/dashboard.sass | 1 + .../frontend/src/style/pages/sidebar.sass | 3 +- maubot/server.py | 13 +++- 14 files changed, 92 insertions(+), 90 deletions(-) diff --git a/maubot/management/api/auth.py b/maubot/management/api/auth.py index 34303d1..e754809 100644 --- a/maubot/management/api/auth.py +++ b/maubot/management/api/auth.py @@ -13,6 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import Optional from time import time import json @@ -39,13 +40,31 @@ def create_token(user: UserID) -> str: }) -@routes.post("/auth/ping") -async def ping(request: web.Request) -> web.Response: +def get_token(request: web.Request) -> str: token = request.headers.get("Authorization", "") if not token or not token.startswith("Bearer "): + token = request.query.get("access_token", None) + else: + token = token[len("Bearer "):] + return token + + +def check_token(request: web.Request) -> Optional[web.Response]: + token = get_token(request) + if not token: + return resp.no_token + elif not is_valid_token(token): + return resp.invalid_token + return None + + +@routes.post("/auth/ping") +async def ping(request: web.Request) -> web.Response: + token = get_token(request) + if not token: return resp.no_token - data = verify_token(get_config()["server.unshared_secret"], token[len("Bearer "):]) + data = verify_token(get_config()["server.unshared_secret"], token) if not data: return resp.invalid_token user = data.get("user_id", None) diff --git a/maubot/management/api/client.py b/maubot/management/api/client.py index 5bee0af..2975581 100644 --- a/maubot/management/api/client.py +++ b/maubot/management/api/client.py @@ -83,6 +83,8 @@ async def _update_client(client: Client, data: dict) -> web.Response: return resp.bad_client_access_token except MatrixRequestError: return resp.bad_client_access_details + except MatrixConnectionError: + return resp.bad_client_connection_details except ValueError as e: return resp.mxid_mismatch(str(e)[len("MXID mismatch: "):]) await client.update_avatar_url(data.get("avatar_url", None)) @@ -142,3 +144,14 @@ async def upload_avatar(request: web.Request) -> web.Response: "content_uri": await client.client.upload_media( content, request.headers.get("Content-Type", None)), }) + + +@routes.get("/client/{id}/avatar") +async def download_avatar(request: web.Request) -> web.Response: + user_id = request.match_info.get("id", None) + client = Client.get(user_id, None) + if not client: + return resp.client_not_found + if not client.avatar_url or client.avatar_url == "disable": + return web.Response() + return web.Response(body=await client.client.download_media(client.avatar_url)) diff --git a/maubot/management/api/middleware.py b/maubot/management/api/middleware.py index 2fefbe8..79bc26b 100644 --- a/maubot/management/api/middleware.py +++ b/maubot/management/api/middleware.py @@ -19,7 +19,7 @@ import logging from aiohttp import web from .responses import resp -from .auth import is_valid_token +from .auth import check_token Handler = Callable[[web.Request], Awaitable[web.Response]] @@ -28,12 +28,7 @@ Handler = Callable[[web.Request], Awaitable[web.Response]] async def auth(request: web.Request, handler: Handler) -> web.Response: if "/auth/" in request.path: return await handler(request) - token = request.headers.get("Authorization", "") - if not token or not token.startswith("Bearer "): - return resp.no_token - if not is_valid_token(token[len("Bearer "):]): - return resp.invalid_token - return await handler(request) + return check_token(request) or await handler(request) log = logging.getLogger("maubot.server") diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index e9fc926..6342b9d 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -const BASE_PATH = "/_matrix/maubot/v1" +export const BASE_PATH = "/_matrix/maubot/v1" export async function login(username, password) { const resp = await fetch(`${BASE_PATH}/auth/login`, { @@ -105,6 +105,10 @@ export async function uploadAvatar(id, data, mime) { return await resp.json() } +export function getAvatarURL(id) { + return `${BASE_PATH}/client/${id}/avatar?access_token=${localStorage.accessToken}` +} + export async function putClient(client) { const resp = await fetch(`${BASE_PATH}/client/${client.id}`, { headers: getHeaders(), @@ -128,8 +132,9 @@ export async function deleteClient(id) { } export default { + BASE_PATH, login, ping, getInstances, getInstance, getPlugins, getPlugin, uploadPlugin, - getClients, getClient, uploadAvatar, putClient, deleteClient, + getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient, } diff --git a/maubot/management/frontend/src/components/Switch.js b/maubot/management/frontend/src/components/Switch.js index 975b47c..6ef43fd 100644 --- a/maubot/management/frontend/src/components/Switch.js +++ b/maubot/management/frontend/src/components/Switch.js @@ -37,9 +37,12 @@ class Switch extends Component { } } + toggleKeyboard = evt => (evt.key === " " || evt.key === "Enter") && this.toggle() + render() { return ( -
+
{this.props.onText || "On"} diff --git a/maubot/management/frontend/src/pages/Main.js b/maubot/management/frontend/src/pages/Main.js index efb9ac3..b655085 100644 --- a/maubot/management/frontend/src/pages/Main.js +++ b/maubot/management/frontend/src/pages/Main.js @@ -44,7 +44,7 @@ class Main extends Component { localStorage.username = username this.setState({ authed: true }) } else { - localStorage.accessToken = undefined + delete localStorage.accessToken } } catch (err) { console.error(err) diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index 1350a31..ef414a0 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -21,14 +21,6 @@ import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTab import Spinner from "../../components/Spinner" import api from "../../api" -function getAvatarURL(client) { - if (!client.avatar_url) { - return "" - } - const id = client.avatar_url.substr("mxc://".length) - return `${client.homeserver}/_matrix/media/r0/download/${id}` -} - const ClientListEntry = ({ client }) => { const classes = ["client", "entry"] if (!client.enabled) { @@ -38,7 +30,7 @@ const ClientListEntry = ({ client }) => { } return ( - {client.id.substr(1, + {client.displayname || client.id} @@ -153,8 +145,8 @@ class Client extends Component { startOrStop = async () => { this.setState({ startingOrStopping: true }) const resp = await api.putClient({ - id: this.state.id, - started: !this.state.started, + id: this.props.client.id, + started: !this.props.client.started, }) if (resp.id) { this.props.onChange(resp) @@ -172,11 +164,11 @@ class Client extends Component { return !Boolean(this.props.client) } - renderSidebar = () => ( + renderSidebar = () => !this.isNew && (
- Avatar + Avatar ) - renderInstances = () => ( + renderPrefButtons = () => <> +
+ {!this.isNew && ( + + )} + +
+
{this.state.error}
+ + + renderInstances = () => !this.isNew && (
-

Instances

+

{this.state.instances.length > 0 ? "Instances" : "No instances :("}

{this.state.instances.map(instance => ( {instance.id} @@ -241,28 +247,14 @@ class Client extends Component {
) - renderInfoContainer = () => ( -
- {this.renderPreferences()} -
- {!this.isNew && ( - - )} - -
-
{this.state.error}
- {this.renderInstances()} -
- ) - render() { return
- {!this.isNew && this.renderSidebar()} - {this.renderInfoContainer()} + {this.renderSidebar()} +
+ {this.renderPreferences()} + {this.renderPrefButtons()} + {this.renderInstances()} +
} } diff --git a/maubot/management/frontend/src/style/base/body.sass b/maubot/management/frontend/src/style/base/body.sass index ba0406b..c30dd67 100644 --- a/maubot/management/frontend/src/style/base/body.sass +++ b/maubot/management/frontend/src/style/base/body.sass @@ -34,34 +34,6 @@ body right: 0 background-color: $background-dark - > * - background-color: $background - .maubot-loading margin-top: 10rem width: 10rem - -//.lindeb - > header - position: absolute - top: 0 - height: $header-height - left: 0 - right: 0 - - > main - position: absolute - top: $header-height - bottom: 0 - left: 0 - right: 0 - - text-align: center - - > .lindeb-content - text-align: left - display: inline-block - width: 100% - max-width: $max-width - box-sizing: border-box - padding: 0 1rem diff --git a/maubot/management/frontend/src/style/base/elements.sass b/maubot/management/frontend/src/style/base/elements.sass index 6270aba..99a6e5e 100644 --- a/maubot/management/frontend/src/style/base/elements.sass +++ b/maubot/management/frontend/src/style/base/elements.sass @@ -22,7 +22,7 @@ background-color: $background border: none border-radius: .25rem - color: $inverted-text-color + color: $text-color box-sizing: border-box font-size: 1rem diff --git a/maubot/management/frontend/src/style/lib/switch.sass b/maubot/management/frontend/src/style/lib/switch.sass index 570320c..ac18689 100644 --- a/maubot/management/frontend/src/style/lib/switch.sass +++ b/maubot/management/frontend/src/style/lib/switch.sass @@ -37,7 +37,7 @@ transition: .5s text-align: center - color: $inverted-text-color + color: $text-color border-radius: .15rem 0 0 .15rem background-color: $primary @@ -50,7 +50,7 @@ text-align: center vertical-align: middle - color: $inverted-text-color + color: $text-color font-size: 1rem user-select: none @@ -67,14 +67,9 @@ transform: translateX(100%) border-radius: 0 .15rem .15rem 0 - background-color: $primary .on display: inline .off display: none - - - - diff --git a/maubot/management/frontend/src/style/pages/client/instances.sass b/maubot/management/frontend/src/style/pages/client/instances.sass index 7d5fb3c..24c5407 100644 --- a/maubot/management/frontend/src/style/pages/client/instances.sass +++ b/maubot/management/frontend/src/style/pages/client/instances.sass @@ -32,5 +32,4 @@ border: 1px solid $primary &:hover - color: $inverted-text-color background-color: $primary diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 8ec9563..9eb8ecb 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -21,6 +21,7 @@ max-width: 60rem margin: auto box-shadow: 0 .5rem .5rem rgba(0, 0, 0, 0.5) + background-color: $background > a.title grid-area: title diff --git a/maubot/management/frontend/src/style/pages/sidebar.sass b/maubot/management/frontend/src/style/pages/sidebar.sass index 8015e1a..c086714 100644 --- a/maubot/management/frontend/src/style/pages/sidebar.sass +++ b/maubot/management/frontend/src/style/pages/sidebar.sass @@ -29,9 +29,8 @@ div.title h2 - margin: 0 display: inline-block - + margin: 0 0 .25rem 0 font-size: 1.25rem a diff --git a/maubot/server.py b/maubot/server.py index 501677a..5788d8a 100644 --- a/maubot/server.py +++ b/maubot/server.py @@ -13,16 +13,25 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from aiohttp import web import logging import asyncio +from aiohttp import web +from aiohttp.abc import AbstractAccessLogger + from mautrix.api import PathBuilder, Method from .config import Config from .__meta__ import __version__ +class AccessLogger(AbstractAccessLogger): + def log(self, request: web.Request, response: web.Response, time: int): + self.logger.info(f'{request.remote} "{request.method} {request.path} ' + f'{response.status} {response.body_length} ' + f'in {round(time, 4)}s"') + + class MaubotServer: log: logging.Logger = logging.getLogger("maubot.server") @@ -39,7 +48,7 @@ class MaubotServer: as_path = PathBuilder(config["server.appservice_base_path"]) self.add_route(Method.PUT, as_path.transactions, self.handle_transaction) - self.runner = web.AppRunner(self.app) + self.runner = web.AppRunner(self.app, access_log_class=AccessLogger) def add_route(self, method: Method, path: PathBuilder, handler) -> None: self.app.router.add_route(method.value, str(path), handler) From 0a406ac071bfe74359190c5c43de0278aa215783 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 10 Nov 2018 13:49:54 +0200 Subject: [PATCH 045/101] Change more things --- .../management/frontend/src/pages/dashboard/Client.js | 10 ++++++++-- .../management/frontend/src/style/base/elements.sass | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index ef414a0..f6d9ed9 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -222,10 +222,16 @@ class Client extends Component { ) + get hasInstances() { + return this.state.instances.length > 0 + } + renderPrefButtons = () => <>
{!this.isNew && ( - )} @@ -238,7 +244,7 @@ class Client extends Component { renderInstances = () => !this.isNew && (
-

{this.state.instances.length > 0 ? "Instances" : "No instances :("}

+

{this.hasInstances ? "Instances" : "No instances :("}

{this.state.instances.map(instance => ( {instance.id} diff --git a/maubot/management/frontend/src/style/base/elements.sass b/maubot/management/frontend/src/style/base/elements.sass index 99a6e5e..91a20c2 100644 --- a/maubot/management/frontend/src/style/base/elements.sass +++ b/maubot/management/frontend/src/style/base/elements.sass @@ -26,6 +26,9 @@ box-sizing: border-box font-size: 1rem + &.disabled-bg + background-color: $background-dark + &:not(:disabled) cursor: pointer From b0d782906b30064c8e69ea45360387a4d4b1808d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 10 Nov 2018 16:33:12 +0200 Subject: [PATCH 046/101] Implement instance view --- maubot/instance.py | 17 ++ maubot/management/api/instance.py | 1 + maubot/management/frontend/package.json | 3 +- maubot/management/frontend/src/api.js | 101 ++++----- .../src/components/PreferenceTable.js | 7 + .../frontend/src/pages/dashboard/Client.js | 8 +- .../frontend/src/pages/dashboard/Instance.js | 157 ++++++++++++- .../frontend/src/pages/dashboard/index.js | 13 +- .../src/style/lib/preferencetable.sass | 4 + .../src/style/pages/client/index.sass | 23 -- .../frontend/src/style/pages/dashboard.sass | 23 ++ .../frontend/src/style/pages/instance.sass | 14 +- maubot/management/frontend/yarn.lock | 206 +++++++++++++++++- 13 files changed, 478 insertions(+), 99 deletions(-) diff --git a/maubot/instance.py b/maubot/instance.py index 00f8be7..c797466 100644 --- a/maubot/instance.py +++ b/maubot/instance.py @@ -186,10 +186,27 @@ class PluginInstance: self.db_instance.primary_user = client.id self.client.references.remove(self) self.client = client + self.client.references.add(self) await self.start() self.log.debug(f"Primary user switched to {self.client.id}") return True + async def update_type(self, type: str) -> bool: + if not type or type == self.type: + return True + try: + loader = PluginLoader.find(type) + except KeyError: + return False + await self.stop() + self.db_instance.type = loader.id + self.loader.references.remove(self) + self.loader = loader + self.loader.references.add(self) + await self.start() + self.log.debug(f"Type switched to {self.loader.id}") + return True + async def update_started(self, started: bool) -> None: if started is not None and started != self.started: await (self.start() if started else self.stop()) diff --git a/maubot/management/api/instance.py b/maubot/management/api/instance.py index 57cf2f3..ad7f429 100644 --- a/maubot/management/api/instance.py +++ b/maubot/management/api/instance.py @@ -70,6 +70,7 @@ async def _update_instance(instance: PluginInstance, data: dict) -> web.Response instance.update_enabled(data.get("enabled", None)) instance.update_config(data.get("config", None)) await instance.update_started(data.get("started", None)) + await instance.update_type(data.get("type", None)) instance.db.commit() return resp.updated(instance.to_dict()) diff --git a/maubot/management/frontend/package.json b/maubot/management/frontend/package.json index 75765fa..7bd08ef 100644 --- a/maubot/management/frontend/package.json +++ b/maubot/management/frontend/package.json @@ -7,7 +7,8 @@ "react": "^16.6.0", "react-dom": "^16.6.0", "react-router-dom": "^4.3.1", - "react-scripts": "2.0.5" + "react-scripts": "2.0.5", + "react-select": "^2.1.1" }, "scripts": { "start": "react-scripts start", diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index 6342b9d..e266383 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -16,6 +16,40 @@ export const BASE_PATH = "/_matrix/maubot/v1" +function getHeaders(contentType = "application/json") { + return { + "Content-Type": contentType, + "Authorization": `Bearer ${localStorage.accessToken}`, + } +} + +async function defaultDelete(type, id) { + const resp = await fetch(`${BASE_PATH}/${type}/${id}`, { + headers: getHeaders(), + method: "DELETE", + }) + if (resp.status === 204) { + return { + "success": true, + } + } + return await resp.json() +} + +async function defaultPut(type, entry, id = undefined) { + const resp = await fetch(`${BASE_PATH}/${type}/${id || entry.id}`, { + headers: getHeaders(), + body: JSON.stringify(entry), + method: "PUT", + }) + return await resp.json() +} + +async function defaultGet(url) { + const resp = await fetch(`${BASE_PATH}/${url}`, { headers: getHeaders() }) + return await resp.json() +} + export async function login(username, password) { const resp = await fetch(`${BASE_PATH}/auth/login`, { method: "POST", @@ -27,13 +61,6 @@ export async function login(username, password) { return await resp.json() } -function getHeaders(contentType = "application/json") { - return { - "Content-Type": contentType, - "Authorization": `Bearer ${localStorage.accessToken}`, - } -} - export async function ping() { const response = await fetch(`${BASE_PATH}/auth/ping`, { method: "POST", @@ -48,25 +75,13 @@ export async function ping() { throw json } -export async function getInstances() { - const resp = await fetch(`${BASE_PATH}/instances`, { headers: getHeaders() }) - return await resp.json() -} +export const getInstances = () => defaultGet("/instances") +export const getInstance = id => defaultGet(`/instance/${id}`) +export const putInstance = (instance, id) => defaultPut("instance", instance, id) +export const deleteInstance = id => defaultDelete("instance", id) -export async function getInstance(id) { - const resp = await fetch(`${BASE_PATH}/instance/${id}`, { headers: getHeaders() }) - return await resp.json() -} - -export async function getPlugins() { - const resp = await fetch(`${BASE_PATH}/plugins`, { headers: getHeaders() }) - return await resp.json() -} - -export async function getPlugin(id) { - const resp = await fetch(`${BASE_PATH}/plugin/${id}`, { headers: getHeaders() }) - return await resp.json() -} +export const getPlugins = () => defaultGet("/plugins") +export const getPlugin = id => defaultGet(`/plugin/${id}`) export async function uploadPlugin(data, id) { let resp @@ -86,15 +101,8 @@ export async function uploadPlugin(data, id) { return await resp.json() } -export async function getClients() { - const resp = await fetch(`${BASE_PATH}/clients`, { headers: getHeaders() }) - return await resp.json() -} - -export async function getClient(id) { - const resp = await fetch(`${BASE_PATH}/client/${id}`, { headers: getHeaders() }) - return await resp.json() -} +export const getClients = () => defaultGet("/clients") +export const getClient = id => defaultGet(`/clients/${id}`) export async function uploadAvatar(id, data, mime) { const resp = await fetch(`${BASE_PATH}/client/${id}/avatar`, { @@ -109,32 +117,13 @@ export function getAvatarURL(id) { return `${BASE_PATH}/client/${id}/avatar?access_token=${localStorage.accessToken}` } -export async function putClient(client) { - const resp = await fetch(`${BASE_PATH}/client/${client.id}`, { - headers: getHeaders(), - body: JSON.stringify(client), - method: "PUT", - }) - return await resp.json() -} - -export async function deleteClient(id) { - const resp = await fetch(`${BASE_PATH}/client/${id}`, { - headers: getHeaders(), - method: "DELETE", - }) - if (resp.status === 204) { - return { - "success": true, - } - } - return await resp.json() -} +export const putClient = client => defaultPut("client", client) +export const deleteClient = id => defaultDelete("client", id) export default { BASE_PATH, login, ping, - getInstances, getInstance, + getInstances, getInstance, putInstance, deleteInstance, getPlugins, getPlugin, uploadPlugin, getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient, } diff --git a/maubot/management/frontend/src/components/PreferenceTable.js b/maubot/management/frontend/src/components/PreferenceTable.js index 28ddfab..b58c55b 100644 --- a/maubot/management/frontend/src/components/PreferenceTable.js +++ b/maubot/management/frontend/src/components/PreferenceTable.js @@ -14,6 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React from "react" +import Select from "react-select" import Switch from "./Switch" export const PrefTable = ({ children, wrapperClass }) => { @@ -52,4 +53,10 @@ export const PrefSwitch = ({ rowName, ...args }) => ( ) +export const PrefSelect = ({ rowName, ...args }) => ( + + +export const PrefInput = ({ rowName, fullWidth = false, ...args }) => ( + + ) -export const PrefSwitch = ({ rowName, ...args }) => ( - - +export const PrefSwitch = ({ rowName, fullWidth = false, ...args }) => ( + + ) -export const PrefSelect = ({ rowName, ...args }) => ( - - ) diff --git a/maubot/management/frontend/src/components/Switch.js b/maubot/management/frontend/src/components/Switch.js index 6ef43fd..a063abe 100644 --- a/maubot/management/frontend/src/components/Switch.js +++ b/maubot/management/frontend/src/components/Switch.js @@ -42,7 +42,7 @@ class Switch extends Component { render() { return (
+ tabIndex="0" onKeyPress={this.toggleKeyboard} id={this.props.id}>
{this.props.onText || "On"} diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index dba8eb2..24e91f2 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -193,18 +193,18 @@ class Client extends Component { renderPreferences = () => ( - - + diff --git a/maubot/management/frontend/src/pages/dashboard/Instance.js b/maubot/management/frontend/src/pages/dashboard/Instance.js index 67bcf16..f983938 100644 --- a/maubot/management/frontend/src/pages/dashboard/Instance.js +++ b/maubot/management/frontend/src/pages/dashboard/Instance.js @@ -63,8 +63,8 @@ class Instance extends Component { } componentWillReceiveProps(nextProps) { - this.setState(Object.assign(this.initialState, nextProps.instance), () => - this.updateClientOptions()) + this.setState(Object.assign(this.initialState, nextProps.instance)) + this.updateClientOptions() } clientSelectEntry = client => client && { @@ -127,7 +127,9 @@ class Instance extends Component { } get selectedClientEntry() { - return this.clientSelectEntry(this.props.ctx.clients[this.state.primary_user]) + return this.state.primary_user + ? this.clientSelectEntry(this.props.ctx.clients[this.state.primary_user]) + : {} } get selectedPluginEntry() { @@ -159,7 +161,7 @@ class Instance extends Component { + disabled={!this.isNew} fullWidth={true}/> this.setState({ enabled })}/> Maubot Manager -
-
- {localStorage.username} -
+ +
+ {localStorage.username}
@@ -128,12 +133,12 @@ class Dashboard extends Component { this.add("plugins", newEntry)}/>}/> - this.renderView("instance", Instance, match.params.id)}/> + this.renderView("instances", Instance, match.params.id)}/> - this.renderView("client", Client, match.params.id)}/> + this.renderView("clients", Client, match.params.id)}/> this.renderView("plugin", Plugin, match.params.id)}/> - "Not found :("}/> + this.renderNotFound()}/>
diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index c5fe3e9..56269f2 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -69,6 +69,11 @@ @import instance @import plugin + > .not-found + text-align: center + margin-top: 5rem + font-size: 1.5rem + div.buttons +button-group display: flex From 9603f59b96f68b973bb4c793a037a3b932e42955 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 10 Nov 2018 22:16:34 +0200 Subject: [PATCH 051/101] Fix bugs and improve minor UI things --- .../frontend/src/pages/dashboard/Client.js | 2 +- .../frontend/src/pages/dashboard/Instance.js | 4 +-- .../frontend/src/pages/dashboard/Plugin.js | 27 +++++++++---------- .../frontend/src/pages/dashboard/index.js | 2 +- .../src/style/lib/preferencetable.sass | 4 +++ 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index 04d195f..0424911 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -157,7 +157,7 @@ class Client extends BaseMainView { renderPreferences = () => ( - + disabled={!this.isNew} fullWidth={true} className="id"/> this.setState({ enabled })}/> + {!this.isNew && + + + }
evt.target.parentElement.classList.remove("drag")}/> {this.state.uploading && }
- {!this.isNew && <> - - - - -
- -
- } + {!this.isNew &&
+ +
}
{this.state.error}
{!this.isNew && this.renderInstances()}
diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index f356db9..ed72dc7 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -137,7 +137,7 @@ class Dashboard extends Component { this.renderView("clients", Client, match.params.id)}/> - this.renderView("plugin", Plugin, match.params.id)}/> + this.renderView("plugins", Plugin, match.params.id)}/> this.renderNotFound()}/>
diff --git a/maubot/management/frontend/src/style/lib/preferencetable.sass b/maubot/management/frontend/src/style/lib/preferencetable.sass index 66fcad7..64c2124 100644 --- a/maubot/management/frontend/src/style/lib/preferencetable.sass +++ b/maubot/management/frontend/src/style/lib/preferencetable.sass @@ -62,6 +62,10 @@ border-bottom: 1px solid $background + &.id:disabled + font-family: "Fira Code", monospace + font-weight: bold + &:not(:disabled) border-bottom: 1px dotted $primary From 3a36b862df59ae272cb46a071c6568305baded3d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 10 Nov 2018 23:29:31 +0200 Subject: [PATCH 052/101] Add mobile compatibility to management UI --- .../frontend/src/pages/dashboard/index.js | 23 +++++- .../src/style/lib/preferencetable.sass | 3 + .../src/style/pages/client/avatar.sass | 3 + .../src/style/pages/client/index.sass | 8 +- .../src/style/pages/dashboard-grid.css | 19 +++++ .../frontend/src/style/pages/dashboard.sass | 14 ++++ .../frontend/src/style/pages/instance.sass | 2 - .../frontend/src/style/pages/login.sass | 9 ++- .../frontend/src/style/pages/plugin.sass | 2 - .../frontend/src/style/pages/sidebar.sass | 2 +- .../frontend/src/style/pages/topbar.sass | 74 +++++++++++++++++++ 11 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 maubot/management/frontend/src/style/pages/topbar.sass diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index ed72dc7..567c443 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" -import { Route, Switch, Link } from "react-router-dom" +import { Route, Switch, Link, withRouter } from "react-router-dom" import api from "../../api" import { ReactComponent as Plus } from "../../res/plus.svg" import Instance from "./Instance" @@ -28,10 +28,17 @@ class Dashboard extends Component { instances: {}, clients: {}, plugins: {}, + sidebarOpen: false, } window.maubot = this } + componentDidUpdate(prevProps) { + if (this.props.location !== prevProps.location) { + this.setState({ sidebarOpen: false }) + } + } + async componentWillMount() { const [instanceList, clientList, pluginList] = await Promise.all([ api.getInstances(), api.getClients(), api.getPlugins()]) @@ -90,15 +97,15 @@ class Dashboard extends Component { ) render() { - return
+ return
Maubot Manager -
{localStorage.username}
+ + +
+
this.setState({ sidebarOpen: !this.state.sidebarOpen })}> + +
+
+
"Hello, World!"}/> @@ -145,4 +160,4 @@ class Dashboard extends Component { } } -export default Dashboard +export default withRouter(Dashboard) diff --git a/maubot/management/frontend/src/style/lib/preferencetable.sass b/maubot/management/frontend/src/style/lib/preferencetable.sass index 64c2124..c72dff1 100644 --- a/maubot/management/frontend/src/style/lib/preferencetable.sass +++ b/maubot/management/frontend/src/style/lib/preferencetable.sass @@ -23,6 +23,9 @@ > .entry display: block + + @media screen and (max-width: 55rem) + width: calc(100% - 1rem) width: calc(50% - 1rem) margin: .5rem diff --git a/maubot/management/frontend/src/style/pages/client/avatar.sass b/maubot/management/frontend/src/style/pages/client/avatar.sass index 062a605..25e596a 100644 --- a/maubot/management/frontend/src/style/pages/client/avatar.sass +++ b/maubot/management/frontend/src/style/pages/client/avatar.sass @@ -21,6 +21,9 @@ height: 8rem border-radius: 50% + @media screen and (max-width: 40rem) + margin: 0 auto 1rem + > img.avatar position: absolute display: block diff --git a/maubot/management/frontend/src/style/pages/client/index.sass b/maubot/management/frontend/src/style/pages/client/index.sass index 9444513..3d004db 100644 --- a/maubot/management/frontend/src/style/pages/client/index.sass +++ b/maubot/management/frontend/src/style/pages/client/index.sass @@ -16,7 +16,6 @@ > div.client display: flex - margin: 2rem 4rem > div.sidebar vertical-align: top @@ -36,3 +35,10 @@ > div.instances +instancelist + + @media screen and (max-width: 40rem) + flex-wrap: wrap + + > div.sidebar, > div.info + width: 100% + margin-right: 0 diff --git a/maubot/management/frontend/src/style/pages/dashboard-grid.css b/maubot/management/frontend/src/style/pages/dashboard-grid.css index 88010db..516c7ff 100644 --- a/maubot/management/frontend/src/style/pages/dashboard-grid.css +++ b/maubot/management/frontend/src/style/pages/dashboard-grid.css @@ -5,3 +5,22 @@ [row3-start] "sidebar main" auto [row3-end] / 15rem auto; } + + +@media screen and (max-width: 35rem) { + .dashboard { + grid-template: + [row1-start] "topbar" 3.5rem [row1-end] + [row2-start] "main" auto [row2-end] + / auto; + } + + .dashboard.sidebar-open { + grid-template: + [row1-start] "title topbar" 3.5rem [row1-end] + [row2-start] "user main" 2.5rem [row2-end] + [row3-start] "sidebar main" auto [row3-end] + / 15rem 100%; + overflow-x: hidden; + } +} diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 56269f2..681728e 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -60,11 +60,19 @@ border-radius: .25rem @import sidebar + @import topbar + + @media screen and (max-width: 35rem) + &:not(.sidebar-open) + > nav.sidebar, > a.title, > div.user + display: none !important > main.view grid-area: main border-left: 1px solid $border-color + overflow-y: scroll + @import client/index @import instance @import plugin @@ -74,6 +82,12 @@ margin-top: 5rem font-size: 1.5rem + > div:not(.not-found) + margin: 2rem 4rem + + @media screen and (max-width: 50rem) + margin: 2rem 1rem + div.buttons +button-group display: flex diff --git a/maubot/management/frontend/src/style/pages/instance.sass b/maubot/management/frontend/src/style/pages/instance.sass index 607352c..18b63a0 100644 --- a/maubot/management/frontend/src/style/pages/instance.sass +++ b/maubot/management/frontend/src/style/pages/instance.sass @@ -15,8 +15,6 @@ // along with this program. If not, see . > div.instance - margin: 2rem 4rem - > div.preference-table .select-client display: flex diff --git a/maubot/management/frontend/src/style/pages/login.sass b/maubot/management/frontend/src/style/pages/login.sass index 6447f54..c1be6a4 100644 --- a/maubot/management/frontend/src/style/pages/login.sass +++ b/maubot/management/frontend/src/style/pages/login.sass @@ -28,6 +28,10 @@ border-radius: .25rem margin-top: 3rem + @media screen and (max-width: 27rem) + margin: 3rem 1rem 0 + width: calc(100% - 2rem) + h1 color: $primary margin: 3rem 0 @@ -35,13 +39,14 @@ input, button margin: .5rem 2.5rem height: 3rem - width: 20rem + width: calc(100% - 5rem) + box-sizing: border-box input +input button - +button($width: 20rem, $height: 3rem, $padding: 0) + +button($width: calc(100% - 5rem), $height: 3rem, $padding: 0) +main-color-button .spinner diff --git a/maubot/management/frontend/src/style/pages/plugin.sass b/maubot/management/frontend/src/style/pages/plugin.sass index b45d867..b9fed11 100644 --- a/maubot/management/frontend/src/style/pages/plugin.sass +++ b/maubot/management/frontend/src/style/pages/plugin.sass @@ -15,8 +15,6 @@ // along with this program. If not, see . > .plugin - margin: 2rem 4rem - > .upload-box +upload-box diff --git a/maubot/management/frontend/src/style/pages/sidebar.sass b/maubot/management/frontend/src/style/pages/sidebar.sass index 58078f4..497ba06 100644 --- a/maubot/management/frontend/src/style/pages/sidebar.sass +++ b/maubot/management/frontend/src/style/pages/sidebar.sass @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -> .sidebar +> nav.sidebar grid-area: sidebar background-color: white diff --git a/maubot/management/frontend/src/style/pages/topbar.sass b/maubot/management/frontend/src/style/pages/topbar.sass new file mode 100644 index 0000000..e7d8312 --- /dev/null +++ b/maubot/management/frontend/src/style/pages/topbar.sass @@ -0,0 +1,74 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +.topbar + background-color: $primary + + display: flex + justify-items: center + align-items: center + padding: 0 .75rem + + @media screen and (min-width: calc(35rem + 1px)) + display: none + +// Hamburger menu based on "Pure CSS Hamburger fold-out menu" codepen by Erik Terwan (MIT license) +// https://codepen.io/erikterwan/pen/EVzeRP + +.hamburger + display: block + user-select: none + cursor: pointer + + > span + display: block + width: 29px + height: 4px + margin-bottom: 5px + position: relative + + background: white + border-radius: 3px + + z-index: 1 + + transform-origin: 4px 0 + + //transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0), opacity 0.55s ease + + &:nth-of-type(1) + transform-origin: 0 0 + + &:nth-of-type(3) + transform-origin: 0 100% + + transform: translateY(2px) + + &.active + transform: translateX(1px) translateY(4px) + + &.active > span + opacity: 1 + + &:nth-of-type(1) + transform: rotate(45deg) translate(-2px, -1px) + + &:nth-of-type(2) + opacity: 0 + transform: rotate(0deg) scale(0.2, 0.2) + + &:nth-of-type(3) + transform: rotate(-45deg) translate(0, -1px) From 0264f7b79447d6b1b0f94a3b0a40669618c8cd5a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 10 Nov 2018 23:46:37 +0200 Subject: [PATCH 053/101] Hide unnecessary scrollbars --- maubot/management/frontend/src/style/pages/dashboard.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 681728e..a5b82bf 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -71,7 +71,7 @@ grid-area: main border-left: 1px solid $border-color - overflow-y: scroll + overflow-y: auto @import client/index @import instance From e0306d29b5074a0d33c83e3e5b2a603f2becfdf6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 11 Nov 2018 00:43:58 +0200 Subject: [PATCH 054/101] Make maubot http server serve frontend for production --- example-config.yaml | 5 ++ maubot/__main__.py | 7 +-- maubot/config.py | 3 ++ maubot/management/api/base.py | 8 ++++ maubot/management/frontend/package.json | 1 + maubot/management/frontend/src/pages/Main.js | 2 +- maubot/server.py | 48 +++++++++++++++++--- 7 files changed, 64 insertions(+), 10 deletions(-) diff --git a/example-config.yaml b/example-config.yaml index b3987f1..1b02d67 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -24,6 +24,11 @@ server: port: 29316 # The base management API path. base_path: /_matrix/maubot/v1 + # The base path for the UI. + ui_base_path: /_matrix/maubot + # Override path from where to load UI resources. + # Set to false to using pkg_resources to find the path. + override_resource_path: false # The base appservice API path. Use / for legacy appservice API and /_matrix/app/v1 for v1. appservice_base_path: /_matrix/app/v1 # The shared secret to sign API access tokens. diff --git a/maubot/__main__.py b/maubot/__main__.py index 3929095..e970c50 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -26,7 +26,7 @@ from .server import MaubotServer from .client import Client, init as init_client_class from .loader.zip import init as init_zip_loader from .instance import init as init_plugin_instance_class -from .management.api import init as init_management +from .management.api import init as init_management_api from .__meta__ import __version__ parser = argparse.ArgumentParser(description="A plugin-based Matrix bot system.", @@ -52,8 +52,9 @@ init_zip_loader(config) db_session = init_db(config) clients = init_client_class(db_session, loop) plugins = init_plugin_instance_class(db_session, config, loop) -management_api = init_management(config, loop) -server = MaubotServer(config, management_api, loop) +management_api = init_management_api(config, loop) +server = MaubotServer(config, loop) +server.app.add_subapp(config["server.base_path"], management_api) for plugin in plugins: plugin.load() diff --git a/maubot/config.py b/maubot/config.py index cf39d00..ea8dd3c 100644 --- a/maubot/config.py +++ b/maubot/config.py @@ -38,6 +38,9 @@ class Config(BaseFileConfig): copy("server.hostname") copy("server.port") copy("server.listen") + copy("server.base_path") + copy("server.ui_base_path") + copy("server.override_resource_path") copy("server.appservice_base_path") shared_secret = self["server.unshared_secret"] if shared_secret is None or shared_secret == "generate": diff --git a/maubot/management/api/base.py b/maubot/management/api/base.py index d9c2077..4cf9636 100644 --- a/maubot/management/api/base.py +++ b/maubot/management/api/base.py @@ -15,6 +15,7 @@ # along with this program. If not, see . from aiohttp import web +from ...__meta__ import __version__ from ...config import Config routes: web.RouteTableDef = web.RouteTableDef() @@ -28,3 +29,10 @@ def set_config(config: Config) -> None: def get_config() -> Config: return _config + + +@routes.get("/version") +async def version(_: web.Request) -> web.Response: + return web.json_response({ + "version": __version__ + }) diff --git a/maubot/management/frontend/package.json b/maubot/management/frontend/package.json index c25113f..320679b 100644 --- a/maubot/management/frontend/package.json +++ b/maubot/management/frontend/package.json @@ -26,6 +26,7 @@ "last 2 ios_saf versions" ], "proxy": "http://localhost:29316", + "homepage": ".", "devDependencies": { "sass-lint": "^1.12.1", "sass-lint-auto-fix": "^0.15.0" diff --git a/maubot/management/frontend/src/pages/Main.js b/maubot/management/frontend/src/pages/Main.js index b655085..63f419c 100644 --- a/maubot/management/frontend/src/pages/Main.js +++ b/maubot/management/frontend/src/pages/Main.js @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" -import { BrowserRouter as Router, Switch } from "react-router-dom" +import { HashRouter as Router, Switch } from "react-router-dom" import PrivateRoute from "../components/PrivateRoute" import Spinner from "../components/Spinner" import api from "../api" diff --git a/maubot/server.py b/maubot/server.py index 5788d8a..06f1c4d 100644 --- a/maubot/server.py +++ b/maubot/server.py @@ -18,6 +18,7 @@ import asyncio from aiohttp import web from aiohttp.abc import AbstractAccessLogger +import pkg_resources from mautrix.api import PathBuilder, Method @@ -35,21 +36,56 @@ class AccessLogger(AbstractAccessLogger): class MaubotServer: log: logging.Logger = logging.getLogger("maubot.server") - def __init__(self, config: Config, management: web.Application, - loop: asyncio.AbstractEventLoop) -> None: + def __init__(self, config: Config, loop: asyncio.AbstractEventLoop) -> None: self.loop = loop or asyncio.get_event_loop() self.app = web.Application(loop=self.loop) self.config = config - path = PathBuilder(config["server.base_path"]) - self.add_route(Method.GET, path.version, self.version) - self.app.add_subapp(config["server.base_path"], management) - as_path = PathBuilder(config["server.appservice_base_path"]) self.add_route(Method.PUT, as_path.transactions, self.handle_transaction) + self.setup_management_ui() + self.runner = web.AppRunner(self.app, access_log_class=AccessLogger) + def setup_management_ui(self) -> None: + ui_base = self.config["server.ui_base_path"] + if ui_base == "/": + ui_base = "" + directory = (self.config["server.override_resource_path"] + or pkg_resources.resource_filename("maubot", "management/frontend/build")) + self.app.router.add_static(f"{ui_base}/static", f"{directory}/static") + self.setup_static_root_files(directory, ui_base) + + with open(f"{directory}/index.html", "r") as file: + index_html = file.read() + + @web.middleware + async def frontend_404_middleware(request, handler): + if hasattr(handler, "__self__") and isinstance(handler.__self__, web.StaticResource): + try: + return await handler(request) + except web.HTTPNotFound: + return web.Response(body=index_html, content_type="text/html") + return await handler(request) + + self.app.middlewares.append(frontend_404_middleware) + self.app.router.add_get(f"{ui_base}/", lambda _: web.Response(body=index_html, + content_type="text/html")) + self.app.router.add_get(ui_base, lambda _: web.HTTPFound(f"{ui_base}/")) + + def setup_static_root_files(self, directory: str, ui_base: str) -> None: + files = { + "asset-manifest.json": "application/json", + "manifest.json": "application/json", + "favicon.png": "image/png", + } + for file, mime in files.items(): + with open(f"{directory}/{file}", "rb") as stream: + data = stream.read() + self.app.router.add_get(f"{ui_base}/{file}", lambda _: web.Response(body=data, + content_type=mime)) + def add_route(self, method: Method, path: PathBuilder, handler) -> None: self.app.router.add_route(method.value, str(path), handler) From 3d88277fd24b0643ef82950b2e71d280608e950a Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 11 Nov 2018 00:52:28 +0200 Subject: [PATCH 055/101] Build frontend in dockerfile --- Dockerfile | 6 ++++++ docker/example-config.yaml | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/Dockerfile b/Dockerfile index 710b778..ae90a34 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,15 @@ +FROM node:10 AS frontend-builder + +COPY ./maubot/management/frontend /frontend +RUN cd /frontend && yarn --prod && yarn build + FROM alpine:3.8 ENV UID=1337 \ GID=1337 COPY . /opt/maubot +COPY --from=frontend-builder /frontend/build /opt/maubot/frontend WORKDIR /opt/maubot RUN apk add --no-cache \ py3-aiohttp \ diff --git a/docker/example-config.yaml b/docker/example-config.yaml index 77f97f0..a273629 100644 --- a/docker/example-config.yaml +++ b/docker/example-config.yaml @@ -24,6 +24,11 @@ server: port: 29316 # The base management API path. base_path: /_matrix/maubot/v1 + # The base path for the UI. + ui_base_path: /_matrix/maubot + # Override path from where to load UI resources. + # Set to false to using pkg_resources to find the path. + override_resource_path: /opt/maubot/frontend # The base appservice API path. Use / for legacy appservice API and /_matrix/app/v1 for v1. appservice_base_path: /_matrix/app/v1 # The shared secret to sign API access tokens. From 640caa2f2e10a1a72555dc69b07e827b4e70fdd1 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 11 Nov 2018 00:58:08 +0200 Subject: [PATCH 056/101] Fix double slash in API paths --- maubot/management/frontend/src/api.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index bad6458..8d7d9b6 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -45,8 +45,8 @@ async function defaultPut(type, entry, id = undefined) { return await resp.json() } -async function defaultGet(url) { - const resp = await fetch(`${BASE_PATH}/${url}`, { headers: getHeaders() }) +async function defaultGet(path) { + const resp = await fetch(`${BASE_PATH}${path}`, { headers: getHeaders() }) return await resp.json() } From b52766ab1e0705b03d130467c76d278e2d1f6a7b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 11 Nov 2018 16:06:06 +0200 Subject: [PATCH 057/101] Fix minor things in management API --- maubot/management/api/middleware.py | 1 - maubot/server.py | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/maubot/management/api/middleware.py b/maubot/management/api/middleware.py index 79bc26b..f58dcd8 100644 --- a/maubot/management/api/middleware.py +++ b/maubot/management/api/middleware.py @@ -39,7 +39,6 @@ async def error(request: web.Request, handler: Handler) -> web.Response: try: return await handler(request) except web.HTTPException as ex: - print(ex) if ex.status_code == 404: return resp.path_not_found elif ex.status_code == 405: diff --git a/maubot/server.py b/maubot/server.py index 06f1c4d..827fd7e 100644 --- a/maubot/server.py +++ b/maubot/server.py @@ -61,7 +61,7 @@ class MaubotServer: index_html = file.read() @web.middleware - async def frontend_404_middleware(request, handler): + async def frontend_404_middleware(request: web.Request, handler) -> web.Response: if hasattr(handler, "__self__") and isinstance(handler.__self__, web.StaticResource): try: return await handler(request) @@ -69,10 +69,13 @@ class MaubotServer: return web.Response(body=index_html, content_type="text/html") return await handler(request) + async def ui_base_redirect(_: web.Request) -> web.Response: + raise web.HTTPFound(f"{ui_base}/") + self.app.middlewares.append(frontend_404_middleware) self.app.router.add_get(f"{ui_base}/", lambda _: web.Response(body=index_html, content_type="text/html")) - self.app.router.add_get(ui_base, lambda _: web.HTTPFound(f"{ui_base}/")) + self.app.router.add_get(ui_base, ui_base_redirect) def setup_static_root_files(self, directory: str, ui_base: str) -> None: files = { From 303a9827a1ee3aa660c545ad41be8fe25db6564c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 11 Nov 2018 19:27:09 +0200 Subject: [PATCH 058/101] Add log websocket --- maubot/__main__.py | 4 +- maubot/management/api/__init__.py | 8 +- maubot/management/api/base.py | 11 ++ maubot/management/api/log.py | 114 ++++++++++++++++++ maubot/management/frontend/src/api.js | 55 ++++++++- .../frontend/src/pages/dashboard/index.js | 3 + 6 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 maubot/management/api/log.py diff --git a/maubot/__main__.py b/maubot/__main__.py index e970c50..ccfceb8 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -26,7 +26,7 @@ from .server import MaubotServer from .client import Client, init as init_client_class from .loader.zip import init as init_zip_loader from .instance import init as init_plugin_instance_class -from .management.api import init as init_management_api +from .management.api import init as init_management_api, stop as stop_management_api from .__meta__ import __version__ parser = argparse.ArgumentParser(description="A plugin-based Matrix bot system.", @@ -87,6 +87,8 @@ except KeyboardInterrupt: loop.run_until_complete(asyncio.gather(*[client.stop() for client in Client.cache.values()], loop=loop)) db_session.commit() + log.debug("Closing websockets") + loop.run_until_complete(stop_management_api()) log.debug("Stopping server") loop.run_until_complete(server.stop()) log.debug("Closing event loop") diff --git a/maubot/management/api/__init__.py b/maubot/management/api/__init__.py index d8d1917..70f6e4b 100644 --- a/maubot/management/api/__init__.py +++ b/maubot/management/api/__init__.py @@ -17,16 +17,22 @@ from aiohttp import web from asyncio import AbstractEventLoop from ...config import Config -from .base import routes, set_config +from .base import routes, set_config, set_loop from .middleware import auth, error from .auth import web as _ from .plugin import web as _ from .instance import web as _ from .client import web as _ +from .log import stop_all as stop_log_sockets def init(cfg: Config, loop: AbstractEventLoop) -> web.Application: set_config(cfg) + set_loop(loop) app = web.Application(loop=loop, middlewares=[auth, error]) app.add_routes(routes) return app + + +async def stop() -> None: + await stop_log_sockets() diff --git a/maubot/management/api/base.py b/maubot/management/api/base.py index 4cf9636..c1e80e3 100644 --- a/maubot/management/api/base.py +++ b/maubot/management/api/base.py @@ -14,12 +14,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from aiohttp import web +import asyncio from ...__meta__ import __version__ from ...config import Config routes: web.RouteTableDef = web.RouteTableDef() _config: Config = None +_loop: asyncio.AbstractEventLoop = None def set_config(config: Config) -> None: @@ -31,6 +33,15 @@ def get_config() -> Config: return _config +def set_loop(loop: asyncio.AbstractEventLoop) -> None: + global _loop + _loop = loop + + +def get_loop() -> asyncio.AbstractEventLoop: + return _loop + + @routes.get("/version") async def version(_: web.Request) -> web.Response: return web.json_response({ diff --git a/maubot/management/api/log.py b/maubot/management/api/log.py new file mode 100644 index 0000000..8700629 --- /dev/null +++ b/maubot/management/api/log.py @@ -0,0 +1,114 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from datetime import datetime +import logging +import asyncio + +from aiohttp import web + +from .base import routes, get_loop +from .auth import is_valid_token + +BUILTIN_ATTRS = {"args", "asctime", "created", "exc_info", "exc_text", "filename", "funcName", + "levelname", "levelno", "lineno", "module", "msecs", "message", "msg", "name", + "pathname", "process", "processName", "relativeCreated", "stack_info", "thread", + "threadName"} +INCLUDE_ATTRS = {"filename", "funcName", "levelname", "levelno", "lineno", "module", "name", + "pathname"} +EXCLUDE_ATTRS = BUILTIN_ATTRS - INCLUDE_ATTRS + + +class WebSocketHandler(logging.Handler): + def __init__(self, ws, level=logging.NOTSET) -> None: + super().__init__(level) + self.ws = ws + self.formatter = logging.Formatter() + + def emit(self, record: logging.LogRecord) -> None: + # JSON conversion based on Marsel Mavletkulov's json-log-formatter (MIT license) + # https://github.com/marselester/json-log-formatter + content = { + name: value + for name, value in record.__dict__.items() + if name not in EXCLUDE_ATTRS + } + content["msg"] = record.getMessage() + content["time"] = datetime.utcnow() + + if record.exc_info: + content["exc_info"] = self.formatter.formatException(record.exc_info) + + for name, value in content.items(): + if isinstance(value, datetime): + content[name] = value.astimezone().isoformat() + + asyncio.ensure_future(self.send(content), loop=get_loop()) + + async def send(self, record: dict) -> None: + try: + await self.ws.send_json(record) + except Exception as e: + pass + + +log_root = logging.getLogger("maubot") +log = logging.getLogger("maubot.server.websocket") +sockets = [] + + +async def stop_all() -> None: + for socket in sockets: + try: + await socket.close(1012) + except Exception: + pass + + +@routes.get("/logs") +async def log_websocket(request: web.Request) -> web.WebSocketResponse: + ws = web.WebSocketResponse() + await ws.prepare(request) + sockets.append(ws) + log.debug(f"Connection from {request.remote} opened") + handler = WebSocketHandler(ws) + authenticated = False + + async def close_if_not_authenticated(): + await asyncio.sleep(5, loop=get_loop()) + if not authenticated: + await ws.close(code=4000) + log.debug(f"Connection from {request.remote} terminated due to no authentication") + + asyncio.ensure_future(close_if_not_authenticated()) + + try: + async for msg in ws: + if msg.type != web.WSMsgType.TEXT: + continue + if is_valid_token(msg.data): + if not authenticated: + log.debug(f"Connection from {request.remote} authenticated") + log_root.addHandler(handler) + authenticated = True + await ws.send_json({"auth_success": True}) + elif not authenticated: + await ws.send_json({"auth_success": False}) + except Exception: + pass + log_root.removeHandler(handler) + log.debug(f"Connection from {request.remote} closed") + sockets.remove(ws) + return ws diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index 8d7d9b6..fc4e518 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -75,6 +75,59 @@ export async function ping() { throw json } +export async function openLogSocket() { + let protocol = window.location.protocol === "https:" ? "wss:" : "ws:" + const url = `${protocol}//${window.location.host}${BASE_PATH}/logs` + const wrapper = { + socket: null, + connected: false, + authenticated: false, + fails: -1, + } + const openHandler = () => { + wrapper.socket.send(localStorage.accessToken) + wrapper.connected = true + } + const messageHandler = evt => { + // TODO use logs + const data = JSON.parse(evt.data) + if (data.auth_success !== undefined) { + if (data.auth_success) { + console.info("Websocket connection authentication successful") + wrapper.authenticated = true + wrapper.fails = -1 + } else { + console.info("Websocket connection authentication failed") + } + } else { + console.log("SERVLOG", data) + } + } + const closeHandler = evt => { + if (evt) { + if (evt.code === 4000) { + console.error("Websocket connection failed: access token invalid or not provided") + } else if (evt.code === 1012) { + console.info("Websocket connection closed: server is restarting") + } + } + wrapper.connected = false + wrapper.socket = null + wrapper.fails++ + const SECOND = 1000 + setTimeout(() => { + wrapper.socket = new WebSocket(url) + wrapper.socket.onopen = openHandler + wrapper.socket.onmessage = messageHandler + wrapper.socket.onclose = closeHandler + }, Math.min(wrapper.fails * 5 * SECOND, 30 * SECOND)) + } + + closeHandler() + + return wrapper +} + export const getInstances = () => defaultGet("/instances") export const getInstance = id => defaultGet(`/instance/${id}`) export const putInstance = (instance, id) => defaultPut("instance", instance, id) @@ -123,7 +176,7 @@ export const deleteClient = id => defaultDelete("client", id) export default { BASE_PATH, - login, ping, + login, ping, openLogSocket, getInstances, getInstance, putInstance, deleteInstance, getPlugins, getPlugin, uploadPlugin, deletePlugin, getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient, diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index 567c443..26b70fa 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -55,6 +55,9 @@ class Dashboard extends Component { plugins[plugin.id] = plugin } this.setState({ instances, clients, plugins }) + const logs = await api.openLogSocket() + console.log("WebSocket opened:", logs) + window.logs = logs } renderList(field, type) { From 91cdfc93780d2665fa5de74a0cc770b757a77641 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 11 Nov 2018 20:10:48 +0200 Subject: [PATCH 059/101] Fix websockets in dev server proxy --- maubot/management/frontend/package.json | 1 - maubot/management/frontend/src/setupProxy.js | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 maubot/management/frontend/src/setupProxy.js diff --git a/maubot/management/frontend/package.json b/maubot/management/frontend/package.json index 320679b..29e19e0 100644 --- a/maubot/management/frontend/package.json +++ b/maubot/management/frontend/package.json @@ -25,7 +25,6 @@ "last 2 safari versions", "last 2 ios_saf versions" ], - "proxy": "http://localhost:29316", "homepage": ".", "devDependencies": { "sass-lint": "^1.12.1", diff --git a/maubot/management/frontend/src/setupProxy.js b/maubot/management/frontend/src/setupProxy.js new file mode 100644 index 0000000..666772d --- /dev/null +++ b/maubot/management/frontend/src/setupProxy.js @@ -0,0 +1,6 @@ +const proxy = require("http-proxy-middleware") + +module.exports = function(app) { + app.use(proxy("/_matrix/maubot/v1", { target: "http://localhost:29316" })) + app.use(proxy("/_matrix/maubot/v1/logs", { target: "http://localhost:29316", ws: true })) +} From 5c5b3568cde18b057656f5a5f9dc151f0a069575 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 11 Nov 2018 20:21:51 +0200 Subject: [PATCH 060/101] Add animations to sidebar toggling --- .../management/frontend/src/style/pages/dashboard-grid.css | 7 ------- maubot/management/frontend/src/style/pages/dashboard.sass | 7 ++++--- maubot/management/frontend/src/style/pages/topbar.sass | 3 ++- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/maubot/management/frontend/src/style/pages/dashboard-grid.css b/maubot/management/frontend/src/style/pages/dashboard-grid.css index 516c7ff..5f12093 100644 --- a/maubot/management/frontend/src/style/pages/dashboard-grid.css +++ b/maubot/management/frontend/src/style/pages/dashboard-grid.css @@ -9,13 +9,6 @@ @media screen and (max-width: 35rem) { .dashboard { - grid-template: - [row1-start] "topbar" 3.5rem [row1-end] - [row2-start] "main" auto [row2-end] - / auto; - } - - .dashboard.sidebar-open { grid-template: [row1-start] "title topbar" 3.5rem [row1-end] [row2-start] "user main" 2.5rem [row2-end] diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index a5b82bf..31f15bb 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -63,9 +63,10 @@ @import topbar @media screen and (max-width: 35rem) - &:not(.sidebar-open) - > nav.sidebar, > a.title, > div.user - display: none !important + &:not(.sidebar-open) > * + transform: translateX(-15rem) + > * + transition: transform 0.4s > main.view grid-area: main diff --git a/maubot/management/frontend/src/style/pages/topbar.sass b/maubot/management/frontend/src/style/pages/topbar.sass index e7d8312..c241974 100644 --- a/maubot/management/frontend/src/style/pages/topbar.sass +++ b/maubot/management/frontend/src/style/pages/topbar.sass @@ -47,7 +47,7 @@ transform-origin: 4px 0 - //transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0), opacity 0.55s ease + transition: transform 0.4s cubic-bezier(0.77, 0.2, 0.05, 1.0), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0), opacity 0.55s ease &:nth-of-type(1) transform-origin: 0 0 @@ -56,6 +56,7 @@ transform-origin: 0 100% transform: translateY(2px) + transition: transform 0.4s cubic-bezier(0.77, 0.2, 0.05, 1.0) &.active transform: translateX(1px) translateY(4px) From 20a9ca6aa6633680d9000388b70189fa22b21b07 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 11 Nov 2018 20:24:59 +0200 Subject: [PATCH 061/101] Make both instance dropdowns behave the same way --- maubot/management/frontend/src/pages/dashboard/Instance.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maubot/management/frontend/src/pages/dashboard/Instance.js b/maubot/management/frontend/src/pages/dashboard/Instance.js index 2c2f825..8f1ed42 100644 --- a/maubot/management/frontend/src/pages/dashboard/Instance.js +++ b/maubot/management/frontend/src/pages/dashboard/Instance.js @@ -145,7 +145,7 @@ class Instance extends BaseMainView { this.setState({ primary_user: id })}/> - this.setState({ type: id })}/> From 6f2162d5f34134c2342c6e05b5a616ea7ef6f758 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 11 Nov 2018 21:20:50 +0200 Subject: [PATCH 062/101] Add some backup checks when stopping maubot --- maubot/__main__.py | 5 ++++- maubot/management/api/log.py | 7 +++++-- maubot/server.py | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/maubot/__main__.py b/maubot/__main__.py index ccfceb8..95118a7 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -90,7 +90,10 @@ except KeyboardInterrupt: log.debug("Closing websockets") loop.run_until_complete(stop_management_api()) log.debug("Stopping server") - loop.run_until_complete(server.stop()) + try: + loop.run_until_complete(asyncio.wait_for(server.stop(), 5, loop=loop)) + except asyncio.TimeoutError: + log.warning("Stopping server timed out") log.debug("Closing event loop") loop.close() log.debug("Everything stopped, shutting down") diff --git a/maubot/management/api/log.py b/maubot/management/api/log.py index 8700629..467f16e 100644 --- a/maubot/management/api/log.py +++ b/maubot/management/api/log.py @@ -72,7 +72,7 @@ sockets = [] async def stop_all() -> None: for socket in sockets: try: - await socket.close(1012) + await socket.close(code=1012) except Exception: pass @@ -107,7 +107,10 @@ async def log_websocket(request: web.Request) -> web.WebSocketResponse: elif not authenticated: await ws.send_json({"auth_success": False}) except Exception: - pass + try: + await ws.close() + except Exception: + pass log_root.removeHandler(handler) log.debug(f"Connection from {request.remote} closed") sockets.remove(ws) diff --git a/maubot/server.py b/maubot/server.py index 827fd7e..deb17e6 100644 --- a/maubot/server.py +++ b/maubot/server.py @@ -99,6 +99,7 @@ class MaubotServer: self.log.info(f"Listening on {site.name}") async def stop(self) -> None: + await self.runner.shutdown() await self.runner.cleanup() @staticmethod From c2a54514018dd3630604e93cb2886a6bbb81a884 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 12 Nov 2018 00:00:37 +0200 Subject: [PATCH 063/101] Finish basic log viewer --- maubot/management/api/log.py | 9 ++++- maubot/management/frontend/src/api.js | 3 ++ .../frontend/src/pages/dashboard/Client.js | 18 ++++++---- .../frontend/src/pages/dashboard/Home.js | 30 +++++++++++++++++ .../frontend/src/pages/dashboard/Instance.js | 2 ++ .../frontend/src/pages/dashboard/Log.js | 29 ++++++++++++++++ .../frontend/src/pages/dashboard/Plugin.js | 2 ++ .../frontend/src/pages/dashboard/index.js | 15 +++++++-- .../frontend/src/style/pages/dashboard.sass | 33 +++++++++++++++---- 9 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 maubot/management/frontend/src/pages/dashboard/Home.js create mode 100644 maubot/management/frontend/src/pages/dashboard/Log.js diff --git a/maubot/management/api/log.py b/maubot/management/api/log.py index 467f16e..572bf87 100644 --- a/maubot/management/api/log.py +++ b/maubot/management/api/log.py @@ -38,6 +38,12 @@ class WebSocketHandler(logging.Handler): self.formatter = logging.Formatter() def emit(self, record: logging.LogRecord) -> None: + try: + self._emit(record) + except Exception as e: + print("Logging error:", e) + + def _emit(self, record: logging.LogRecord) -> None: # JSON conversion based on Marsel Mavletkulov's json-log-formatter (MIT license) # https://github.com/marselester/json-log-formatter content = { @@ -45,6 +51,7 @@ class WebSocketHandler(logging.Handler): for name, value in record.__dict__.items() if name not in EXCLUDE_ATTRS } + content["id"] = record.relativeCreated content["msg"] = record.getMessage() content["time"] = datetime.utcnow() @@ -61,7 +68,7 @@ class WebSocketHandler(logging.Handler): try: await self.ws.send_json(record) except Exception as e: - pass + print("Log sending error:", e) log_root = logging.getLogger("maubot") diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index fc4e518..0992233 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -82,6 +82,7 @@ export async function openLogSocket() { socket: null, connected: false, authenticated: false, + onLog: data => {}, fails: -1, } const openHandler = () => { @@ -100,7 +101,9 @@ export async function openLogSocket() { console.info("Websocket connection authentication failed") } } else { + data.time = new Date(data.time) console.log("SERVLOG", data) + wrapper.onLog(data) } } const closeHandler = evt => { diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index 0424911..45bf10e 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -21,6 +21,7 @@ import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTab import Spinner from "../../components/Spinner" import api from "../../api" import BaseMainView from "./BaseMainView" +import Log from "./Log" const ClientListEntry = ({ entry }) => { const classes = ["client", "entry"] @@ -200,14 +201,17 @@ class Client extends BaseMainView { render() { - return
- {this.renderSidebar()} -
- {this.renderPreferences()} - {this.renderPrefButtons()} - {this.renderInstances()} + return <> +
+ {this.renderSidebar()} +
+ {this.renderPreferences()} + {this.renderPrefButtons()} + {this.renderInstances()} +
-
+ + } } diff --git a/maubot/management/frontend/src/pages/dashboard/Home.js b/maubot/management/frontend/src/pages/dashboard/Home.js new file mode 100644 index 0000000..f1a4fad --- /dev/null +++ b/maubot/management/frontend/src/pages/dashboard/Home.js @@ -0,0 +1,30 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component } from "react" +import Log from "./Log" + +class Home extends Component { + render() { + return <> +
+ See sidebar to get started +
+ + + } +} + +export default Home diff --git a/maubot/management/frontend/src/pages/dashboard/Instance.js b/maubot/management/frontend/src/pages/dashboard/Instance.js index 8f1ed42..58463be 100644 --- a/maubot/management/frontend/src/pages/dashboard/Instance.js +++ b/maubot/management/frontend/src/pages/dashboard/Instance.js @@ -23,6 +23,7 @@ import PrefTable, { PrefInput, PrefSelect, PrefSwitch } from "../../components/P import api from "../../api" import Spinner from "../../components/Spinner" import BaseMainView from "./BaseMainView" +import Log from "./Log" const InstanceListEntry = ({ entry }) => ( @@ -167,6 +168,7 @@ class Instance extends BaseMainView {
{this.state.error}
+
} } diff --git a/maubot/management/frontend/src/pages/dashboard/Log.js b/maubot/management/frontend/src/pages/dashboard/Log.js new file mode 100644 index 0000000..9b1929c --- /dev/null +++ b/maubot/management/frontend/src/pages/dashboard/Log.js @@ -0,0 +1,29 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react" + +const Log = ({ lines, showName = true }) =>
+ {lines.map(data => +
+ {data.time.toLocaleTimeString()} + {data.levelname} + {showName && {data.name}} + {data.msg} +
, + )} +
+ +export default Log diff --git a/maubot/management/frontend/src/pages/dashboard/Plugin.js b/maubot/management/frontend/src/pages/dashboard/Plugin.js index aac38cf..4ca7f6a 100644 --- a/maubot/management/frontend/src/pages/dashboard/Plugin.js +++ b/maubot/management/frontend/src/pages/dashboard/Plugin.js @@ -21,6 +21,7 @@ import PrefTable, { PrefInput } from "../../components/PreferenceTable" import Spinner from "../../components/Spinner" import api from "../../api" import BaseMainView from "./BaseMainView" +import Log from "./Log" const PluginListEntry = ({ entry }) => ( @@ -90,6 +91,7 @@ class Plugin extends BaseMainView {
}
{this.state.error}
{!this.isNew && this.renderInstances()} +
} } diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index 26b70fa..232923e 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -20,6 +20,7 @@ import { ReactComponent as Plus } from "../../res/plus.svg" import Instance from "./Instance" import Client from "./Client" import Plugin from "./Plugin" +import Home from "./Home" class Dashboard extends Component { constructor(props) { @@ -30,6 +31,8 @@ class Dashboard extends Component { plugins: {}, sidebarOpen: false, } + this.logLines = [] + this.logMap = {} window.maubot = this } @@ -55,9 +58,13 @@ class Dashboard extends Component { plugins[plugin.id] = plugin } this.setState({ instances, clients, plugins }) + const logs = await api.openLogSocket() - console.log("WebSocket opened:", logs) - window.logs = logs + logs.onLog = data => { + this.logLines.push(data) + ;(this.logMap[data.name] || (this.logMap[data.name] = [])).push(data) + this.setState({}) + } } renderList(field, type) { @@ -85,11 +92,13 @@ class Dashboard extends Component { if (!entry) { return this.renderNotFound(field.slice(0, -1)) } + console.log(`maubot.${field.slice(0, -1)}.${id}`) return React.createElement(type, { entry, onDelete: () => this.delete(field, id), onChange: newEntry => this.add(field, newEntry, id), ctx: this.state, + log: this.logMap[`maubot.${field.slice(0, -1)}.${id}`] || [], }) } @@ -142,7 +151,7 @@ class Dashboard extends Component {
- "Hello, World!"}/> + }/> this.add("instances", newEntry)} ctx={this.state}/>}/> diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 31f15bb..3ab03fa 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -78,17 +78,38 @@ @import instance @import plugin - > .not-found - text-align: center - margin-top: 5rem - font-size: 1.5rem - - > div:not(.not-found) + > div margin: 2rem 4rem @media screen and (max-width: 50rem) margin: 2rem 1rem + div.log > div.row + span.level, span.logger + display: none + + > div.not-found, > div.home + text-align: center + margin-top: 5rem + font-size: 1.5rem + + div.log + text-align: left + font-size: 12px + max-height: 20rem + font-family: "Fira Code", monospace + overflow: auto + + > div.row + white-space: nowrap + + > span.level:before + content: " [" + > span.logger:before + content: "@" + > span.text:before + content: "] " + div.buttons +button-group display: flex From d5cb73898f2259810b6cfe1f845fc5130ff0af46 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 12 Nov 2018 00:04:11 +0200 Subject: [PATCH 064/101] Update version --- maubot/__meta__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maubot/__meta__.py b/maubot/__meta__.py index 11b122d..5ad3775 100644 --- a/maubot/__meta__.py +++ b/maubot/__meta__.py @@ -1 +1 @@ -__version__ = "0.1.0.dev6+management" +__version__ = "0.1.0.dev7" From 08eacce0a9685e8408622f6a4138b3a74ad502a9 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 12 Nov 2018 00:13:44 +0200 Subject: [PATCH 065/101] Fix log map entry for instances and plugins --- .../management/frontend/src/pages/dashboard/index.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index 232923e..ec4ad7d 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -87,6 +87,16 @@ class Dashboard extends Component { this.setState({ [stateField]: data }) } + getLog(field, id) { + if (field === "clients") { + return this.logMap[`maubot.client.${id}`] + } else if (field === "instances") { + return this.logMap[`maubot.plugin.${id}`] + } else if (field === "plugins") { + return this.logMap["maubot.loader.zip"] + } + } + renderView(field, type, id) { const entry = this.state[field][id] if (!entry) { @@ -98,7 +108,7 @@ class Dashboard extends Component { onDelete: () => this.delete(field, id), onChange: newEntry => this.add(field, newEntry, id), ctx: this.state, - log: this.logMap[`maubot.${field.slice(0, -1)}.${id}`] || [], + log: this.getLog(field, id) || [], }) } From a3dea42b1a64a33cb610c0f2215beef7ff8bf56b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 12 Nov 2018 00:17:31 +0200 Subject: [PATCH 066/101] Show log level in filtered logs --- maubot/management/frontend/src/style/pages/dashboard.sass | 4 ---- 1 file changed, 4 deletions(-) diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 3ab03fa..5a6c74e 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -84,10 +84,6 @@ @media screen and (max-width: 50rem) margin: 2rem 1rem - div.log > div.row - span.level, span.logger - display: none - > div.not-found, > div.home text-align: center margin-top: 5rem From 52811681f447a505b0b35579a962403c91a4f4cc Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 14 Nov 2018 00:06:03 +0200 Subject: [PATCH 067/101] Fix new plugin/instance/client pages --- .../frontend/src/pages/dashboard/BaseMainView.js | 3 +++ .../frontend/src/pages/dashboard/Client.js | 5 ++--- .../frontend/src/pages/dashboard/Instance.js | 3 +-- .../frontend/src/pages/dashboard/Plugin.js | 14 +++++++++----- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/maubot/management/frontend/src/pages/dashboard/BaseMainView.js b/maubot/management/frontend/src/pages/dashboard/BaseMainView.js index 172a302..de4d4e4 100644 --- a/maubot/management/frontend/src/pages/dashboard/BaseMainView.js +++ b/maubot/management/frontend/src/pages/dashboard/BaseMainView.js @@ -1,5 +1,6 @@ import React, { Component } from "react" import { Link } from "react-router-dom" +import Log from "./Log" class BaseMainView extends Component { constructor(props) { @@ -63,6 +64,8 @@ class BaseMainView extends Component { ))} ) + + renderLog = () => !this.isNew && } export default BaseMainView diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index 45bf10e..0f2b3e7 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -21,7 +21,6 @@ import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTab import Spinner from "../../components/Spinner" import api from "../../api" import BaseMainView from "./BaseMainView" -import Log from "./Log" const ClientListEntry = ({ entry }) => { const classes = ["client", "entry"] @@ -158,7 +157,7 @@ class Client extends BaseMainView { renderPreferences = () => ( - + {this.renderLog()} } } diff --git a/maubot/management/frontend/src/pages/dashboard/Instance.js b/maubot/management/frontend/src/pages/dashboard/Instance.js index 58463be..4b4a736 100644 --- a/maubot/management/frontend/src/pages/dashboard/Instance.js +++ b/maubot/management/frontend/src/pages/dashboard/Instance.js @@ -23,7 +23,6 @@ import PrefTable, { PrefInput, PrefSelect, PrefSwitch } from "../../components/P import api from "../../api" import Spinner from "../../components/Spinner" import BaseMainView from "./BaseMainView" -import Log from "./Log" const InstanceListEntry = ({ entry }) => ( @@ -168,7 +167,7 @@ class Instance extends BaseMainView {
{this.state.error}
- + {this.renderLog()} } } diff --git a/maubot/management/frontend/src/pages/dashboard/Plugin.js b/maubot/management/frontend/src/pages/dashboard/Plugin.js index 4ca7f6a..e3f54d8 100644 --- a/maubot/management/frontend/src/pages/dashboard/Plugin.js +++ b/maubot/management/frontend/src/pages/dashboard/Plugin.js @@ -14,14 +14,13 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React from "react" -import { NavLink } from "react-router-dom" +import { NavLink, withRouter } from "react-router-dom" import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" import { ReactComponent as UploadButton } from "../../res/upload.svg" import PrefTable, { PrefInput } from "../../components/PreferenceTable" import Spinner from "../../components/Spinner" import api from "../../api" import BaseMainView from "./BaseMainView" -import Log from "./Log" const PluginListEntry = ({ entry }) => ( @@ -34,6 +33,11 @@ const PluginListEntry = ({ entry }) => ( class Plugin extends BaseMainView { static ListEntry = PluginListEntry + constructor(props) { + super(props) + this.deleteFunc = api.deletePlugin + } + get initialState() { return { id: "", @@ -90,10 +94,10 @@ class Plugin extends BaseMainView { }
{this.state.error}
- {!this.isNew && this.renderInstances()} - + {this.renderInstances()} + {this.renderLog()} } } -export default Plugin +export default withRouter(Plugin) From 034eead8403f32772bde2aa30f4fa4cebcee6dae Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 17 Nov 2018 01:11:12 +0200 Subject: [PATCH 068/101] Reload plugin metadata when reloading plugin --- maubot/loader/zip.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/maubot/loader/zip.py b/maubot/loader/zip.py index d014aeb..3f63f76 100644 --- a/maubot/loader/zip.py +++ b/maubot/loader/zip.py @@ -65,6 +65,7 @@ class ZippedPluginLoader(PluginLoader): self.id = None self._loaded = None self._importer = None + self._file = None self._load_meta() self._run_preload_checks(self._get_importer()) try: @@ -185,6 +186,7 @@ class ZippedPluginLoader(PluginLoader): def _load(self, reset_cache: bool = False) -> Type[PluginClass]: if self._loaded is not None and not reset_cache: return self._loaded + self._load_meta() importer = self._get_importer(reset_cache=reset_cache) self._run_preload_checks(importer) if reset_cache: From 73e86ebb173bff9e821280c26b5e84c7759c5688 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 27 Nov 2018 00:53:19 +0200 Subject: [PATCH 069/101] Hide config in new instances and add exceptions to log --- .../management/frontend/src/pages/dashboard/Instance.js | 3 ++- maubot/management/frontend/src/pages/dashboard/Log.js | 9 ++++++--- .../management/frontend/src/style/pages/dashboard.sass | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/maubot/management/frontend/src/pages/dashboard/Instance.js b/maubot/management/frontend/src/pages/dashboard/Instance.js index 4b4a736..37596de 100644 --- a/maubot/management/frontend/src/pages/dashboard/Instance.js +++ b/maubot/management/frontend/src/pages/dashboard/Instance.js @@ -149,12 +149,13 @@ class Instance extends BaseMainView { value={this.selectedPluginEntry} onChange={({ id }) => this.setState({ type: id })}/>
+ {!this.isNew && this.setState({ config })} name="config" value={this.state.config} editorProps={{ fontSize: "10pt", $blockScrolling: true, - }}/> + }}/>}
{!this.isNew && (
export default Log diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 5a6c74e..5568854 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -97,7 +97,7 @@ overflow: auto > div.row - white-space: nowrap + white-space: pre > span.level:before content: " [" From d252bfbb0884c8c3bf838be6fdb3943dac4d76c2 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 27 Nov 2018 02:35:44 +0200 Subject: [PATCH 070/101] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f0a3e46..a69d059 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ Matrix room: [#maubot:maunium.net](https://matrix.to/#/#maubot:maunium.net) * [karma](https://github.com/maubot/karma) - A user karma tracker bot. * [xkcd](https://github.com/maubot/xkcd) - A bot to view xkcd comics. * [echo](https://github.com/maubot/echo) - A bot that echoes pings and other stuff. +* [rss](https://github.com/maubot/rss) - A bot that posts RSS feed updates to Matrix. ### Upcoming -* rss - A bot that posts new RSS entries to rooms. * dictionary - A bot to get the dictionary definitions of words. * poll - A simple poll bot. * reminder - A bot to ping you about something after a certain amount of time. From 8cfe2e8884578cd7d3cb34c0ed0fbf195b23874d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 28 Nov 2018 02:33:37 +0200 Subject: [PATCH 071/101] "Fix" file upload accept list --- maubot/management/frontend/src/pages/dashboard/Plugin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maubot/management/frontend/src/pages/dashboard/Plugin.js b/maubot/management/frontend/src/pages/dashboard/Plugin.js index e3f54d8..b29ec2d 100644 --- a/maubot/management/frontend/src/pages/dashboard/Plugin.js +++ b/maubot/management/frontend/src/pages/dashboard/Plugin.js @@ -80,7 +80,7 @@ class Plugin extends BaseMainView { }
- evt.target.parentElement.classList.add("drag")} onDragLeave={evt => evt.target.parentElement.classList.remove("drag")}/> From 6a6e8a818e54cc5000cbe475b48a936e92a9cddc Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 28 Nov 2018 02:33:59 +0200 Subject: [PATCH 072/101] Simplify Event.reply and add MaubotMatrixClient.send_markdown --- maubot/matrix.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/maubot/matrix.py b/maubot/matrix.py index 1ac9167..7523369 100644 --- a/maubot/matrix.py +++ b/maubot/matrix.py @@ -13,14 +13,14 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, List, Union, Callable, Awaitable +from typing import Dict, List, Union, Callable, Awaitable, Optional import attr import commonmark from mautrix import Client as MatrixClient from mautrix.client import EventHandler from mautrix.types import (EventType, MessageEvent, Event, EventID, RoomID, MessageEventContent, - MessageType, TextMessageEventContent, Format) + MessageType, TextMessageEventContent, Format, RelatesTo) from .command_spec import ParsedCommand, CommandSpec @@ -35,24 +35,20 @@ class MaubotMessageEvent(MessageEvent): def respond(self, content: Union[str, MessageEventContent], event_type: EventType = EventType.ROOM_MESSAGE, - markdown: bool = True) -> Awaitable[EventID]: + markdown: bool = True, reply: bool = False) -> Awaitable[EventID]: if isinstance(content, str): content = TextMessageEventContent(msgtype=MessageType.NOTICE, body=content) if markdown: content.format = Format.HTML content.formatted_body = commonmark.commonmark(content.body) + if reply: + content.set_reply(self) return self._client.send_message_event(self.room_id, event_type, content) def reply(self, content: Union[str, MessageEventContent], event_type: EventType = EventType.ROOM_MESSAGE, markdown: bool = True) -> Awaitable[EventID]: - if isinstance(content, str): - content = TextMessageEventContent(msgtype=MessageType.NOTICE, body=content) - if markdown: - content.format = Format.HTML - content.formatted_body = commonmark.commonmark(content.body) - content.set_reply(self) - return self._client.send_message_event(self.room_id, event_type, content) + return self.respond(content, event_type, markdown, reply=True) def mark_read(self) -> Awaitable[None]: return self._client.send_receipt(self.room_id, self.event_id, "m.read") @@ -67,6 +63,14 @@ class MaubotMatrixClient(MatrixClient): self.add_event_handler(self._command_event_handler, EventType.ROOM_MESSAGE) + def send_markdown(self, room_id: RoomID, markdown: str, msgtype: MessageType = MessageType.TEXT, + relates_to: Optional[RelatesTo] = None, **kwargs) -> Awaitable[EventID]: + content = TextMessageEventContent(msgtype=msgtype, body=markdown, format=Format.HTML, + formatted_body=commonmark.commonmark(markdown)) + if relates_to: + content.relates_to = relates_to + return self.send_message(room_id, content, **kwargs) + def set_command_spec(self, plugin_id: str, spec: CommandSpec) -> None: self.command_specs[plugin_id] = spec self._reparse_command_specs() From c39cacbab491f0f95bfed17ad631aaeb4d2cbcc2 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 28 Nov 2018 15:28:34 +0200 Subject: [PATCH 073/101] Switch to Python-Markdown Commonmark doesn't have any built-in HTML sanitization --- maubot/matrix.py | 26 +++++++++++++++++++++----- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/maubot/matrix.py b/maubot/matrix.py index 7523369..c152af9 100644 --- a/maubot/matrix.py +++ b/maubot/matrix.py @@ -13,11 +13,13 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, List, Union, Callable, Awaitable, Optional +from typing import Dict, List, Union, Callable, Awaitable, Optional, Tuple +from markdown.extensions import Extension +import markdown as md import attr -import commonmark from mautrix import Client as MatrixClient +from mautrix.util.formatter import parse_html from mautrix.client import EventHandler from mautrix.types import (EventType, MessageEvent, Event, EventID, RoomID, MessageEventContent, MessageType, TextMessageEventContent, Format, RelatesTo) @@ -25,6 +27,20 @@ from mautrix.types import (EventType, MessageEvent, Event, EventID, RoomID, Mess from .command_spec import ParsedCommand, CommandSpec +class EscapeHTML(Extension): + def extendMarkdown(self, md): + md.preprocessors.deregister("html_block") + md.inlinePatterns.deregister("html") + + +escape_html = EscapeHTML() + + +def parse_markdown(markdown: str, allow_html: bool = False) -> Tuple[str, str]: + html = md.markdown(markdown, extensions=[escape_html] if not allow_html else []) + return parse_html(html), html + + class MaubotMessageEvent(MessageEvent): _client: MatrixClient @@ -40,7 +56,7 @@ class MaubotMessageEvent(MessageEvent): content = TextMessageEventContent(msgtype=MessageType.NOTICE, body=content) if markdown: content.format = Format.HTML - content.formatted_body = commonmark.commonmark(content.body) + content.body, content.formatted_body = parse_markdown(content.body) if reply: content.set_reply(self) return self._client.send_message_event(self.room_id, event_type, content) @@ -65,8 +81,8 @@ class MaubotMatrixClient(MatrixClient): def send_markdown(self, room_id: RoomID, markdown: str, msgtype: MessageType = MessageType.TEXT, relates_to: Optional[RelatesTo] = None, **kwargs) -> Awaitable[EventID]: - content = TextMessageEventContent(msgtype=msgtype, body=markdown, format=Format.HTML, - formatted_body=commonmark.commonmark(markdown)) + content = TextMessageEventContent(msgtype=msgtype, format=Format.HTML) + content.body, content.formatted_body = parse_markdown(markdown) if relates_to: content.relates_to = relates_to return self.send_message(room_id, content, **kwargs) diff --git a/requirements.txt b/requirements.txt index 2a918f9..ed45a6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ mautrix aiohttp SQLAlchemy alembic -commonmark +Markdown ruamel.yaml attrs bcrypt diff --git a/setup.py b/setup.py index 2995b45..7f92fa0 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setuptools.setup( "aiohttp>=3.0.1,<4", "SQLAlchemy>=1.2.3,<2", "alembic>=1.0.0,<2", - "commonmark>=0.8.1,<1", + "Markdown>=3.0.0,<4", "ruamel.yaml>=0.15.35,<0.16", "attrs>=18.1.0,<19", "bcrypt>=3.1.4,<4", From aac99b7ee47a91fe0bfff98b89b1a818cf645e13 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 28 Nov 2018 15:34:21 +0200 Subject: [PATCH 074/101] Update version --- maubot/__meta__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maubot/__meta__.py b/maubot/__meta__.py index 5ad3775..fb8c51a 100644 --- a/maubot/__meta__.py +++ b/maubot/__meta__.py @@ -1 +1 @@ -__version__ = "0.1.0.dev7" +__version__ = "0.1.0.dev8" From 32b60fa0ff501dbd779a5d2f976268feb2f3d8ac Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 29 Nov 2018 00:58:20 +0200 Subject: [PATCH 075/101] Overhaul log viewer * Move viewer to separate modal to allow using more horizontal space * Load log history (up to 2048 lines) * Add colors and use table styling for log viewer * Add links to log entries that open mentioned instance/client or line in code --- maubot/__main__.py | 7 +- maubot/instance.py | 4 +- maubot/management/api/__init__.py | 3 +- maubot/management/api/dev_open.py | 64 +++++++++ maubot/management/api/log.py | 47 ++++--- maubot/management/frontend/package.json | 1 + maubot/management/frontend/src/api.js | 24 +++- .../src/pages/dashboard/BaseMainView.js | 5 +- .../frontend/src/pages/dashboard/Client.js | 2 +- .../frontend/src/pages/dashboard/Home.js | 5 +- .../frontend/src/pages/dashboard/Instance.js | 2 +- .../frontend/src/pages/dashboard/Log.js | 124 ++++++++++++++++-- .../frontend/src/pages/dashboard/Modal.js | 51 +++++++ .../frontend/src/pages/dashboard/Plugin.js | 2 +- .../frontend/src/pages/dashboard/index.js | 71 +++++++--- .../management/frontend/src/style/index.sass | 2 + .../frontend/src/style/pages/dashboard.sass | 21 +-- .../frontend/src/style/pages/log.sass | 80 +++++++++++ .../frontend/src/style/pages/modal.sass | 71 ++++++++++ maubot/management/frontend/yarn.lock | 41 +++++- 20 files changed, 545 insertions(+), 82 deletions(-) create mode 100644 maubot/management/api/dev_open.py create mode 100644 maubot/management/frontend/src/pages/dashboard/Modal.js create mode 100644 maubot/management/frontend/src/style/pages/log.sass create mode 100644 maubot/management/frontend/src/style/pages/modal.sass diff --git a/maubot/__main__.py b/maubot/__main__.py index 95118a7..4d6d525 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -26,7 +26,7 @@ from .server import MaubotServer from .client import Client, init as init_client_class from .loader.zip import init as init_zip_loader from .instance import init as init_plugin_instance_class -from .management.api import init as init_management_api, stop as stop_management_api +from .management.api import init as init_mgmt_api, stop as stop_mgmt_api, init_log_listener from .__meta__ import __version__ parser = argparse.ArgumentParser(description="A plugin-based Matrix bot system.", @@ -43,6 +43,7 @@ config.load() config.update() logging.config.dictConfig(copy.deepcopy(config["logging"])) +init_log_listener() log = logging.getLogger("maubot.init") log.info(f"Initializing maubot {__version__}") @@ -52,7 +53,7 @@ init_zip_loader(config) db_session = init_db(config) clients = init_client_class(db_session, loop) plugins = init_plugin_instance_class(db_session, config, loop) -management_api = init_management_api(config, loop) +management_api = init_mgmt_api(config, loop) server = MaubotServer(config, loop) server.app.add_subapp(config["server.base_path"], management_api) @@ -88,7 +89,7 @@ except KeyboardInterrupt: loop=loop)) db_session.commit() log.debug("Closing websockets") - loop.run_until_complete(stop_management_api()) + loop.run_until_complete(stop_mgmt_api()) log.debug("Stopping server") try: loop.run_until_complete(asyncio.wait_for(server.stop(), 5, loop=loop)) diff --git a/maubot/instance.py b/maubot/instance.py index c797466..494ac4b 100644 --- a/maubot/instance.py +++ b/maubot/instance.py @@ -31,7 +31,7 @@ from .client import Client from .loader import PluginLoader from .plugin_base import Plugin -log = logging.getLogger("maubot.plugin") +log = logging.getLogger("maubot.instance") yaml = YAML() yaml.indent(4) @@ -54,7 +54,7 @@ class PluginInstance: def __init__(self, db_instance: DBPlugin): self.db_instance = db_instance - self.log = logging.getLogger(f"maubot.plugin.{self.id}") + self.log = log.getChild(self.id) self.config = None self.started = False self.loader = None diff --git a/maubot/management/api/__init__.py b/maubot/management/api/__init__.py index 70f6e4b..760299e 100644 --- a/maubot/management/api/__init__.py +++ b/maubot/management/api/__init__.py @@ -23,7 +23,8 @@ from .auth import web as _ from .plugin import web as _ from .instance import web as _ from .client import web as _ -from .log import stop_all as stop_log_sockets +from .dev_open import web as _ +from .log import stop_all as stop_log_sockets, init as init_log_listener def init(cfg: Config, loop: AbstractEventLoop) -> web.Application: diff --git a/maubot/management/api/dev_open.py b/maubot/management/api/dev_open.py new file mode 100644 index 0000000..1447edb --- /dev/null +++ b/maubot/management/api/dev_open.py @@ -0,0 +1,64 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from string import Template +from subprocess import run +import re + +from ruamel.yaml import YAML +from aiohttp import web + +from .base import routes + +enabled = False + + +@routes.get("/debug/open") +async def check_enabled(_: web.Request) -> web.Response: + return web.json_response({ + "enabled": enabled, + }) + + +try: + yaml = YAML() + + with open(".dev-open-cfg.yaml", "r") as file: + cfg = yaml.load(file) + editor_command = Template(cfg["editor"]) + pathmap = [(re.compile(item["find"]), item["replace"]) for item in cfg["pathmap"]] + + + @routes.post("/debug/open") + async def open_file(request: web.Request) -> web.Response: + data = await request.json() + try: + path = data["path"] + for find, replace in pathmap: + path = find.sub(replace, path) + cmd = editor_command.substitute(path=path, line=data["line"]) + except (KeyError, ValueError): + return web.Response(status=400) + res = run(cmd, shell=True) + return web.json_response({ + "return": res.returncode, + "stdout": res.stdout, + "stderr": res.stderr + }) + + + enabled = True +except Exception: + pass diff --git a/maubot/management/api/log.py b/maubot/management/api/log.py index 572bf87..93fe41d 100644 --- a/maubot/management/api/log.py +++ b/maubot/management/api/log.py @@ -13,13 +13,15 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import Deque, List from datetime import datetime +from collections import deque import logging import asyncio from aiohttp import web -from .base import routes, get_loop +from .base import routes, get_loop, get_config from .auth import is_valid_token BUILTIN_ATTRS = {"args", "asctime", "created", "exc_info", "exc_text", "filename", "funcName", @@ -29,13 +31,19 @@ BUILTIN_ATTRS = {"args", "asctime", "created", "exc_info", "exc_text", "filename INCLUDE_ATTRS = {"filename", "funcName", "levelname", "levelno", "lineno", "module", "name", "pathname"} EXCLUDE_ATTRS = BUILTIN_ATTRS - INCLUDE_ATTRS +MAX_LINES = 2048 -class WebSocketHandler(logging.Handler): - def __init__(self, ws, level=logging.NOTSET) -> None: +class LogCollector(logging.Handler): + lines: Deque[dict] + formatter: logging.Formatter + listeners: List[web.WebSocketResponse] + + def __init__(self, level=logging.NOTSET) -> None: super().__init__(level) - self.ws = ws + self.lines = deque(maxlen=MAX_LINES) self.formatter = logging.Formatter() + self.listeners = [] def emit(self, record: logging.LogRecord) -> None: try: @@ -51,9 +59,9 @@ class WebSocketHandler(logging.Handler): for name, value in record.__dict__.items() if name not in EXCLUDE_ATTRS } - content["id"] = record.relativeCreated + content["id"] = str(record.relativeCreated) content["msg"] = record.getMessage() - content["time"] = datetime.utcnow() + content["time"] = datetime.fromtimestamp(record.created) if record.exc_info: content["exc_info"] = self.formatter.formatException(record.exc_info) @@ -61,22 +69,29 @@ class WebSocketHandler(logging.Handler): for name, value in content.items(): if isinstance(value, datetime): content[name] = value.astimezone().isoformat() - - asyncio.ensure_future(self.send(content), loop=get_loop()) + asyncio.ensure_future(self.send(content)) + self.lines.append(content) async def send(self, record: dict) -> None: - try: - await self.ws.send_json(record) - except Exception as e: - print("Log sending error:", e) + for ws in self.listeners: + try: + await ws.send_json(record) + except Exception as e: + print("Log sending error:", e) +handler = LogCollector() log_root = logging.getLogger("maubot") log = logging.getLogger("maubot.server.websocket") sockets = [] +def init() -> None: + log_root.addHandler(handler) + + async def stop_all() -> None: + log_root.removeHandler(handler) for socket in sockets: try: await socket.close(code=1012) @@ -90,7 +105,6 @@ async def log_websocket(request: web.Request) -> web.WebSocketResponse: await ws.prepare(request) sockets.append(ws) log.debug(f"Connection from {request.remote} opened") - handler = WebSocketHandler(ws) authenticated = False async def close_if_not_authenticated(): @@ -106,11 +120,12 @@ async def log_websocket(request: web.Request) -> web.WebSocketResponse: if msg.type != web.WSMsgType.TEXT: continue if is_valid_token(msg.data): + await ws.send_json({"auth_success": True}) + await ws.send_json({"history": list(handler.lines)}) if not authenticated: log.debug(f"Connection from {request.remote} authenticated") - log_root.addHandler(handler) + handler.listeners.append(ws) authenticated = True - await ws.send_json({"auth_success": True}) elif not authenticated: await ws.send_json({"auth_success": False}) except Exception: @@ -118,7 +133,7 @@ async def log_websocket(request: web.Request) -> web.WebSocketResponse: await ws.close() except Exception: pass - log_root.removeHandler(handler) + handler.listeners.remove(ws) log.debug(f"Connection from {request.remote} closed") sockets.remove(ws) return ws diff --git a/maubot/management/frontend/package.json b/maubot/management/frontend/package.json index 29e19e0..e754eef 100644 --- a/maubot/management/frontend/package.json +++ b/maubot/management/frontend/package.json @@ -7,6 +7,7 @@ "react": "^16.6.0", "react-ace": "^6.2.0", "react-dom": "^16.6.0", + "react-json-tree": "^0.11.0", "react-router-dom": "^4.3.1", "react-scripts": "2.0.5", "react-select": "^2.1.1" diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index 0992233..49465af 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -82,7 +82,8 @@ export async function openLogSocket() { socket: null, connected: false, authenticated: false, - onLog: data => {}, + onLog: data => undefined, + onHistory: history => undefined, fails: -1, } const openHandler = () => { @@ -100,9 +101,9 @@ export async function openLogSocket() { } else { console.info("Websocket connection authentication failed") } + } else if (data.history) { + wrapper.onHistory(data.history) } else { - data.time = new Date(data.time) - console.log("SERVLOG", data) wrapper.onLog(data) } } @@ -131,6 +132,21 @@ export async function openLogSocket() { return wrapper } +let _debugOpenFileEnabled = undefined +export const debugOpenFileEnabled = () => _debugOpenFileEnabled +export const updateDebugOpenFileEnabled = async () => { + const resp = await defaultGet("/debug/open") + _debugOpenFileEnabled = resp["enabled"] || false +} +export async function debugOpenFile(path, line) { + const resp = await fetch(`${BASE_PATH}/debug/open`, { + headers: getHeaders(), + body: JSON.stringify({ path, line }), + method: "POST", + }) + return await resp.json() +} + export const getInstances = () => defaultGet("/instances") export const getInstance = id => defaultGet(`/instance/${id}`) export const putInstance = (instance, id) => defaultPut("instance", instance, id) @@ -179,7 +195,7 @@ export const deleteClient = id => defaultDelete("client", id) export default { BASE_PATH, - login, ping, openLogSocket, + login, ping, openLogSocket, debugOpenFile, debugOpenFileEnabled, updateDebugOpenFileEnabled, getInstances, getInstance, putInstance, deleteInstance, getPlugins, getPlugin, uploadPlugin, deletePlugin, getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient, diff --git a/maubot/management/frontend/src/pages/dashboard/BaseMainView.js b/maubot/management/frontend/src/pages/dashboard/BaseMainView.js index de4d4e4..2ad72b1 100644 --- a/maubot/management/frontend/src/pages/dashboard/BaseMainView.js +++ b/maubot/management/frontend/src/pages/dashboard/BaseMainView.js @@ -1,6 +1,5 @@ import React, { Component } from "react" import { Link } from "react-router-dom" -import Log from "./Log" class BaseMainView extends Component { constructor(props) { @@ -65,7 +64,9 @@ class BaseMainView extends Component {
) - renderLog = () => !this.isNew && + renderLogButton = (filter) => !this.isNew &&
+ +
} export default BaseMainView diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index 0f2b3e7..e520c26 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -196,6 +196,7 @@ class Client extends BaseMainView { {this.state.saving ? : (this.isNew ? "Create" : "Save")} + {this.renderLogButton(this.state.id)}
{this.state.error}
@@ -209,7 +210,6 @@ class Client extends BaseMainView { {this.renderInstances()} - {this.renderLog()} } } diff --git a/maubot/management/frontend/src/pages/dashboard/Home.js b/maubot/management/frontend/src/pages/dashboard/Home.js index f1a4fad..8dc22b9 100644 --- a/maubot/management/frontend/src/pages/dashboard/Home.js +++ b/maubot/management/frontend/src/pages/dashboard/Home.js @@ -14,7 +14,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" -import Log from "./Log" class Home extends Component { render() { @@ -22,7 +21,9 @@ class Home extends Component {
See sidebar to get started
- +
+ +
} } diff --git a/maubot/management/frontend/src/pages/dashboard/Instance.js b/maubot/management/frontend/src/pages/dashboard/Instance.js index 37596de..5ea2d2b 100644 --- a/maubot/management/frontend/src/pages/dashboard/Instance.js +++ b/maubot/management/frontend/src/pages/dashboard/Instance.js @@ -167,8 +167,8 @@ class Instance extends BaseMainView { {this.state.saving ? : (this.isNew ? "Create" : "Save")} + {this.renderLogButton(`instance.${this.state.id}`)}
{this.state.error}
- {this.renderLog()} } } diff --git a/maubot/management/frontend/src/pages/dashboard/Log.js b/maubot/management/frontend/src/pages/dashboard/Log.js index 55b5b86..7e710f9 100644 --- a/maubot/management/frontend/src/pages/dashboard/Log.js +++ b/maubot/management/frontend/src/pages/dashboard/Log.js @@ -13,20 +13,116 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React from "react" +import React, { PureComponent } from "react" +import { Link } from "react-router-dom" +import JSONTree from "react-json-tree" +import api from "../../api" +import Modal from "./Modal" -const Log = ({ lines, showName = true }) =>
- {lines.map(data => <> -
- {data.time.toLocaleTimeString()} - {data.levelname} - {showName && {data.name}} - {data.msg} -
- {data.exc_info &&
- {data.exc_info.replace(/\\n/g, "\n")} -
} - )} -
+class LogEntry extends PureComponent { + static contextType = Modal.Context + + renderName() { + const line = this.props.line + if (line.nameLink) { + const modal = this.context + return ( + + {line.name} + + ) + } + return line.name + } + + renderContent() { + if (this.props.line.matrix_http_request) { + const req = this.props.line.matrix_http_request + + return <> + {req.method} {req.path} +
+ {Object.entries(req.content).length > 0 + && } +
+ + } + return this.props.line.msg + } + + renderTime() { + return this.props.line.time.toLocaleTimeString("en-GB") + } + + renderLevelName() { + return this.props.line.levelname + } + + get unfocused() { + return this.props.focus && this.props.line.name !== this.props.focus + ? "unfocused" + : "" + } + + renderRow(content) { + return ( +
+ {this.renderTime()} + {this.renderLevelName()} + {this.renderName()} + {content} +
+ ) + } + + renderExceptionInfo() { + if (!api.debugOpenFileEnabled()) { + return this.props.line.exc_info + } + const fileLinks = [] + let str = this.props.line.exc_info.replace( + /File "(.+)", line ([0-9]+), in (.+)/g, + (_, file, line, method) => { + fileLinks.push( + { + api.debugOpenFile(file, line) + return false + }}>File "{file}", line {line}, in {method}, + ) + return "||EDGE||" + }) + fileLinks.reverse() + + const result = [] + let key = 0 + for (const part of str.split("||EDGE||")) { + result.push( + {part} + {fileLinks.pop()} + ) + } + return result + } + + render() { + return <> + {this.renderRow(this.renderContent())} + {this.props.line.exc_info && this.renderRow(this.renderExceptionInfo())} + + } +} + +class Log extends PureComponent { + render() { + return ( +
+
+ {this.props.lines.map(data => )} +
+
+ ) + } +} export default Log diff --git a/maubot/management/frontend/src/pages/dashboard/Modal.js b/maubot/management/frontend/src/pages/dashboard/Modal.js new file mode 100644 index 0000000..9bfc48e --- /dev/null +++ b/maubot/management/frontend/src/pages/dashboard/Modal.js @@ -0,0 +1,51 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component, createContext } from "react" + +const rem = 16 + +class Modal extends Component { + static Context = createContext(null) + + constructor(props) { + super(props) + this.state = { + open: false, + } + this.wrapper = { clientWidth: 9001 } + } + + open = () => this.setState({ open: true }) + close = () => this.setState({ open: false }) + + render() { + return this.state.open && ( +
this.wrapper = ref} + onClick={() => this.wrapper.clientWidth > 45 * rem && this.close()}> +
evt.stopPropagation()}> + +
+ + {this.props.children} + +
+
+
+ ) + } +} + +export default Modal diff --git a/maubot/management/frontend/src/pages/dashboard/Plugin.js b/maubot/management/frontend/src/pages/dashboard/Plugin.js index b29ec2d..e3ea809 100644 --- a/maubot/management/frontend/src/pages/dashboard/Plugin.js +++ b/maubot/management/frontend/src/pages/dashboard/Plugin.js @@ -93,9 +93,9 @@ class Plugin extends BaseMainView { {this.state.deleting ? : "Delete"} } + {this.renderLogButton("loader.zip")}
{this.state.error}
{this.renderInstances()} - {this.renderLog()} } } diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index ec4ad7d..c042fe8 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -21,6 +21,8 @@ import Instance from "./Instance" import Client from "./Client" import Plugin from "./Plugin" import Home from "./Home" +import Log from "./Log" +import Modal from "./Modal" class Dashboard extends Component { constructor(props) { @@ -30,9 +32,14 @@ class Dashboard extends Component { clients: {}, plugins: {}, sidebarOpen: false, + modalOpen: false, + logFocus: "", } this.logLines = [] this.logMap = {} + this.logModal = { + open: () => undefined, + } window.maubot = this } @@ -44,7 +51,8 @@ class Dashboard extends Component { async componentWillMount() { const [instanceList, clientList, pluginList] = await Promise.all([ - api.getInstances(), api.getClients(), api.getPlugins()]) + api.getInstances(), api.getClients(), api.getPlugins(), + api.updateDebugOpenFileEnabled()]) const instances = {} for (const instance of instanceList) { instances[instance.id] = instance @@ -60,10 +68,32 @@ class Dashboard extends Component { this.setState({ instances, clients, plugins }) const logs = await api.openLogSocket() + + const processEntry = (entry) => { + entry.time = new Date(entry.time) + if (entry.name.startsWith("maubot.")) { + entry.name = entry.name.substr("maubot.".length) + } + if (entry.name.startsWith("client.")) { + entry.name = entry.name.substr("client.".length) + entry.nameLink = `/client/${entry.name}` + } else if (entry.name.startsWith("instance.")) { + entry.nameLink = `/instance/${entry.name.substr("instance.".length)}` + } + (this.logMap[entry.name] || (this.logMap[entry.name] = [])).push(entry) + } + + logs.onHistory = history => { + for (const data of history) { + processEntry(data) + } + this.logLines = history + this.setState({ logFocus: this.state.logFocus }) + } logs.onLog = data => { + processEntry(data) this.logLines.push(data) - ;(this.logMap[data.name] || (this.logMap[data.name] = [])).push(data) - this.setState({}) + this.setState({ logFocus: this.state.logFocus }) } } @@ -87,28 +117,22 @@ class Dashboard extends Component { this.setState({ [stateField]: data }) } - getLog(field, id) { - if (field === "clients") { - return this.logMap[`maubot.client.${id}`] - } else if (field === "instances") { - return this.logMap[`maubot.plugin.${id}`] - } else if (field === "plugins") { - return this.logMap["maubot.loader.zip"] - } - } - renderView(field, type, id) { const entry = this.state[field][id] if (!entry) { return this.renderNotFound(field.slice(0, -1)) } - console.log(`maubot.${field.slice(0, -1)}.${id}`) return React.createElement(type, { entry, onDelete: () => this.delete(field, id), onChange: newEntry => this.add(field, newEntry, id), + openLog: filter => { + this.setState({ + logFocus: filter, + }) + this.logModal.open() + }, ctx: this.state, - log: this.getLog(field, id) || [], }) } @@ -118,7 +142,7 @@ class Dashboard extends Component { ) - render() { + renderMain() { return
@@ -161,7 +185,7 @@ class Dashboard extends Component {
- }/> + }/> this.add("instances", newEntry)} ctx={this.state}/>}/> @@ -180,6 +204,19 @@ class Dashboard extends Component {
} + + renderModal() { + return this.logModal = ref}> + + + } + + render() { + return <> + {this.renderMain()} + {this.renderModal()} + + } } export default withRouter(Dashboard) diff --git a/maubot/management/frontend/src/style/index.sass b/maubot/management/frontend/src/style/index.sass index b496d8b..9c1e193 100644 --- a/maubot/management/frontend/src/style/index.sass +++ b/maubot/management/frontend/src/style/index.sass @@ -27,3 +27,5 @@ @import pages/login @import pages/dashboard +@import pages/modal +@import pages/log diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 5568854..d04cbba 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -89,29 +89,16 @@ margin-top: 5rem font-size: 1.5rem - div.log - text-align: left - font-size: 12px - max-height: 20rem - font-family: "Fira Code", monospace - overflow: auto - - > div.row - white-space: pre - - > span.level:before - content: " [" - > span.logger:before - content: "@" - > span.text:before - content: "] " - div.buttons +button-group display: flex margin: 1rem .5rem width: calc(100% - 1rem) + button.open-log + +button + +main-color-button + div.error +notification($error) margin: 1rem .5rem diff --git a/maubot/management/frontend/src/style/pages/log.sass b/maubot/management/frontend/src/style/pages/log.sass new file mode 100644 index 0000000..8b75dfe --- /dev/null +++ b/maubot/management/frontend/src/style/pages/log.sass @@ -0,0 +1,80 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +div.log + height: 100% + width: 100% + overflow: auto + + > div.lines + text-align: left + font-size: 12px + max-height: 100% + min-width: 100% + font-family: "Fira Code", monospace + display: table + + > div.row + display: table-row + white-space: pre + + &.debug + background-color: $background + + &:nth-child(odd) + background-color: $background-dark + + &.info + background-color: #AAFAFA + + &:nth-child(odd) + background-color: #66FAFA + + &.warning, &.warn + background-color: #FABB77 + + &:nth-child(odd) + background-color: #FAAA55 + + &.error + background-color: #FAAAAA + + &:nth-child(odd) + background-color: #FA9999 + + &.fatal + background-color: #CC44CC + + &:nth-child(odd) + background-color: #AA44AA + + &.unfocused + opacity: .25 + + > span + padding: .125rem .25rem + display: table-cell + + a + color: inherit + text-decoration: none + + &:hover + text-decoration: underline + + > span.text + > div.content > * + background-color: inherit !important + margin: 0 !important diff --git a/maubot/management/frontend/src/style/pages/modal.sass b/maubot/management/frontend/src/style/pages/modal.sass new file mode 100644 index 0000000..876e563 --- /dev/null +++ b/maubot/management/frontend/src/style/pages/modal.sass @@ -0,0 +1,71 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +div.modal-wrapper-wrapper + z-index: 9001 + position: fixed + top: 0 + bottom: 0 + left: 0 + right: 0 + background-color: rgba(0, 0, 0, 0.5) + + --modal-margin: 2.5rem + --button-height: 0rem + + @media screen and (max-width: 45rem) + --modal-margin: 1rem + --button-height: 2.5rem + + @media screen and (max-width: 35rem) + --modal-margin: 0rem + --button-height: 3rem + + button.close + +button + + display: none + + width: 100% + height: var(--button-height) + border-radius: .25rem .25rem 0 0 + + @media screen and (max-width: 45rem) + display: block + @media screen and (max-width: 35rem) + border-radius: 0 + + div.modal-wrapper + width: calc(100% - 2 * var(--modal-margin)) + height: calc(100% - 2 * var(--modal-margin) - var(--button-height)) + margin: var(--modal-margin) + border-radius: .25rem + + @media screen and (max-width: 35rem) + border-radius: 0 + + div.modal + padding: 1rem + height: 100% + width: 100% + background-color: $background + box-sizing: border-box + border-radius: .25rem + + @media screen and (max-width: 45rem) + border-radius: 0 0 .25rem .25rem + @media screen and (max-width: 35rem) + border-radius: 0 + padding: .5rem diff --git a/maubot/management/frontend/yarn.lock b/maubot/management/frontend/yarn.lock index a0489ab..f72ec5e 100644 --- a/maubot/management/frontend/yarn.lock +++ b/maubot/management/frontend/yarn.lock @@ -1770,7 +1770,7 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.22.0, babel-runtime@^6.26.0: +babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.6.1: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= @@ -1829,6 +1829,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base16@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70" + integrity sha1-4pf2DX7BAUp6lxo568ipjAtoHnA= + base64-js@^1.0.2: version "1.3.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" @@ -6303,11 +6308,21 @@ lodash.clonedeep@^4.3.2: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.curry@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" + integrity sha1-JI42By7ekGUB11lmIAqG2riyMXA= + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.flow@^3.3.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" + integrity sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o= + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -8366,6 +8381,11 @@ punycode@^1.2.4, punycode@^1.4.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= +pure-color@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e" + integrity sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4= + q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -8476,6 +8496,16 @@ react-app-polyfill@^0.1.3: raf "3.4.0" whatwg-fetch "3.0.0" +react-base16-styling@^0.5.1: + version "0.5.3" + resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.5.3.tgz#3858f24e9c4dd8cbd3f702f3f74d581ca2917269" + integrity sha1-OFjyTpxN2MvT9wLz901YHKKRcmk= + dependencies: + base16 "^1.0.0" + lodash.curry "^4.0.1" + lodash.flow "^3.3.0" + pure-color "^1.2.0" + react-dev-utils@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-6.0.5.tgz#6ef34d0a416dc1c97ac20025031ea1f0d819b21d" @@ -8526,6 +8556,15 @@ react-input-autosize@^2.2.1: dependencies: prop-types "^15.5.8" +react-json-tree@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/react-json-tree/-/react-json-tree-0.11.0.tgz#f5b17e83329a9c76ae38be5c04fda3a7fd684a35" + integrity sha1-9bF+gzKanHauOL5cBP2jp/1oSjU= + dependencies: + babel-runtime "^6.6.1" + prop-types "^15.5.8" + react-base16-styling "^0.5.1" + react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" From c3dbf9685df77ba9dbf6d7c757d0b43bfbd1aea7 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 29 Nov 2018 01:06:58 +0200 Subject: [PATCH 076/101] Fix clearing log focus --- .../frontend/src/pages/dashboard/index.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index c042fe8..a688ff5 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -33,7 +33,7 @@ class Dashboard extends Component { plugins: {}, sidebarOpen: false, modalOpen: false, - logFocus: "", + logFocus: null, } this.logLines = [] this.logMap = {} @@ -126,16 +126,18 @@ class Dashboard extends Component { entry, onDelete: () => this.delete(field, id), onChange: newEntry => this.add(field, newEntry, id), - openLog: filter => { - this.setState({ - logFocus: filter, - }) - this.logModal.open() - }, + openLog: this.openLog, ctx: this.state, }) } + openLog = filter => { + this.setState({ + logFocus: typeof filter === "string" ? filter : null, + }) + this.logModal.open() + } + renderNotFound = (thing = "path") => (
Oops! I'm afraid that {thing} couldn't be found. @@ -185,7 +187,7 @@ class Dashboard extends Component {
- }/> + }/> this.add("instances", newEntry)} ctx={this.state}/>}/> From 8fc5350644c90cfc0bef59ed81ab7a4ddb790c27 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 29 Nov 2018 11:43:41 +0200 Subject: [PATCH 077/101] Remove unused import --- maubot/management/api/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maubot/management/api/log.py b/maubot/management/api/log.py index 93fe41d..bead189 100644 --- a/maubot/management/api/log.py +++ b/maubot/management/api/log.py @@ -21,7 +21,7 @@ import asyncio from aiohttp import web -from .base import routes, get_loop, get_config +from .base import routes, get_loop from .auth import is_valid_token BUILTIN_ATTRS = {"args", "asctime", "created", "exc_info", "exc_text", "filename", "funcName", From b858b5a056be7ad0f07ef619b0a1117b298c69ed Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 29 Nov 2018 11:43:57 +0200 Subject: [PATCH 078/101] Add spec for /logs and /debug/open endpoints --- maubot/management/api/spec.md | 81 +++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 maubot/management/api/spec.md diff --git a/maubot/management/api/spec.md b/maubot/management/api/spec.md new file mode 100644 index 0000000..b487818 --- /dev/null +++ b/maubot/management/api/spec.md @@ -0,0 +1,81 @@ +# Maubot Management API +Most of the API is simple HTTP+JSON and has OpenAPI documentation (see +[spec.yaml](spec.yaml), [rendered](https://maubot.xyz/spec/)). However, +some parts of the API aren't documented in the OpenAPI document. + +## Log viewing +1. Open websocket to `/_matrix/maubot/v1/logs`. +2. Send authentication token as a plain string. +3. Server will respond with `{"auth_success": true}` and then with + `{"history": ...}` where `...` is a list of log entries. +4. Server will send new log entries as JSON. + +### Log entry object format +Log entries are a JSON-serialized form of Python log records. + +Log entries will always have: +* `id` - A string that should uniquely identify the row. Currently + uses the `relativeCreated` field of Python logging records. +* `msg` - The text in the entry. +* `time` - The ISO date when the log entry was created. + +Log entries should also always have: +* `levelname` - The log level (e.g. `DEBUG` or `ERROR`). +* `levelno` - The integer log level. +* `name` - The name of the logger. Possible values: + * `maubot.client.` - Client loggers (Matrix HTTP requests) + * `maubot.instance.` - Plugin instance loggers + * `maubot.loader.zip` - The zip plugin loader (plugins don't + have their own logs) +* `module` - The Python module name where the log call happened. +* `pathname` - The full path of the file where the log call happened. +* `filename` - The file name portion of `pathname` +* `lineno` - The line in code where the log call happened. +* `funcName` - The name of the function where the log call happened. + +Log entries might have: +* `exc_info` - The formatted exception info if an exception was logged. +* `matrix_http_request` - The info about a Matrix HTTP request. Subfields: + * `method` - The HTTP method used. + * `path` - The API path used. + * `content` - The content sent. + * `user` - The appservice user who the request was ran as. + +## Debug file open +For debug and development purposes, the API and frontend support +clicking on lines in stack traces to open that line in the selected +editor. + +### Configuration +First, the directory where maubot is run from must have a +`.dev-open-cfg.yaml` file. The file should contain the following +fields: +* `editor` - The command to run to open a file. + * `$path` is replaced with the full file path. + * `$line` is replaced with the line number. +* `pathmap` - A list of find-and-replaces to execute on paths. + These are needed to map things like `.mbp` files to the extracted + sources on disk. Each pathmap entry should have: + * `find` - The regex to match. + * `replace` - The replacement. May insert capture groups with Python + syntax (e.g. `\1`) + +Example file: +```yaml +editor: pycharm --line $line $path +pathmap: +- find: "maubot/plugins/xyz\\.maubot\\.(.+)-v.+(?:-ts[0-9]+)?.mbp" + replace: "mbplugins/\\1" +- find: "maubot/.venv/lib/python3.6/site-packages/mautrix" + replace: "mautrix-python/mautrix" +``` + +### API +Clients can `GET /_matrix/maubot/v1/debug/open` to check if the file +open endpoint has been set up. The response is a JSON object with a +single field `enabled`. If the value is true, the endpoint can be used. + +To open files, clients can `POST /_matrix/maubot/v1/debug/open` with +a JSON body containing +* `path` - The full file path to open +* `line` - The line number to open From 2b8a5353be9cde505f3ea4076e31cf72aaa06649 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 29 Nov 2018 12:27:31 +0200 Subject: [PATCH 079/101] Fix log live updating and add autoscrolling --- .../frontend/src/pages/dashboard/Log.js | 46 ++++++++++++++++++- .../frontend/src/pages/dashboard/Modal.js | 1 + .../frontend/src/pages/dashboard/index.js | 17 +++---- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/maubot/management/frontend/src/pages/dashboard/Log.js b/maubot/management/frontend/src/pages/dashboard/Log.js index 7e710f9..2cbbe31 100644 --- a/maubot/management/frontend/src/pages/dashboard/Log.js +++ b/maubot/management/frontend/src/pages/dashboard/Log.js @@ -113,13 +113,57 @@ class LogEntry extends PureComponent { } class Log extends PureComponent { + constructor(props) { + super(props) + + this.linesRef = React.createRef() + this.linesBottomRef = React.createRef() + } + + getSnapshotBeforeUpdate() { + if (this.linesRef.current && this.linesBottomRef.current) { + return Log.isVisible(this.linesRef.current, this.linesBottomRef.current) + } + return false + } + + + componentDidUpdate(_1, _2, wasVisible) { + if (wasVisible) { + Log.scrollParentToChild(this.linesRef.current, this.linesBottomRef.current) + } + } + + componentDidMount() { + if (this.linesRef.current && this.linesBottomRef.current) { + Log.scrollParentToChild(this.linesRef.current, this.linesBottomRef.current) + } + } + + static scrollParentToChild(parent, child) { + const parentRect = parent.getBoundingClientRect() + const childRect = child.getBoundingClientRect() + + if (!Log.isVisible(parent, child)) { + parent.scrollBy({ top: (childRect.top + parent.scrollTop) - parentRect.top }) + } + } + + static isVisible(parent, child) { + const parentRect = parent.getBoundingClientRect() + const childRect = child.getBoundingClientRect() + return (childRect.top >= parentRect.top) + && (childRect.top <= parentRect.top + parent.clientHeight) + } + render() { return ( -
+
{this.props.lines.map(data => )}
+
) } diff --git a/maubot/management/frontend/src/pages/dashboard/Modal.js b/maubot/management/frontend/src/pages/dashboard/Modal.js index 9bfc48e..b51301a 100644 --- a/maubot/management/frontend/src/pages/dashboard/Modal.js +++ b/maubot/management/frontend/src/pages/dashboard/Modal.js @@ -30,6 +30,7 @@ class Modal extends Component { open = () => this.setState({ open: true }) close = () => this.setState({ open: false }) + isOpen = () => this.state.open render() { return this.state.open && ( diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index a688ff5..c593bd0 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -34,11 +34,11 @@ class Dashboard extends Component { sidebarOpen: false, modalOpen: false, logFocus: null, + logLines: [], } - this.logLines = [] - this.logMap = {} this.logModal = { open: () => undefined, + isOpen: () => false, } window.maubot = this } @@ -80,20 +80,21 @@ class Dashboard extends Component { } else if (entry.name.startsWith("instance.")) { entry.nameLink = `/instance/${entry.name.substr("instance.".length)}` } - (this.logMap[entry.name] || (this.logMap[entry.name] = [])).push(entry) } logs.onHistory = history => { for (const data of history) { processEntry(data) } - this.logLines = history - this.setState({ logFocus: this.state.logFocus }) + this.setState({ + logLines: history, + }) } logs.onLog = data => { processEntry(data) - this.logLines.push(data) - this.setState({ logFocus: this.state.logFocus }) + this.setState({ + logLines: this.state.logLines.concat(data), + }) } } @@ -209,7 +210,7 @@ class Dashboard extends Component { renderModal() { return this.logModal = ref}> - + } From 30a6dd62ef027eb12123a954b1a6f14ca1352589 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 29 Nov 2018 12:28:23 +0200 Subject: [PATCH 080/101] Don't try to remove websocket from log listeners if it had not been added --- maubot/management/api/log.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/maubot/management/api/log.py b/maubot/management/api/log.py index bead189..fee233f 100644 --- a/maubot/management/api/log.py +++ b/maubot/management/api/log.py @@ -133,7 +133,8 @@ async def log_websocket(request: web.Request) -> web.WebSocketResponse: await ws.close() except Exception: pass - handler.listeners.remove(ws) + if authenticated: + handler.listeners.remove(ws) log.debug(f"Connection from {request.remote} closed") sockets.remove(ws) return ws From 9f593ed04e23e95eb89a76a0d8293d7b01e2b796 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 29 Nov 2018 12:32:36 +0200 Subject: [PATCH 081/101] Show date when hovering over time in log --- .../management/frontend/src/pages/dashboard/Log.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/maubot/management/frontend/src/pages/dashboard/Log.js b/maubot/management/frontend/src/pages/dashboard/Log.js index 2cbbe31..a4e8d0b 100644 --- a/maubot/management/frontend/src/pages/dashboard/Log.js +++ b/maubot/management/frontend/src/pages/dashboard/Log.js @@ -51,11 +51,15 @@ class LogEntry extends PureComponent { } renderTime() { - return this.props.line.time.toLocaleTimeString("en-GB") + return + {this.props.line.time.toLocaleTimeString("en-GB")} + } renderLevelName() { - return this.props.line.levelname + return + {this.props.line.levelname} + } get unfocused() { @@ -67,8 +71,8 @@ class LogEntry extends PureComponent { renderRow(content) { return (
- {this.renderTime()} - {this.renderLevelName()} + {this.renderTime()} + {this.renderLevelName()} {this.renderName()} {content}
From 3362e8e1185b996c36b6c8d1b8b146b835e433b0 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 29 Nov 2018 13:12:24 +0200 Subject: [PATCH 082/101] Remove unnecessary padding in log view --- maubot/management/frontend/src/style/pages/log.sass | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/maubot/management/frontend/src/style/pages/log.sass b/maubot/management/frontend/src/style/pages/log.sass index 8b75dfe..5a41395 100644 --- a/maubot/management/frontend/src/style/pages/log.sass +++ b/maubot/management/frontend/src/style/pages/log.sass @@ -67,6 +67,12 @@ div.log padding: .125rem .25rem display: table-cell + &:first-child + padding-left: 0 + + &:last-child + padding-right: 0 + a color: inherit text-decoration: none From c2065a4af7ffdd88905bb659a1ae741bf11bbd80 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 5 Dec 2018 16:38:13 +0200 Subject: [PATCH 083/101] Fix mautrix dep version --- maubot/__meta__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/maubot/__meta__.py b/maubot/__meta__.py index fb8c51a..466098a 100644 --- a/maubot/__meta__.py +++ b/maubot/__meta__.py @@ -1 +1 @@ -__version__ = "0.1.0.dev8" +__version__ = "0.1.0.dev9" diff --git a/setup.py b/setup.py index 7f92fa0..e34d099 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ setuptools.setup( packages=setuptools.find_packages(), install_requires=[ - "mautrix>=0.4,<0.5", + "mautrix>=0.4.dev1,<0.5", "aiohttp>=3.0.1,<4", "SQLAlchemy>=1.2.3,<2", "alembic>=1.0.0,<2", From 2d66ec25346ee4cdebeaa3a310b882279f9e3b00 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 5 Dec 2018 16:43:12 +0200 Subject: [PATCH 084/101] Add missing __init__.py's --- maubot/__meta__.py | 2 +- maubot/lib/__init__.py | 0 maubot/management/__init__.py | 0 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 maubot/lib/__init__.py create mode 100644 maubot/management/__init__.py diff --git a/maubot/__meta__.py b/maubot/__meta__.py index 466098a..360b262 100644 --- a/maubot/__meta__.py +++ b/maubot/__meta__.py @@ -1 +1 @@ -__version__ = "0.1.0.dev9" +__version__ = "0.1.0.dev10" diff --git a/maubot/lib/__init__.py b/maubot/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/maubot/management/__init__.py b/maubot/management/__init__.py new file mode 100644 index 0000000..e69de29 From 311497a448e37057770167ef818663da40da5e66 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 5 Dec 2018 17:16:40 +0200 Subject: [PATCH 085/101] Update readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a69d059..78e810d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # maubot A plugin-based [Matrix](https://matrix.org) bot system written in Python. -Management API spec: [maubot.xyz/spec](https://maubot.xyz/spec) +### [Wiki](https://github.com/maubot/maubot/wiki) + +### [Management API spec](https://github.com/maubot/maubot/blob/master/maubot/management/api/spec.md) ## Discussion Matrix room: [#maubot:maunium.net](https://matrix.to/#/#maubot:maunium.net) From c55f0c78408be4b3d29b83fff9e4d739fb8ffc5b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 5 Dec 2018 17:18:21 +0200 Subject: [PATCH 086/101] Update readme again --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 78e810d..c26c4e7 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ Matrix room: [#maubot:maunium.net](https://matrix.to/#/#maubot:maunium.net) * [jesaribot](https://github.com/maubot/jesaribot) - A simple bot that replies with an image when you say "jesari". * [sed](https://github.com/maubot/sed) - A bot to do sed-like replacements. * [factorial](https://github.com/maubot/factorial) - A bot to calculate unexpected factorials. -* [dictionary](https://github.com/maubot/dictionary) - A bot that provides dictionary definitions for words. * [media](https://github.com/maubot/media) - A bot that replies with the MXC URI of images you send it. * [dice](https://github.com/maubot/dice) - A combined dice rolling and calculator bot. * [karma](https://github.com/maubot/karma) - A user karma tracker bot. From 5337d4d98e147645ed2f12a2b7555a22f7d25132 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 5 Dec 2018 17:37:25 +0200 Subject: [PATCH 087/101] Link to debug file open in log line timestamp --- .../frontend/src/pages/dashboard/Log.js | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/maubot/management/frontend/src/pages/dashboard/Log.js b/maubot/management/frontend/src/pages/dashboard/Log.js index a4e8d0b..01b011e 100644 --- a/maubot/management/frontend/src/pages/dashboard/Log.js +++ b/maubot/management/frontend/src/pages/dashboard/Log.js @@ -50,10 +50,25 @@ class LogEntry extends PureComponent { return this.props.line.msg } + onClickOpen(path, line) { + return () => { + if (api.debugOpenFileEnabled()) { + api.debugOpenFile(path, line) + } + return false + } + } + + renderTimeTitle() { + return this.props.line.time.toDateString() + } + renderTime() { - return + return {this.props.line.time.toLocaleTimeString("en-GB")} - + } renderLevelName() { @@ -88,10 +103,9 @@ class LogEntry extends PureComponent { /File "(.+)", line ([0-9]+), in (.+)/g, (_, file, line, method) => { fileLinks.push( - { - api.debugOpenFile(file, line) - return false - }}>File "{file}", line {line}, in {method}, + + File "{file}", line {line}, in {method} + , ) return "||EDGE||" }) From 332ad5ea52611c51650638fa57b5baec9ca0055b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 8 Dec 2018 01:13:40 +0200 Subject: [PATCH 088/101] Add Matrix register/login API and full Matrix API proxy (ref #19, #15) --- docker/example-config.yaml | 8 ++ example-config.yaml | 8 ++ maubot/config.py | 1 + maubot/management/api/__init__.py | 2 + maubot/management/api/client.py | 25 ---- maubot/management/api/client_auth.py | 121 ++++++++++++++++++ maubot/management/api/client_proxy.py | 58 +++++++++ maubot/management/api/middleware.py | 9 +- maubot/management/api/responses.py | 21 +++ maubot/management/frontend/.eslintrc.json | 3 - maubot/management/frontend/src/api.js | 12 +- .../frontend/src/pages/dashboard/Client.js | 4 +- .../frontend/src/pages/dashboard/Instance.js | 2 +- 13 files changed, 238 insertions(+), 36 deletions(-) create mode 100644 maubot/management/api/client_auth.py create mode 100644 maubot/management/api/client_proxy.py diff --git a/docker/example-config.yaml b/docker/example-config.yaml index a273629..6ac3492 100644 --- a/docker/example-config.yaml +++ b/docker/example-config.yaml @@ -35,6 +35,14 @@ server: # Set to "generate" to generate and save a new token at startup. unshared_secret: generate +# Shared registration secrets to allow registering new users from the management UI +registration_secrets: + example.com: + # Client-server API URL + url: https://example.com + # registration_shared_secret from synapse config + secret: synapse_shared_registration_secret + # List of administrator users. Plaintext passwords will be bcrypted on startup. Set empty password # to prevent normal login. Root is a special user that can't have a password and will always exist. admins: diff --git a/example-config.yaml b/example-config.yaml index 1b02d67..384d877 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -35,6 +35,14 @@ server: # Set to "generate" to generate and save a new token at startup. unshared_secret: generate +# Shared registration secrets to allow registering new users from the management UI +registration_secrets: + example.com: + # Client-server API URL + url: https://example.com + # registration_shared_secret from synapse config + secret: synapse_shared_registration_secret + # List of administrator users. Plaintext passwords will be bcrypted on startup. Set empty password # to prevent normal login. Root is a special user that can't have a password and will always exist. admins: diff --git a/maubot/config.py b/maubot/config.py index ea8dd3c..ab3e080 100644 --- a/maubot/config.py +++ b/maubot/config.py @@ -47,6 +47,7 @@ class Config(BaseFileConfig): base["server.unshared_secret"] = self._new_token() else: base["server.unshared_secret"] = shared_secret + copy("registration_secrets") copy("admins") for username, password in base["admins"].items(): if password and not bcrypt_regex.match(password): diff --git a/maubot/management/api/__init__.py b/maubot/management/api/__init__.py index 760299e..93e994d 100644 --- a/maubot/management/api/__init__.py +++ b/maubot/management/api/__init__.py @@ -23,6 +23,8 @@ from .auth import web as _ from .plugin import web as _ from .instance import web as _ from .client import web as _ +from .client_proxy import web as _ +from .client_auth import web as _ from .dev_open import web as _ from .log import stop_all as stop_log_sockets, init as init_log_listener diff --git a/maubot/management/api/client.py b/maubot/management/api/client.py index 2975581..8fbf894 100644 --- a/maubot/management/api/client.py +++ b/maubot/management/api/client.py @@ -15,7 +15,6 @@ # along with this program. If not, see . from typing import Optional from json import JSONDecodeError -from http import HTTPStatus from aiohttp import web @@ -131,27 +130,3 @@ async def delete_client(request: web.Request) -> web.Response: await client.stop() client.delete() return resp.deleted - - -@routes.post("/client/{id}/avatar") -async def upload_avatar(request: web.Request) -> web.Response: - user_id = request.match_info.get("id", None) - client = Client.get(user_id, None) - if not client: - return resp.client_not_found - content = await request.read() - return web.json_response({ - "content_uri": await client.client.upload_media( - content, request.headers.get("Content-Type", None)), - }) - - -@routes.get("/client/{id}/avatar") -async def download_avatar(request: web.Request) -> web.Response: - user_id = request.match_info.get("id", None) - client = Client.get(user_id, None) - if not client: - return resp.client_not_found - if not client.avatar_url or client.avatar_url == "disable": - return web.Response() - return web.Response(body=await client.client.download_media(client.avatar_url)) diff --git a/maubot/management/api/client_auth.py b/maubot/management/api/client_auth.py new file mode 100644 index 0000000..ec5d4d3 --- /dev/null +++ b/maubot/management/api/client_auth.py @@ -0,0 +1,121 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from typing import Dict, Tuple, NamedTuple, Optional +from json import JSONDecodeError +import hmac +import hashlib + +from aiohttp import web +from mautrix.api import HTTPAPI, Path, Method +from mautrix.errors import MatrixRequestError + +from .base import routes, get_config, get_loop +from .responses import resp + + +def registration_secrets() -> Dict[str, Dict[str, str]]: + return get_config()["registration_secrets"] + + +def generate_mac(secret: str, nonce: str, user: str, password: str, admin: bool = False): + mac = hmac.new(key=secret.encode("utf-8"), digestmod=hashlib.sha1) + mac.update(nonce.encode("utf-8")) + mac.update(b"\x00") + mac.update(user.encode("utf-8")) + mac.update(b"\x00") + mac.update(password.encode("utf-8")) + mac.update(b"\x00") + mac.update(b"admin" if admin else b"notadmin") + return mac.hexdigest() + + +@routes.get("/client/auth/servers") +async def get_registerable_servers(_: web.Request) -> web.Response: + return web.json_response(list(registration_secrets().keys())) + + +AuthRequestInfo = NamedTuple("AuthRequestInfo", api=HTTPAPI, secret=str, username=str, password=str) + + +async def read_client_auth_request(request: web.Request) -> Tuple[Optional[AuthRequestInfo], + Optional[web.Response]]: + server_name = request.match_info.get("server", None) + server = registration_secrets().get(server_name, None) + if not server: + return None, resp.server_not_found + try: + body = await request.json() + except JSONDecodeError: + return None, resp.body_not_json + try: + username = body["username"] + password = body["password"] + except KeyError: + return None, resp.username_or_password_missing + try: + base_url = server["url"] + secret = server["secret"] + except KeyError: + return None, resp.invalid_server + api = HTTPAPI(base_url, "", loop=get_loop()) + return (api, secret, username, password), None + + +@routes.post("/client/auth/{server}/register") +async def register(request: web.Request) -> web.Response: + info, err = await read_client_auth_request(request) + if err is not None: + return err + api, secret, username, password = info + res = await api.request(Method.GET, Path.admin.register) + nonce = res["nonce"] + mac = generate_mac(secret, nonce, username, password) + try: + return web.json_response(await api.request(Method.POST, Path.admin.register, content={ + "nonce": nonce, + "username": username, + "password": password, + "admin": False, + "mac": mac, + })) + except MatrixRequestError as e: + return web.json_response({ + "errcode": e.errcode, + "error": e.message, + }, status=e.http_status) + + +@routes.post("/client/auth/{server}/login") +async def login(request: web.Request) -> web.Response: + info, err = await read_client_auth_request(request) + if err is not None: + return err + api, _, username, password = info + try: + return web.json_response(await api.request(Method.POST, Path.login, content={ + "type": "m.login.password", + "identifier": { + "type": "m.id.user", + "user": username, + }, + "password": password, + "device_id": "maubot", + })) + except MatrixRequestError as e: + return web.json_response({ + "errcode": e.errcode, + "error": e.message, + }, status=e.http_status) diff --git a/maubot/management/api/client_proxy.py b/maubot/management/api/client_proxy.py new file mode 100644 index 0000000..b6f5787 --- /dev/null +++ b/maubot/management/api/client_proxy.py @@ -0,0 +1,58 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from aiohttp import web, client as http + +from ...client import Client +from .base import routes +from .responses import resp + +PROXY_CHUNK_SIZE = 32 * 1024 + + +@routes.view("/proxy/{id}/{path:_matrix/.+}") +async def proxy(request: web.Request) -> web.StreamResponse: + user_id = request.match_info.get("id", None) + client = Client.get(user_id, None) + if not client: + return resp.client_not_found + + path = request.match_info.get("path", None) + query = request.query.copy() + try: + del query["access_token"] + except KeyError: + pass + headers = request.headers.copy() + headers["Authorization"] = f"Bearer {client.access_token}" + if "X-Forwarded-For" not in headers: + peer = request.transport.get_extra_info("peername") + if peer is not None: + host, port = peer + headers["X-Forwarded-For"] = f"{host}:{port}" + + data = await request.read() + chunked = PROXY_CHUNK_SIZE if not data else None + async with http.request(request.method, f"{client.homeserver}/{path}", headers=headers, + params=query, chunked=chunked, data=data) as proxy_resp: + response = web.StreamResponse(status=proxy_resp.status, headers=proxy_resp.headers) + await response.prepare(request) + content = proxy_resp.content + chunk = await content.read(PROXY_CHUNK_SIZE) + while chunk: + await response.write(chunk) + chunk = await content.read(PROXY_CHUNK_SIZE) + await response.write_eof() + return response diff --git a/maubot/management/api/middleware.py b/maubot/management/api/middleware.py index f58dcd8..538dba5 100644 --- a/maubot/management/api/middleware.py +++ b/maubot/management/api/middleware.py @@ -20,15 +20,20 @@ from aiohttp import web from .responses import resp from .auth import check_token +from .base import get_config Handler = Callable[[web.Request], Awaitable[web.Response]] @web.middleware async def auth(request: web.Request, handler: Handler) -> web.Response: - if "/auth/" in request.path: + subpath = request.path.lstrip(get_config()["server.base_path"]) + if subpath.startswith("/auth/") or subpath == "/logs" or subpath == "logs": return await handler(request) - return check_token(request) or await handler(request) + err = check_token(request) + if err is not None: + return err + return await handler(request) log = logging.getLogger("maubot.server") diff --git a/maubot/management/api/responses.py b/maubot/management/api/responses.py index 5204927..34fd110 100644 --- a/maubot/management/api/responses.py +++ b/maubot/management/api/responses.py @@ -75,6 +75,13 @@ class _Response: "errcode": "pid_mismatch", }, status=HTTPStatus.BAD_REQUEST) + @property + def username_or_password_missing(self) -> web.Response: + return web.json_response({ + "error": "Username or password missing", + "errcode": "username_or_password_missing", + }, status=HTTPStatus.BAD_REQUEST) + @property def bad_auth(self) -> web.Response: return web.json_response({ @@ -138,6 +145,13 @@ class _Response: "errcode": "resource_not_found", }, status=HTTPStatus.NOT_FOUND) + @property + def server_not_found(self) -> web.Response: + return web.json_response({ + "error": "Registration target server not found", + "errcode": "server_not_found", + }, status=HTTPStatus.NOT_FOUND) + @property def method_not_allowed(self) -> web.Response: return web.json_response({ @@ -196,6 +210,13 @@ class _Response: "errcode": "internal_server_error", }, status=HTTPStatus.INTERNAL_SERVER_ERROR) + @property + def invalid_server(self) -> web.Response: + return web.json_response({ + "error": "Invalid registration server object in maubot configuration", + "errcode": "invalid_server", + }, status=HTTPStatus.INTERNAL_SERVER_ERROR) + @property def unsupported_plugin_loader(self) -> web.Response: return web.json_response({ diff --git a/maubot/management/frontend/.eslintrc.json b/maubot/management/frontend/.eslintrc.json index 4052abf..8451d44 100644 --- a/maubot/management/frontend/.eslintrc.json +++ b/maubot/management/frontend/.eslintrc.json @@ -34,9 +34,6 @@ "semi": ["error", "never"], "comma-dangle": ["error", "always-multiline"], "max-len": ["warn", 100], - "camelcase": ["error", { - "properties": "always" - }], "space-before-function-paren": ["error", { "anonymous": "never", "named": "never", diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index 49465af..d01952e 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -138,6 +138,7 @@ export const updateDebugOpenFileEnabled = async () => { const resp = await defaultGet("/debug/open") _debugOpenFileEnabled = resp["enabled"] || false } + export async function debugOpenFile(path, line) { const resp = await fetch(`${BASE_PATH}/debug/open`, { headers: getHeaders(), @@ -178,7 +179,7 @@ export const getClients = () => defaultGet("/clients") export const getClient = id => defaultGet(`/clients/${id}`) export async function uploadAvatar(id, data, mime) { - const resp = await fetch(`${BASE_PATH}/client/${id}/avatar`, { + const resp = await fetch(`${BASE_PATH}/proxy/${id}/_matrix/media/r0/upload`, { headers: getHeaders(mime), body: data, method: "POST", @@ -186,8 +187,13 @@ export async function uploadAvatar(id, data, mime) { return await resp.json() } -export function getAvatarURL(id) { - return `${BASE_PATH}/client/${id}/avatar?access_token=${localStorage.accessToken}` +export function getAvatarURL({ id, avatar_url }) { + avatar_url = avatar_url || "" + if (avatar_url.startsWith("mxc://")) { + avatar_url = avatar_url.substr("mxc://".length) + } + return `${BASE_PATH}/proxy/${id}/_matrix/media/r0/download/${avatar_url}?access_token=${ + localStorage.accessToken}` } export const putClient = client => defaultPut("client", client) diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index e520c26..c4ed60d 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -31,7 +31,7 @@ const ClientListEntry = ({ entry }) => { } return ( - + {entry.displayname || entry.id} @@ -129,7 +129,7 @@ class Client extends BaseMainView {
- Avatar + Avatar - + {client.displayname || client.id}
), From 4f723b24dae116d1a824197e9a2ea8c798cb290e Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 8 Dec 2018 01:37:18 +0200 Subject: [PATCH 089/101] Update version --- maubot/__meta__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/maubot/__meta__.py b/maubot/__meta__.py index 360b262..4ae7d5c 100644 --- a/maubot/__meta__.py +++ b/maubot/__meta__.py @@ -1 +1 @@ -__version__ = "0.1.0.dev10" +__version__ = "0.1.0.dev11" diff --git a/setup.py b/setup.py index e34d099..066de36 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ setuptools.setup( packages=setuptools.find_packages(), install_requires=[ - "mautrix>=0.4.dev1,<0.5", + "mautrix>=0.4.dev20,<0.5", "aiohttp>=3.0.1,<4", "SQLAlchemy>=1.2.3,<2", "alembic>=1.0.0,<2", From 4f7eef6029f038425c4e10da70ce5f65e25d77c7 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 8 Dec 2018 13:07:12 +0200 Subject: [PATCH 090/101] Fix uploading and downloading avatars --- maubot/management/api/__init__.py | 2 +- maubot/management/api/client_proxy.py | 9 ++------- maubot/management/api/middleware.py | 4 +--- maubot/server.py | 2 +- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/maubot/management/api/__init__.py b/maubot/management/api/__init__.py index 93e994d..ceb86ef 100644 --- a/maubot/management/api/__init__.py +++ b/maubot/management/api/__init__.py @@ -32,7 +32,7 @@ from .log import stop_all as stop_log_sockets, init as init_log_listener def init(cfg: Config, loop: AbstractEventLoop) -> web.Application: set_config(cfg) set_loop(loop) - app = web.Application(loop=loop, middlewares=[auth, error]) + app = web.Application(loop=loop, middlewares=[auth, error], client_max_size=100*1024*1024) app.add_routes(routes) return app diff --git a/maubot/management/api/client_proxy.py b/maubot/management/api/client_proxy.py index b6f5787..8dbb650 100644 --- a/maubot/management/api/client_proxy.py +++ b/maubot/management/api/client_proxy.py @@ -14,7 +14,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . from aiohttp import web, client as http - from ...client import Client from .base import routes from .responses import resp @@ -44,15 +43,11 @@ async def proxy(request: web.Request) -> web.StreamResponse: headers["X-Forwarded-For"] = f"{host}:{port}" data = await request.read() - chunked = PROXY_CHUNK_SIZE if not data else None async with http.request(request.method, f"{client.homeserver}/{path}", headers=headers, - params=query, chunked=chunked, data=data) as proxy_resp: + params=query, data=data) as proxy_resp: response = web.StreamResponse(status=proxy_resp.status, headers=proxy_resp.headers) await response.prepare(request) - content = proxy_resp.content - chunk = await content.read(PROXY_CHUNK_SIZE) - while chunk: + async for chunk in proxy_resp.content.iter_chunked(PROXY_CHUNK_SIZE): await response.write(chunk) - chunk = await content.read(PROXY_CHUNK_SIZE) await response.write_eof() return response diff --git a/maubot/management/api/middleware.py b/maubot/management/api/middleware.py index 538dba5..7406b61 100644 --- a/maubot/management/api/middleware.py +++ b/maubot/management/api/middleware.py @@ -23,6 +23,7 @@ from .auth import check_token from .base import get_config Handler = Callable[[web.Request], Awaitable[web.Response]] +log = logging.getLogger("maubot.server") @web.middleware @@ -36,9 +37,6 @@ async def auth(request: web.Request, handler: Handler) -> web.Response: return await handler(request) -log = logging.getLogger("maubot.server") - - @web.middleware async def error(request: web.Request, handler: Handler) -> web.Response: try: diff --git a/maubot/server.py b/maubot/server.py index deb17e6..35682ee 100644 --- a/maubot/server.py +++ b/maubot/server.py @@ -38,7 +38,7 @@ class MaubotServer: def __init__(self, config: Config, loop: asyncio.AbstractEventLoop) -> None: self.loop = loop or asyncio.get_event_loop() - self.app = web.Application(loop=self.loop) + self.app = web.Application(loop=self.loop, client_max_size=100*1024*1024) self.config = config as_path = PathBuilder(config["server.appservice_base_path"]) From d09dd8ddc6735a232a4365a0ac507e33dcc5545b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 8 Dec 2018 13:29:53 +0200 Subject: [PATCH 091/101] Highlight edited fields --- .../src/components/PreferenceTable.js | 34 +++++++++++-------- .../src/pages/dashboard/BaseMainView.js | 2 +- .../frontend/src/pages/dashboard/Client.js | 30 +++++++++------- .../frontend/src/pages/dashboard/Instance.js | 9 +++-- .../frontend/src/pages/dashboard/index.js | 12 ++++--- .../src/style/lib/preferencetable.sass | 5 +++ 6 files changed, 56 insertions(+), 36 deletions(-) diff --git a/maubot/management/frontend/src/components/PreferenceTable.js b/maubot/management/frontend/src/components/PreferenceTable.js index 92dc3ce..ee10726 100644 --- a/maubot/management/frontend/src/components/PreferenceTable.js +++ b/maubot/management/frontend/src/components/PreferenceTable.js @@ -34,28 +34,32 @@ export const PrefTable = ({ children, wrapperClass }) => { ) } -export const PrefRow = ({ name, fullWidth = false, labelFor = undefined, children }) => ( -
- -
{children}
-
-) +export const PrefRow = + ({ name, fullWidth = false, labelFor = undefined, changed = false, children }) => ( +
+ +
{children}
+
+ ) -export const PrefInput = ({ rowName, fullWidth = false, ...args }) => ( - - +export const PrefInput = ({ rowName, value, origValue, fullWidth = false, ...args }) => ( + + ) -export const PrefSwitch = ({ rowName, fullWidth = false, ...args }) => ( - - +export const PrefSwitch = ({ rowName, active, origActive, fullWidth = false, ...args }) => ( + + ) -export const PrefSelect = ({ rowName, fullWidth = false, ...args }) => ( - - ) diff --git a/maubot/management/frontend/src/pages/dashboard/BaseMainView.js b/maubot/management/frontend/src/pages/dashboard/BaseMainView.js index 2ad72b1..7a8ba47 100644 --- a/maubot/management/frontend/src/pages/dashboard/BaseMainView.js +++ b/maubot/management/frontend/src/pages/dashboard/BaseMainView.js @@ -34,7 +34,7 @@ class BaseMainView extends Component { } get isNew() { - return !this.props.entry + return !this.props.entry.id } inputChange = event => { diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index c4ed60d..5d965d4 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -157,25 +157,31 @@ class Client extends BaseMainView { renderPreferences = () => ( + value={this.state.homeserver} origValue={this.props.entry.homeserver} + placeholder="https://example.com" onChange={this.inputChange}/> - + - + this.setState({ sync })}/> - this.setState({ autojoin })}/> - this.setState({ enabled, started: enabled && this.state.started, diff --git a/maubot/management/frontend/src/pages/dashboard/Instance.js b/maubot/management/frontend/src/pages/dashboard/Instance.js index d89cda0..83ceed5 100644 --- a/maubot/management/frontend/src/pages/dashboard/Instance.js +++ b/maubot/management/frontend/src/pages/dashboard/Instance.js @@ -138,15 +138,18 @@ class Instance extends BaseMainView { - this.setState({ enabled })}/> - this.setState({ started })}/> this.setState({ primary_user: id })}/> this.setState({ type: id })}/> {!this.isNew && diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index c593bd0..4aabe92 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -191,11 +191,13 @@ class Dashboard extends Component { }/> this.add("instances", newEntry)} - ctx={this.state}/>}/> - this.add("clients", newEntry)}/>}/> - this.add("plugins", newEntry)}/>}/> + entry={{}} ctx={this.state}/>}/> + + + this.add("clients", newEntry)}/>}/> + + + this.add("plugins", newEntry)}/>}/> this.renderView("instances", Instance, match.params.id)}/> diff --git a/maubot/management/frontend/src/style/lib/preferencetable.sass b/maubot/management/frontend/src/style/lib/preferencetable.sass index c72dff1..35b3ab5 100644 --- a/maubot/management/frontend/src/style/lib/preferencetable.sass +++ b/maubot/management/frontend/src/style/lib/preferencetable.sass @@ -32,6 +32,11 @@ &.full-width width: 100% + &.changed > label + font-weight: bold + &:after + content: "*" + > label, > .value display: block width: 100% From 67a4e8540647900b2d7e51608927d9997b5d4cbf Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 8 Dec 2018 13:57:04 +0200 Subject: [PATCH 092/101] Fix logging in --- maubot/management/api/middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maubot/management/api/middleware.py b/maubot/management/api/middleware.py index 7406b61..f5bd1e4 100644 --- a/maubot/management/api/middleware.py +++ b/maubot/management/api/middleware.py @@ -29,7 +29,7 @@ log = logging.getLogger("maubot.server") @web.middleware async def auth(request: web.Request, handler: Handler) -> web.Response: subpath = request.path.lstrip(get_config()["server.base_path"]) - if subpath.startswith("/auth/") or subpath == "/logs" or subpath == "logs": + if subpath.startswith("auth/") or subpath == "logs": return await handler(request) err = check_token(request) if err is not None: From 5775560933163f79282acf2a1cd882d958131d22 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 8 Dec 2018 14:12:33 +0200 Subject: [PATCH 093/101] Actually fix login --- maubot/__meta__.py | 2 +- maubot/management/api/middleware.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/maubot/__meta__.py b/maubot/__meta__.py index 4ae7d5c..c40e5e8 100644 --- a/maubot/__meta__.py +++ b/maubot/__meta__.py @@ -1 +1 @@ -__version__ = "0.1.0.dev11" +__version__ = "0.1.0.dev13" diff --git a/maubot/management/api/middleware.py b/maubot/management/api/middleware.py index f5bd1e4..5f7a788 100644 --- a/maubot/management/api/middleware.py +++ b/maubot/management/api/middleware.py @@ -28,8 +28,8 @@ log = logging.getLogger("maubot.server") @web.middleware async def auth(request: web.Request, handler: Handler) -> web.Response: - subpath = request.path.lstrip(get_config()["server.base_path"]) - if subpath.startswith("auth/") or subpath == "logs": + subpath = request.path[len(get_config()["server.base_path"]):] + if subpath.startswith("/auth/") or subpath == "/logs": return await handler(request) err = check_token(request) if err is not None: From 8824c2178879475e053cf9712ef2d6221527434b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 8 Dec 2018 19:47:40 +0200 Subject: [PATCH 094/101] Add spec for Matrix API proxy --- maubot/management/api/spec.md | 14 +++++++++++- maubot/management/api/spec.yaml | 39 --------------------------------- 2 files changed, 13 insertions(+), 40 deletions(-) diff --git a/maubot/management/api/spec.md b/maubot/management/api/spec.md index b487818..2e2b399 100644 --- a/maubot/management/api/spec.md +++ b/maubot/management/api/spec.md @@ -3,6 +3,18 @@ Most of the API is simple HTTP+JSON and has OpenAPI documentation (see [spec.yaml](spec.yaml), [rendered](https://maubot.xyz/spec/)). However, some parts of the API aren't documented in the OpenAPI document. +## Matrix API proxy +The full Matrix API can be accessed for each client with a request to +`/_matrix/maubot/v1/proxy//`. `` is the Matrix user +ID of the user to access the API as and `` is the whole API +path to access (e.g. `/_matrix/client/r0/whoami`). + +The body, headers, query parameters, etc are sent to the Matrix server +as-is, with a few exceptions: +* The `Authorization` header will be replaced with the access token + for the Matrix user from the maubot database. +* The `access_token` query parameter will be removed. + ## Log viewing 1. Open websocket to `/_matrix/maubot/v1/logs`. 2. Send authentication token as a plain string. @@ -22,7 +34,7 @@ Log entries will always have: Log entries should also always have: * `levelname` - The log level (e.g. `DEBUG` or `ERROR`). * `levelno` - The integer log level. -* `name` - The name of the logger. Possible values: +* `name` - The name of the logger. Common values: * `maubot.client.` - Client loggers (Matrix HTTP requests) * `maubot.instance.` - Plugin instance loggers * `maubot.loader.zip` - The zip plugin loader (plugins don't diff --git a/maubot/management/api/spec.yaml b/maubot/management/api/spec.yaml index af93a65..8bd1206 100644 --- a/maubot/management/api/spec.yaml +++ b/maubot/management/api/spec.yaml @@ -399,45 +399,6 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' - '/client/{id}/avatar': - parameters: - - name: id - in: path - description: The Matrix user ID of the client to get - required: true - schema: - type: string - post: - operationId: upload_avatar - summary: Upload a profile picture for a bot - tags: [Clients] - requestBody: - content: - image/png: - schema: - type: string - format: binary - example: The avatar to upload - image/jpeg: - schema: - type: string - format: binary - example: The avatar to upload - responses: - 200: - description: The avatar was uploaded successfully - content: - application/json: - schema: - type: object - properties: - content_uri: - type: string - description: The MXC URI of the uploaded avatar - 400: - $ref: '#/components/responses/BadRequest' - 401: - $ref: '#/components/responses/Unauthorized' components: responses: From f5b996d2a2cce35ae251ceff0f69eb9b9f39da30 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 8 Dec 2018 20:41:23 +0200 Subject: [PATCH 095/101] Move view logs button to sidebar --- maubot/management/frontend/src/pages/dashboard/Home.js | 1 - maubot/management/frontend/src/pages/dashboard/index.js | 5 +++++ .../pages/{dashboard-grid.css => dashboard-grid.scss} | 0 maubot/management/frontend/src/style/pages/sidebar.sass | 8 ++++++++ 4 files changed, 13 insertions(+), 1 deletion(-) rename maubot/management/frontend/src/style/pages/{dashboard-grid.css => dashboard-grid.scss} (100%) diff --git a/maubot/management/frontend/src/pages/dashboard/Home.js b/maubot/management/frontend/src/pages/dashboard/Home.js index 8dc22b9..7c2fe13 100644 --- a/maubot/management/frontend/src/pages/dashboard/Home.js +++ b/maubot/management/frontend/src/pages/dashboard/Home.js @@ -22,7 +22,6 @@ class Home extends Component { See sidebar to get started
-
} diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index 4aabe92..2d8cdb0 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -156,6 +156,11 @@ class Dashboard extends Component {