diff --git a/build/react-frontend/package-lock.json b/build/react-frontend/package-lock.json index 0c6ab43..42018f5 100644 --- a/build/react-frontend/package-lock.json +++ b/build/react-frontend/package-lock.json @@ -1163,6 +1163,19 @@ "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz", "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==" }, + "@date-io/core": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz", + "integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==" + }, + "@date-io/date-fns": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-1.3.13.tgz", + "integrity": "sha512-yXxGzcRUPcogiMj58wVgFjc9qUYrCnnU9eLcyNbsQCmae4jPuZCDoIBR21j8ZURsM7GRtU62VOw5yNd4dDHunA==", + "requires": { + "@date-io/core": "^1.3.13" + } + }, "@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", @@ -1420,6 +1433,19 @@ "@babel/runtime": "^7.4.4" } }, + "@material-ui/pickers": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.10.tgz", + "integrity": "sha512-B8G6Obn5S3RCl7hwahkQj9sKUapwXWFjiaz/Bsw1fhYFdNMnDUolRiWQSoKPb1/oKe37Dtfszoywi1Ynbo3y8w==", + "requires": { + "@babel/runtime": "^7.6.0", + "@date-io/core": "1.x", + "@types/styled-jsx": "^2.2.8", + "clsx": "^1.0.2", + "react-transition-group": "^4.0.0", + "rifm": "^0.7.0" + } + }, "@material-ui/styles": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.10.0.tgz", @@ -1865,6 +1891,14 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==" }, + "@types/styled-jsx": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.8.tgz", + "integrity": "sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg==", + "requires": { + "@types/react": "*" + } + }, "@types/testing-library__dom": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-6.14.0.tgz", @@ -2572,37 +2606,6 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==" }, - "axios": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", - "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", - "requires": { - "follow-redirects": "1.5.10" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -3075,6 +3078,11 @@ } } }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", @@ -3476,6 +3484,103 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001109.tgz", "integrity": "sha512-4JIXRodHzdS3HdK8nSgIqXYLExOvG+D2/EenSvcub2Kp3QEADjo2v2oUn5g0n0D+UNwG9BtwKOyGcSq2qvQXvQ==" }, + "canvg": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-1.5.3.tgz", + "integrity": "sha512-7Gn2IuQzvUQWPIuZuFHrzsTM0gkPz2RRT9OcbdmA03jeKk8kltrD8gqUzNX15ghY/4PV5bbe5lmD6yDLDY6Ybg==", + "requires": { + "jsdom": "^8.1.0", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^1.4.1", + "xmldom": "^0.1.22" + }, + "dependencies": { + "abab": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", + "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=" + }, + "acorn": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=" + }, + "acorn-globals": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", + "integrity": "sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=", + "requires": { + "acorn": "^2.1.0" + } + }, + "cssstyle": { + "version": "0.2.37", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", + "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", + "requires": { + "cssom": "0.3.x" + } + }, + "jsdom": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-8.5.0.tgz", + "integrity": "sha1-1Nj12/J2hjW2KmKCO5R89wcevJg=", + "requires": { + "abab": "^1.0.0", + "acorn": "^2.4.0", + "acorn-globals": "^1.0.4", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.0 < 0.4.0", + "cssstyle": ">= 0.2.34 < 0.3.0", + "escodegen": "^1.6.1", + "iconv-lite": "^0.4.13", + "nwmatcher": ">= 1.3.7 < 2.0.0", + "parse5": "^1.5.1", + "request": "^2.55.0", + "sax": "^1.1.4", + "symbol-tree": ">= 3.1.0 < 4.0.0", + "tough-cookie": "^2.2.0", + "webidl-conversions": "^3.0.1", + "whatwg-url": "^2.0.1", + "xml-name-validator": ">= 2.0.1 < 3.0.0" + } + }, + "parse5": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", + "integrity": "sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=" + }, + "stackblur-canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-1.4.1.tgz", + "integrity": "sha1-hJqm+UsnL/JvZHH6QTDtH35HlVs=" + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-2.0.1.tgz", + "integrity": "sha1-U5ayBD8CDub3BNnEXqhRnnJN5lk=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "xml-name-validator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", + "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=" + } + } + }, "capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -3617,6 +3722,11 @@ } } }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, "clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", @@ -4097,6 +4207,14 @@ "postcss": "^7.0.5" } }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, "css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", @@ -4137,6 +4255,14 @@ } } }, + "css-line-break": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.0.1.tgz", + "integrity": "sha1-GfIGOjPpX7KDG4ZEbAuAwYivRQo=", + "requires": { + "base64-arraybuffer": "^0.1.5" + } + }, "css-loader": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.4.2.tgz", @@ -4399,6 +4525,16 @@ } } }, + "date-fns": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.15.0.tgz", + "integrity": "sha512-ZCPzAMJZn3rNUvvQIMlXhDr4A+Ar07eLeGsGREoWU19a3Pqf5oYa+ccd+B3F6XVtQY6HANMFdOQ8A+ipFnvJdQ==" + }, + "debounce": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz", + "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==" + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -5876,6 +6012,15 @@ "schema-utils": "^2.5.0" } }, + "file-saver": { + "version": "github:eligrey/FileSaver.js#e865e37af9f9947ddcced76b549e27dc45c1cb2e", + "from": "github:eligrey/FileSaver.js#1.3.8" + }, + "filefy": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/filefy/-/filefy-0.1.10.tgz", + "integrity": "sha512-VgoRVOOY1WkTpWH+KBy8zcU1G7uQTVsXqhWEgzryB9A5hg2aqCyZ6aQ/5PSzlqM5+6cnVrX6oYV0XqD3HZSnmQ==" + }, "filesize": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.0.1.tgz", @@ -6584,6 +6729,14 @@ } } }, + "html2canvas": { + "version": "1.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-alpha.12.tgz", + "integrity": "sha1-OxmS48mz9WBjw1/WIElPN+uohRM=", + "requires": { + "css-line-break": "1.0.1" + } + }, "htmlparser2": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", @@ -7889,6 +8042,24 @@ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" }, + "jspdf": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-1.5.3.tgz", + "integrity": "sha512-J9X76xnncMw+wIqb15HeWfPMqPwYxSpPY8yWPJ7rAZN/ZDzFkjCSZObryCyUe8zbrVRNiuCnIeQteCzMn7GnWw==", + "requires": { + "canvg": "1.5.3", + "file-saver": "github:eligrey/FileSaver.js#1.3.8", + "html2canvas": "1.0.0-alpha.12", + "omggif": "1.0.7", + "promise-polyfill": "8.1.0", + "stackblur-canvas": "2.2.0" + } + }, + "jspdf-autotable": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-3.5.3.tgz", + "integrity": "sha512-K+cNWW3x6w0R/1B5m6PYOm6v8CTTDXy/g32lZouc7SuC6zhvzMN2dauhk6dDYxPD0pky0oyPIJFwSJ/tV8PAeg==" + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -8283,6 +8454,32 @@ "object-visit": "^1.0.0" } }, + "material-table": { + "version": "1.67.1", + "resolved": "https://registry.npmjs.org/material-table/-/material-table-1.67.1.tgz", + "integrity": "sha512-cMmzIO601D1b4C+ZYlpiRxBUtvrAVfJ74wcpeF39K4wdfweXyFgQ+bXdfFSIycBv36ZPTFWoku/3bAJiBn5Gzw==", + "requires": { + "@date-io/date-fns": "^1.1.0", + "@material-ui/pickers": "^3.2.2", + "classnames": "^2.2.6", + "date-fns": "^2.0.0-alpha.27", + "debounce": "^1.2.0", + "fast-deep-equal": "2.0.1", + "filefy": "0.1.10", + "jspdf": "1.5.3", + "jspdf-autotable": "3.5.3", + "prop-types": "^15.6.2", + "react-beautiful-dnd": "^13.0.0", + "react-double-scrollbar": "0.0.15" + }, + "dependencies": { + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + } + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -8313,6 +8510,11 @@ "p-is-promise": "^2.0.0" } }, + "memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -8869,6 +9071,11 @@ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, + "nwmatcher": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.4.tgz", + "integrity": "sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ==" + }, "nwsapi": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", @@ -9006,6 +9213,11 @@ "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" }, + "omggif": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.7.tgz", + "integrity": "sha1-WdLuywJj3oRjWz/riHwMmXPx5J0=" + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -10436,6 +10648,11 @@ "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" }, + "promise-polyfill": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.0.tgz", + "integrity": "sha512-OzSf6gcCUQ01byV4BgwyUCswlaQQ6gzXc23aLQWhicvfX9kfsUiUhgt3CCQej8jDnl8/PhGF31JdHX2/MzF3WA==" + }, "prompts": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz", @@ -10571,6 +10788,11 @@ "performance-now": "^2.1.0" } }, + "raf-schd": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz", + "integrity": "sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -10634,6 +10856,20 @@ "whatwg-fetch": "^3.0.0" } }, + "react-beautiful-dnd": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz", + "integrity": "sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg==", + "requires": { + "@babel/runtime": "^7.8.4", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.1.1", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + } + }, "react-dev-utils": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz", @@ -10846,6 +11082,11 @@ "scheduler": "^0.19.1" } }, + "react-double-scrollbar": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/react-double-scrollbar/-/react-double-scrollbar-0.0.15.tgz", + "integrity": "sha1-6RWrjLO5WYdwdfSUNt6/2wQoj+Q=" + }, "react-error-overlay": { "version": "6.0.7", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz", @@ -10856,6 +11097,18 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-redux": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.1.tgz", + "integrity": "sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg==", + "requires": { + "@babel/runtime": "^7.5.5", + "hoist-non-react-statics": "^3.3.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.9.0" + } + }, "react-scripts": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.1.tgz", @@ -10989,6 +11242,15 @@ "strip-indent": "^3.0.0" } }, + "redux": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", + "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, "regenerate": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz", @@ -11349,6 +11611,19 @@ "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" }, + "rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha1-1lBezbMEplldom+ktDMHMGd1lF0=" + }, + "rifm": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz", + "integrity": "sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==", + "requires": { + "@babel/runtime": "^7.3.1" + } + }, "rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -12078,6 +12353,11 @@ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==" }, + "stackblur-canvas": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.2.0.tgz", + "integrity": "sha512-5Gf8dtlf8k6NbLzuly2NkGrkS/Ahh+I5VUjO7TnFizdJtgpfpLLEdQlLe9umbcnZlitU84kfYjXE67xlSXfhfQ==" + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -12425,6 +12705,11 @@ "util.promisify": "~1.0.0" } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -12663,6 +12948,11 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, "tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -12979,6 +13269,11 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-memo-one": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.1.tgz", + "integrity": "sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ==" + }, "util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", @@ -14020,6 +14315,11 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "xmldom": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz", + "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==" + }, "xregexp": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz", diff --git a/build/react-frontend/package.json b/build/react-frontend/package.json index a9d97eb..a3f8b52 100644 --- a/build/react-frontend/package.json +++ b/build/react-frontend/package.json @@ -8,6 +8,7 @@ "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", + "material-table": "^1.67.1", "react": "^16.13.1", "react-dom": "^16.13.1", "react-scripts": "3.4.1" diff --git a/build/react-frontend/public/sampleData/bot.json b/build/react-frontend/public/sampleData/bot.json new file mode 100755 index 0000000..d1269af --- /dev/null +++ b/build/react-frontend/public/sampleData/bot.json @@ -0,0 +1 @@ +{"jointime":"0001-01-01T00:00:00Z","hasleft":false,"videoCacheUpdated":"2020-08-02T17:40:13.480470725Z", "commands": [{"command":"card","action":"RandomPrompt"},{"command":"whotosingwith","action":"AgingSinger"}]} diff --git a/build/react-frontend/src/Components/BotDeets/BotDeets.js b/build/react-frontend/src/Components/BotDeets/BotDeets.js index 3537b77..2d28e50 100755 --- a/build/react-frontend/src/Components/BotDeets/BotDeets.js +++ b/build/react-frontend/src/Components/BotDeets/BotDeets.js @@ -1,5 +1,21 @@ import React, { useEffect } from 'react'; import { Container, Typography, Button } from '@material-ui/core'; +import { AddBox, ArrowDownward } from "@material-ui/icons"; +import MaterialTable from "material-table"; +import Check from '@material-ui/icons/Check'; +import ChevronLeft from '@material-ui/icons/ChevronLeft'; +import ChevronRight from '@material-ui/icons/ChevronRight'; +import Clear from '@material-ui/icons/Clear'; +import DeleteOutline from '@material-ui/icons/DeleteOutline'; +import Edit from '@material-ui/icons/Edit'; +import FilterList from '@material-ui/icons/FilterList'; +import FirstPage from '@material-ui/icons/FirstPage'; +import LastPage from '@material-ui/icons/LastPage'; +import Remove from '@material-ui/icons/Remove'; +import SaveAlt from '@material-ui/icons/SaveAlt'; +import Search from '@material-ui/icons/Search'; +import ViewColumn from '@material-ui/icons/ViewColumn'; +import { forwardRef } from 'react'; const channelName = window.location.href.split("/")[4] const adminToken = window.location.href.split("/")[5] @@ -7,11 +23,36 @@ const dataURL = "/botdeets/"+ channelName + "/" + adminToken function BotDeets() { - const [botState, setBotState] = React.useState({loading: true, botData: []}) + const [botState, setBotState] = React.useState({loading: true, botData: { commands: [{"command":"card","action":"RandomPrompt"},{"command":"whotosingwith","action":"AgingSinger"}]} }); + + const tableIcons = { + Add: forwardRef((props, ref) => ), + Check: forwardRef((props, ref) => ), + Clear: forwardRef((props, ref) => ), + Delete: forwardRef((props, ref) => ), + DetailPanel: forwardRef((props, ref) => ), + Edit: forwardRef((props, ref) => ), + Export: forwardRef((props, ref) => ), + Filter: forwardRef((props, ref) => ), + FirstPage: forwardRef((props, ref) => ), + LastPage: forwardRef((props, ref) => ), + NextPage: forwardRef((props, ref) => ), + PreviousPage: forwardRef((props, ref) => ), + ResetSearch: forwardRef((props, ref) => ), + Search: forwardRef((props, ref) => ), + SortArrow: forwardRef((props, ref) => ), + ThirdStateCheck: forwardRef((props, ref) => ), + ViewColumn: forwardRef((props, ref) => ) + }; useEffect(() => { // We should only fetch once! if (botState.loading) { - fetch(dataURL).then((res) => res.json().then((data)=>{ + let actualURL = dataURL + if (window.location.href.split("/")[2] === "192.168.1.111:3000") { + //Frontend dev mode only + actualURL = "/sampleData/bot.json" + } + fetch(actualURL).then((res) => res.json().then((data)=>{ setBotState({botData: data, loading: false }) })); } @@ -20,8 +61,15 @@ function BotDeets() { if (botState.loading) { return

Sorry, still loading...

} + + if ((window.location.href.split("/")[2] === "192.168.1.111:3000") || channelName === "iMartynOnTwitch") { + + } else { + return

Sorry, still making this... soon soon.

+ } - let dd = botState.botData + const dd = botState.botData + const cmds = dd.commands if (dd.hasleft) { return ( @@ -38,13 +86,83 @@ function BotDeets() { ) } else { return ( + Coming soon! Bot control panel! - - {dd} - +
+ true, + isDeletable: rowData => true, + onRowAddCancelled: rowData => console.log('Row adding cancelled'), + onRowUpdateCancelled: rowData => console.log('Row editing cancelled'), + onRowUpdate: (newData, oldData) => + new Promise((resolve, reject) => { + setTimeout(() => { + const index = cmds.indexOf(oldData); + let newCommands = cmds; + let newState = botState; + newCommands[index] = newData; + newState.botData.commands = newCommands; + setBotState(newState); + resolve(); + }, 1000); + }), + onRowDelete: oldData => + new Promise((resolve, reject) => { + setTimeout(() => { + const index = cmds.indexOf(oldData); + let newCommands = cmds; + let newState = botState; + newCommands.splice(index, 1); + newState.botData.commands = newCommands; + setBotState(newState); + resolve(); + }, 1000); + }), + onRowAdd: newData => + new Promise((resolve, reject) => { + setTimeout(() => { + let newCommands = cmds; + let newState = botState; + newCommands[cmds.length] = newData; + newState.botData.commands = newCommands; + setBotState(newState); + resolve(); + }, 1000); + }), + }} + icons={tableIcons} + columns={[ + { title: "Command", field: "command" }, + { + title: "Action description", + field: "action", + lookup: { RandomPrompt: "Random prompt", AgingSinger: "Singer I haven't sung with in ages", AgingSong: "Song I haven't sung in ages"}, + /* editComponent: tableData => + */ + }, + ]} + data={cmds} + title="Commands" + options={{ + search: false + }} + /> +
); } diff --git a/build/react-frontend/src/Components/TopTenSingers/TopTenSingers.txt b/build/react-frontend/src/Components/TopTenSingers/TopTenSingers.txt deleted file mode 100755 index ed37309..0000000 --- a/build/react-frontend/src/Components/TopTenSingers/TopTenSingers.txt +++ /dev/null @@ -1,161 +0,0 @@ -import React, { useEffect } from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TableSortLabel, Paper } from '@material-ui/core'; -import PropTypes from 'prop-types'; - -const channelName = window.location.href.split("/")[4] -const adminToken = window.location.href.split("/")[5] -const dataURL = "/topsingers/"+ channelName + "/" + adminToken -const headCells = [ - { id: 'singerName', numeric: false, disablePadding: true, label: 'Singer Name' }, - { id: 'singCount', numeric: true, disablePadding: false, label: 'Count' }, -]; - -function descendingComparator(a, b, orderBy) { - if (b[orderBy] < a[orderBy]) { - return -1; - } - if (b[orderBy] > a[orderBy]) { - return 1; - } - return 0; -} - -function stableSort(array, comparator) { - const stabilizedThis = array.map((el, index) => [el, index]); - stabilizedThis.sort((a, b) => { - const order = comparator(a[0], b[0]); - if (order !== 0) return order; - return a[1] - b[1]; - }); - return stabilizedThis.map((el) => el[0]); -} - -function getComparator(order, orderBy) { - return order === 'desc' - ? (a, b) => descendingComparator(a, b, orderBy) - : (a, b) => -descendingComparator(a, b, orderBy); -} - -function EnhancedTableHead(props) { - const { classes, order, orderBy, onRequestSort } = props; - const createSortHandler = (property) => (event) => { - onRequestSort(event, property); - }; - - return ( - - - {headCells.map((headCell) => ( - - - {headCell.label} - {orderBy === headCell.id ? ( - - {order === 'desc' ? 'sorted descending' : 'sorted ascending'} - - ) : null} - - - ))} - - - ); -} - -EnhancedTableHead.propTypes = { - classes: PropTypes.object.isRequired, - numSelected: PropTypes.number.isRequired, - onRequestSort: PropTypes.func.isRequired, - onSelectAllClick: PropTypes.func.isRequired, - order: PropTypes.oneOf(['asc', 'desc']).isRequired, - orderBy: PropTypes.string.isRequired, - rowCount: PropTypes.number.isRequired, -}; - - -class TopTenSingers extends React.Component { - constructor(props) { - super(props); - this.handleClick = this.handleClick.bind(this); - this.state = {loading: true, duetData: []}; - } - - setDuetState(data) { - this.setState(data) - } - - render() { - const useStyles = makeStyles({ - table: { - minWidth: 650, - }, - }); - - const [order, setOrder] = React.useState('asc'); - const [orderBy, setOrderBy] = React.useState('calories'); - const classes = useStyles(); - - const handleRequestSort = (event, property) => { - const isAsc = orderBy === property && order === 'asc'; - setOrder(isAsc ? 'desc' : 'asc'); - setOrderBy(property); - }; - useEffect(() => { - // We should only fetch once! - if (this.state.loading || this.props.triggerRefresh) { - fetch(dataURL).then((res) => res.json().then((data)=>{ - this.setDuetState({duetData: data, loading: false }) - })); - } - }, [this.setState, this.state.loading, this.props.triggerRefresh]); - - if (this.state.loading) { - return

Sorry, still loading...

- } - - let dd = this.state.duetData - - return ( - - - - - Song Name - Sings - - - - {stableSort(dd, getComparator(order, orderBy)) - .map((row, index) => { - const labelId = `enhanced-table-${index}`; - - return ( - - - {row.singerName} - - {row.singCount} - - ); - })} - -
-
- ); - } -} - -export default TopTenSingers.render; diff --git a/internal/irc/irc.go b/internal/irc/irc.go index 3feaa49..757507c 100644 --- a/internal/irc/irc.go +++ b/internal/irc/irc.go @@ -95,13 +95,37 @@ type SingsVideoStruct struct { LastSungSinger time.Time `json:"LastSungSinger"` // Last time a duet was sung with this SINGER, regardless of song, only Duets have this date initialised } +// CommandType Kinda an enum +type CommandType string + +const ( + RandomSinger CommandType = "RandomSinger" + RandomPrompt CommandType = "RandomPrompt" + RandomSong CommandType = "RandomSong" + AgingSinger CommandType = "AgingSinger" + AgingSong CommandType = "AgingSong" +) + +func (ct CommandType) IsValid() error { + switch ct { + case RandomSinger, RandomPrompt, RandomSong, AgingSinger, AgingSong: + return nil + } + return errors.New("Invalid command type") +} + +type CommandStruct struct { + CommandName CommandType `json:"commandName"` + KeyWord string `json:"keyword"` +} + type ChannelData struct { - Name string `json:"name"` - AdminKey string `json:"value,omitempty"` - Command string `json:"customcommand,omitempty"` - ExtraStrings string `json:"extrastrings,omitempty"` - JoinTime time.Time `json:"jointime"` ControlChannel bool + Name string `json:"name"` + AdminKey string `json:"value,omitempty"` + Commands []CommandStruct `json:"commands,omitempty"` + ExtraStrings string `json:"extrastrings,omitempty"` + JoinTime time.Time `json:"jointime"` HasLeft bool `json:"hasleft"` VideoCache []SingsVideoStruct `json:"videoCache"` VideoCacheUpdated time.Time `json:"videoCacheUpdated"` @@ -199,7 +223,7 @@ func (bb *KardBot) HandleChat() error { if nil != matches { realUserName := matches[1] if bb.ChannelData[realUserName].Name == "" { - record := ChannelData{Name: realUserName, JoinTime: time.Now(), Command: "card", ControlChannel: true} + record := ChannelData{Name: realUserName, JoinTime: time.Now(), Commands: nil, ControlChannel: true} bb.Database.Write("channelData", realUserName, record) bb.ChannelData[realUserName] = record } @@ -233,18 +257,25 @@ func (bb *KardBot) HandleChat() error { cmdMatches := CmdRegex.FindStringSubmatch(msg) if nil != cmdMatches { cmd := cmdMatches[1] - - rgb.YPrintf("[%s] Checking cmd %s against %s\n", TimeStamp(), cmd, bb.ChannelData[channel].Command) + cardCommand := "" + commands := bb.ChannelData[channel].Commands + for _, command := range commands { + if command.CommandName == RandomPrompt { + cardCommand = command.KeyWord + } + } + rgb.YPrintf("[%s] Checking cmd %s against [%s]\n", TimeStamp(), cmd, bb.ChannelData[channel].Commands) switch cmd { - case bb.ChannelData[channel].Command: - rgb.CPrintf("[%s] Card asked for by %s on %s' channel!\n", TimeStamp(), userName, channel) - - bb.Say("Your prompt is : "+bb.Prompts[rand.Intn(len(bb.Prompts))], channel) + case cardCommand: + if cardCommand != "" { + rgb.CPrintf("[%s] Card asked for by %s on %s' channel!\n", TimeStamp(), userName, channel) + bb.Say("Your prompt is : "+bb.Prompts[rand.Intn(len(bb.Prompts))], channel) + } case "join": if bb.ChannelData[channel].ControlChannel { rgb.CPrintf("[%s] Join asked for by %s on %s' channel!\n", TimeStamp(), userName, channel) if bb.ChannelData[userName].Name == "" { - record := ChannelData{Name: userName, JoinTime: time.Now(), Command: "card", ControlChannel: true} + record := ChannelData{Name: userName, JoinTime: time.Now(), Commands: nil, ControlChannel: true} bb.Database.Write("channelData", userName, record) bb.ChannelData[userName] = record } @@ -460,7 +491,7 @@ func (bb *KardBot) readChannelData() error { records, err := bb.Database.ReadAll("channelData") if err != nil { // no db? initialise one? - record := ChannelData{Name: bb.Channel, JoinTime: time.Now(), Command: "card"} + record := ChannelData{Name: bb.Channel, JoinTime: time.Now(), Commands: nil} rgb.YPrintf("[%s] No channel table for #%s exists, creating...\n", TimeStamp(), bb.Channel) if err := bb.Database.Write("channelData", bb.Channel, record); err != nil { return err @@ -476,22 +507,11 @@ func (bb *KardBot) readChannelData() error { if err != nil { return err } - if record.Name != "" { - if record.Command == "" { - record.Command = "card" - - rgb.YPrintf("[%s] Rewriting data for #%s...\n", TimeStamp(), bb.Channel) - if err := bb.Database.Write("channelData", record.Name, record); err != nil { - return err - } - } - bb.ChannelData[record.Name] = record - } } // Managed to leave the main channel!? if bb.ChannelData[bb.Channel].Name == "" { rgb.YPrintf("[%s] No channel data for #%s exists, creating...\n", TimeStamp(), bb.Channel) - record := ChannelData{Name: bb.Channel, JoinTime: time.Now(), Command: "card"} + record := ChannelData{Name: bb.Channel, JoinTime: time.Now(), Commands: nil} bb.ChannelData[bb.Channel] = record if err := bb.Database.Write("channelData", bb.Channel, record); err != nil { return err diff --git a/internal/webserver/webserver.go b/internal/webserver/webserver.go index 3af72a9..1817ef5 100755 --- a/internal/webserver/webserver.go +++ b/internal/webserver/webserver.go @@ -217,84 +217,6 @@ func AugmentSingsVideoStructSlice(input []irc.SingsVideoStruct) []AugmentedSings return ret } -func AdminHandler(response http.ResponseWriter, request *http.Request) { - vars := mux.Vars(request) - if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey { - UnauthorizedHandler(response, request) - return - } - type TemplateData struct { - Channel string - Command string - ExtraStrings string - SinceTime time.Time - SinceTimeUTC string - Leaving bool - HasLeft bool - SongData []AugmentedSingsVideoStruct - TopNSongs []SongSings - TopNSingers []SingerSings - ChannelKey string - } - updateCacheIfNecessary(vars["channel"]) - channelData := ircBot.ChannelData[vars["channel"]] - updateCalculatedFields(channelData.VideoCache) - for _, song := range channelData.VideoCache { - if song.Duet && song.OtherSinger == "" { - fmt.Printf("WARNING: found duet with no other singer! %s", song.SongTitle) // should never happen but debug in case it does! - } - } - topNSongs := calculateTopNSongs(channelData.VideoCache, 10) - topNSingers := calculateTopNSingers(channelData.VideoCache, 10) - var td = TemplateData{channelData.Name, channelData.Command, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, AugmentSingsVideoStructSlice(channelData.VideoCache), topNSongs, topNSingers, vars["key"]} - - if request.Method == "POST" { - request.ParseForm() - if strings.Join(request.PostForm["leave"], ",") == "Leave twitch channel" { - td.Leaving = true - } else if strings.Join(request.PostForm["reallyleave"], ",") == "Really leave twitch channel" { - record := ircBot.ChannelData[vars["channel"]] - record.HasLeft = true - ircBot.ChannelData[vars["channel"]] = record - ircBot.LeaveChannel(vars["channel"]) - ircBot.Database.Write("channelData", vars["channel"], record) - LeaveHandler(response, request) - return - } - if strings.Join(request.PostForm["join"], ",") == "Come on in" { - record := ircBot.ChannelData[vars["channel"]] - td.HasLeft = false - record.Name = vars["channel"] - record.JoinTime = time.Now() - record.HasLeft = false - if record.Command == "" { - record.Command = "card" - } - ircBot.Database.Write("channelData", vars["channel"], record) - ircBot.ChannelData[vars["channel"]] = record - td = TemplateData{record.Name, record.Command, record.ExtraStrings, record.JoinTime, record.JoinTime.Format(irc.UTCFormat), false, record.HasLeft, AugmentSingsVideoStructSlice(channelData.VideoCache), topNSongs, topNSingers, vars["key"]} - ircBot.JoinChannel(record.Name) - } - sourceData := ircBot.ChannelData[vars["channel"]] - if strings.Join(request.PostForm["Command"], ",") != "" { - sourceData.Command = strings.Join(request.PostForm["Command"], ",") - td.Command = sourceData.Command - ircBot.ChannelData[vars["channel"]] = sourceData - } - if strings.Join(request.PostForm["ExtraStrings"], ",") != sourceData.ExtraStrings { - sourceData.ExtraStrings = strings.Join(request.PostForm["ExtraStrings"], ",") - td.ExtraStrings = sourceData.ExtraStrings - ircBot.ChannelData[vars["channel"]] = sourceData - } - ircBot.Database.Write("channelData", vars["channel"], sourceData) - } - tmpl, err := template.New("admin.html").ParseFiles("web/admin.html") - if err != nil { - panic(err.Error()) - } - tmpl.Execute(response, td) -} - func UnauthorizedHandler(response http.ResponseWriter, request *http.Request) { response.Header().Add("X-Template-File", "html"+request.URL.Path) response.WriteHeader(401) @@ -318,7 +240,11 @@ func twitchHTTPClient(call string, bearer string) (string, error) { defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) - return string([]byte(body)), nil + if resp.StatusCode != 200 { + return string(http.StatusText(resp.StatusCode)), errors.New("HTTP ERROR: " + http.StatusText(resp.StatusCode)) + } else { + return string([]byte(body)), nil + } } func ValidateTwitchBearerToken(bearer string) (bool, error) { @@ -479,6 +405,31 @@ func (h *KVHeap) Pop() interface{} { var cacheLock sync.Mutex +type CacheDetails struct { + Age time.Duration `json: "cache_age"` + AgeStr string `json: "cache_age_nice"` + SongCount int `json: "expires_in"` +} + +func getCacheDetails(channel string) CacheDetails { + var ret CacheDetails + channelData := ircBot.ChannelData[channel] + ret.Age = time.Now().Sub(channelData.VideoCacheUpdated) + ret.AgeStr = humanize.Time(channelData.VideoCacheUpdated) + ret.SongCount = len(channelData.VideoCache) + return ret +} + +func forceUpdateCache(channel string) { + fmt.Printf("Forcing cache update!") + channelData := ircBot.ChannelData[channel] + tenHours := time.Hour * -10 + videoCacheUpdated := time.Now().Add(tenHours) // Subtract 10 hours from now, cache is 10 hours old. + channelData.VideoCacheUpdated = videoCacheUpdated + ircBot.ChannelData[channel] = channelData + updateCacheIfNecessary(channel) +} + func updateCacheIfNecessary(channel string) { cacheLock.Lock() channelData := ircBot.ChannelData[channel] @@ -601,9 +552,11 @@ func fetchVoDsPagesRecursive(userID string, bearer string, from string) ([]irc.S func fetchAllVoDs(userID string, bearer string) ([]irc.SingsVideoStruct, error) { tokenValid, err := ValidateTwitchBearerToken(bearer) if err != nil { + fmt.Println("Error validating token : " + err.Error()) return nil, err } if !tokenValid { + fmt.Println("Error validating token (revoked?)") return nil, errors.New("Failed to validate token with twitch (authorization revoked?!)") } titles, err := fetchVoDsPagesRecursive(userID, bearer, "") @@ -746,7 +699,7 @@ func CSVHandler(response http.ResponseWriter, request *http.Request) { } type TemplateData struct { Channel string - Command string + Commands []irc.CommandStruct ExtraStrings string SinceTime time.Time SinceTimeUTC string @@ -760,7 +713,7 @@ func CSVHandler(response http.ResponseWriter, request *http.Request) { channelData := ircBot.ChannelData[vars["channel"]] topNSongs := calculateTopNSongs(channelData.VideoCache, 10) topNSingers := calculateTopNSingers(channelData.VideoCache, 10) - var td = TemplateData{channelData.Name, channelData.Command, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, AugmentSingsVideoStructSliceForCSV(channelData.VideoCache), topNSongs, topNSingers} + var td = TemplateData{channelData.Name, channelData.Commands, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, AugmentSingsVideoStructSliceForCSV(channelData.VideoCache), topNSongs, topNSingers} if request.URL.Path[0:4] == "/csv" { response.Header().Add("Content-Disposition", "attachment; filename=\"duets.csv\"") response.Header().Add("Content-type", "text/csv") @@ -782,7 +735,7 @@ func JSONHandler(response http.ResponseWriter, request *http.Request) { } type TemplateData struct { Channel string - Command string + Commands []irc.CommandStruct ExtraStrings string SinceTime time.Time SinceTimeUTC string @@ -794,9 +747,15 @@ func JSONHandler(response http.ResponseWriter, request *http.Request) { } updateCacheIfNecessary(vars["channel"]) channelData := ircBot.ChannelData[vars["channel"]] - topNSongs := calculateTopNSongs(channelData.VideoCache, 10) - topNSingers := calculateTopNSingers(channelData.VideoCache, 10) - var td = TemplateData{channelData.Name, channelData.Command, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, AugmentSingsVideoStructSlice(channelData.VideoCache), topNSongs, topNSingers} + + var topNSongs []SongSings + var topNSingers []SingerSings + + if request.URL.Path[0:4] != "/deb" { + topNSongs = calculateTopNSongs(channelData.VideoCache, 10) + topNSingers = calculateTopNSingers(channelData.VideoCache, 10) + } + var td = TemplateData{channelData.Name, channelData.Commands, channelData.ExtraStrings, channelData.JoinTime, channelData.JoinTime.Format(irc.UTCFormat), false, channelData.HasLeft, AugmentSingsVideoStructSlice(channelData.VideoCache), topNSongs, topNSingers} response.Header().Add("Content-type", "application/json") if request.URL.Path[0:5] == "/json" { tmpl := template.Must(template.ParseFiles("web/data.json")) @@ -810,6 +769,42 @@ func JSONHandler(response http.ResponseWriter, request *http.Request) { } } +func CacheDetailsHandler(response http.ResponseWriter, request *http.Request) { + vars := mux.Vars(request) + if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey { + UnauthorizedHandler(response, request) + return + } + deets := getCacheDetails(vars["channel"]) + response.Header().Add("Content-type", "application/json") + enc := json.NewEncoder(response) + enc.Encode(deets) +} + +func BotDetailsHandler(response http.ResponseWriter, request *http.Request) { + vars := mux.Vars(request) + if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey { + UnauthorizedHandler(response, request) + return + } + type ChannelDataSmaller struct { + Commands []irc.CommandStruct `json:"commands,omitempty"` + ExtraStrings string `json:"extrastrings,omitempty"` + JoinTime time.Time `json:"jointime"` + HasLeft bool `json:"hasleft"` + VideoCacheUpdated time.Time `json:"videoCacheUpdated"` + } + var deets ChannelDataSmaller + deets.Commands = ircBot.ChannelData[vars["channel"]].Commands + deets.ExtraStrings = ircBot.ChannelData[vars["channel"]].ExtraStrings + deets.JoinTime = ircBot.ChannelData[vars["channel"]].JoinTime + deets.HasLeft = ircBot.ChannelData[vars["channel"]].HasLeft + deets.VideoCacheUpdated = ircBot.ChannelData[vars["channel"]].VideoCacheUpdated + response.Header().Add("Content-type", "application/json") + enc := json.NewEncoder(response) + enc.Encode(deets) +} + func ReactIndexHandler(entrypoint string) func(w http.ResponseWriter, r *http.Request) { fn := func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, entrypoint) @@ -818,6 +813,33 @@ func ReactIndexHandler(entrypoint string) func(w http.ResponseWriter, r *http.Re return http.HandlerFunc(fn) } +func ForceRefreshHandler(response http.ResponseWriter, request *http.Request) { + vars := mux.Vars(request) + if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey { + UnauthorizedHandler(response, request) + return + } + forceUpdateCache(vars["channel"]) + response.Header().Add("Content-type", "application/json") + enc := json.NewEncoder(response) + enc.Encode(true) +} + +func JoinHandler(response http.ResponseWriter, request *http.Request) { + vars := mux.Vars(request) + if vars["key"] != ircBot.ChannelData[vars["channel"]].AdminKey { + UnauthorizedHandler(response, request) + return + } + record := ircBot.ChannelData[vars["channel"]] + record.Name = vars["channel"] + record.JoinTime = time.Now() + record.HasLeft = false + ircBot.Database.Write("channelData", vars["channel"], record) + ircBot.ChannelData[vars["channel"]] = record + ircBot.JoinChannel(record.Name) +} + func HandleHTTP(passedIrcBot *irc.KardBot) { ircBot = passedIrcBot r := mux.NewRouter() @@ -825,9 +847,14 @@ func HandleHTTP(passedIrcBot *irc.KardBot) { r.NotFoundHandler = http.HandlerFunc(NotFoundHandler) r.HandleFunc("/healthz", HealthHandler) r.HandleFunc("/web/{.*}", TemplateHandler) + r.HandleFunc("/cachedeets/{channel}/{key}", CacheDetailsHandler) + r.HandleFunc("/botdeets/{channel}/{key}", BotDetailsHandler) + r.HandleFunc("/join/{channel}/{key}", JoinHandler) + r.HandleFunc("/force/{channel}/{key}", ForceRefreshHandler) r.HandleFunc("/csv/{channel}/{key}", CSVHandler) r.HandleFunc("/tsv/{channel}/{key}", CSVHandler) r.HandleFunc("/json/{channel}/{key}", JSONHandler) + r.HandleFunc("/debug/{channel}/{key}", JSONHandler) r.HandleFunc("/topsongs/{channel}/{key}", JSONHandler) r.HandleFunc("/topsingers/{channel}/{key}", JSONHandler) r.Path("/twitchtobackend").Queries("access_token", "{access_token}", "scope", "{scope}", "token_type", "{token_type}").HandlerFunc(TwitchBackendHandler)