{type}
diff --git a/client/app/pages/Clips/Clips.tsx b/client/app/pages/Clips/Clips.tsx
index ea85d3d..3cb33a2 100644
--- a/client/app/pages/Clips/Clips.tsx
+++ b/client/app/pages/Clips/Clips.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import axios from 'axios';
+import { axios } from '../../services';
import { SoundList, SoundType } from '../../components/SoundList';
diff --git a/client/app/pages/Downloader/Downloader.tsx b/client/app/pages/Downloader/Downloader.tsx
index 32cff3f..f93d6cd 100644
--- a/client/app/pages/Downloader/Downloader.tsx
+++ b/client/app/pages/Downloader/Downloader.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import axios from 'axios';
+import { axios } from '../../services';
import './Downloader.scss';
diff --git a/client/app/pages/Home/Home.scss b/client/app/pages/Home/Home.scss
index c6e73a8..e69de29 100644
--- a/client/app/pages/Home/Home.scss
+++ b/client/app/pages/Home/Home.scss
@@ -1,3 +0,0 @@
-.Home {
- padding: 10px;
-}
diff --git a/client/app/pages/Home/Home.tsx b/client/app/pages/Home/Home.tsx
index 25018c3..70e57e3 100644
--- a/client/app/pages/Home/Home.tsx
+++ b/client/app/pages/Home/Home.tsx
@@ -9,9 +9,9 @@ interface State {}
export class Home extends React.Component
{
render() {
return (
-
-
-
Go Discord Bot
+
+
+
Go Discord Bot
04-09-18 Update
diff --git a/client/app/pages/Pubg/Pubg.tsx b/client/app/pages/Pubg/Pubg.tsx
index b123a66..e5896b7 100644
--- a/client/app/pages/Pubg/Pubg.tsx
+++ b/client/app/pages/Pubg/Pubg.tsx
@@ -3,7 +3,7 @@
*/
import React from 'react';
-import axios from 'axios';
+import { axios } from '../../services';
import * as _ from 'lodash';
import './Pubg.scss';
diff --git a/client/app/pages/Soundboard/Soundboard.tsx b/client/app/pages/Soundboard/Soundboard.tsx
index 78d2a14..9ae91ab 100644
--- a/client/app/pages/Soundboard/Soundboard.tsx
+++ b/client/app/pages/Soundboard/Soundboard.tsx
@@ -1,11 +1,9 @@
import React from 'react';
import Dropzone from 'react-dropzone';
-import axios, { AxiosRequestConfig } from 'axios';
-
+import { axios } from '../../services';
import { SoundList, SoundType } from '../../components/SoundList';
-
import './Soundboard.scss';
-import { storage } from '../../storage';
+import { AxiosRequestConfig } from 'axios';
let self: any;
@@ -37,7 +35,6 @@ export class Soundboard extends React.Component {
this.config = {
headers: {
'Content-Type': 'multipart/form-data',
- Authorization: `Bearer ${storage.getJWT()}`,
},
onUploadProgress: progressEvent => {
this.setState({
diff --git a/client/app/pages/oauth/oauth.tsx b/client/app/pages/oauth/oauth.tsx
index 2d05259..42d9c19 100644
--- a/client/app/pages/oauth/oauth.tsx
+++ b/client/app/pages/oauth/oauth.tsx
@@ -1,8 +1,7 @@
import React from 'react';
-import axios from 'axios';
+import { axios, StorageService } from '../../services';
import queryString from 'query-string';
import { RouteComponentProps } from 'react-router-dom';
-import { storage } from '../../storage';
interface Props extends RouteComponentProps {}
@@ -25,7 +24,7 @@ export class Oauth extends React.Component {
private async fetchOauth(code: string) {
try {
const res = await axios.post('/api/oauth', { code });
- storage.setJWT(res.data);
+ StorageService.setJWT(res.data);
window.location.href = '/';
} catch (e) {
console.error(e);
diff --git a/client/app/pages/stats/stats.scss b/client/app/pages/stats/stats.scss
new file mode 100644
index 0000000..9872125
--- /dev/null
+++ b/client/app/pages/stats/stats.scss
@@ -0,0 +1,3 @@
+.Stats {
+ padding: 10px;
+}
diff --git a/client/app/pages/stats/stats.tsx b/client/app/pages/stats/stats.tsx
new file mode 100644
index 0000000..8c0eaca
--- /dev/null
+++ b/client/app/pages/stats/stats.tsx
@@ -0,0 +1,70 @@
+import React, { Component } from 'react';
+import { HorizontalBar } from 'react-chartjs-2';
+import { chain, map } from 'lodash';
+import { axios } from '../../services';
+import './stats.scss';
+
+interface IState {
+ data: {
+ username: string;
+ count: number;
+ }[];
+}
+
+/**
+ * a page to show discord chat statistics
+ * currently keeps track of number messages that contain external links
+ */
+export class Stats extends Component {
+ constructor(props: any) {
+ super(props);
+ this.state = {
+ data: [],
+ };
+ }
+
+ componentDidMount() {
+ this.getdata();
+ }
+
+ async getdata() {
+ const messages = await axios.get('/api/logger/linkedmessages');
+ const data: any = chain(messages.data)
+ .map((v, k) => {
+ return { username: k, count: v };
+ })
+ .orderBy(v => v.count, 'desc')
+ .value();
+
+ this.setState({ data });
+ }
+
+ render() {
+ const data: any = {
+ labels: map(this.state.data, v => v.username),
+ datasets: [
+ {
+ label: 'Count',
+ backgroundColor: 'rgba(114,137,218, 0.4)',
+ borderColor: 'rgba(114,137,218, 0.9)',
+ borderWidth: 1,
+ hoverBackgroundColor: 'rgba(114,137,218, 0.6)',
+ hoverBorderColor: 'rgba(114,137,218, 1)',
+ data: map(this.state.data, v => v.count),
+ },
+ ],
+ options: {
+ responsive: true,
+ },
+ };
+
+ return (
+
+ );
+ }
+}
diff --git a/client/app/scss/style.scss b/client/app/scss/style.scss
index 180d47e..c76bd8a 100644
--- a/client/app/scss/style.scss
+++ b/client/app/scss/style.scss
@@ -25,6 +25,10 @@ body {
padding-left: $navbarWidth;
}
+.content {
+ padding: 10px;
+}
+
.input {
border-radius: 3px;
border: 1px solid $lightGray;
@@ -56,7 +60,7 @@ body {
}
}
-.Card {
+.card {
background-color: $gray2;
border-radius: 5px;
max-width: 800px;
@@ -65,7 +69,7 @@ body {
border: 1px solid $gray3;
}
-.Card__header {
+.card__header {
margin: -10px -10px 10px -10px;
display: flex;
align-items: center;
diff --git a/client/app/services/axios.service.ts b/client/app/services/axios.service.ts
new file mode 100644
index 0000000..7a637e0
--- /dev/null
+++ b/client/app/services/axios.service.ts
@@ -0,0 +1,19 @@
+import ax from 'axios';
+import { StorageService } from './storage.service';
+
+export const axios = ax.create();
+
+axios.interceptors.request.use(
+ config => {
+ const jwt = StorageService.getJWT();
+ if (jwt) {
+ config.headers['Authorization'] = `Bearer ${jwt}`;
+ }
+ // Do something before request is sent
+ return config;
+ },
+ function(error) {
+ // Do something with request error
+ return Promise.reject(error);
+ },
+);
diff --git a/client/app/services/index.ts b/client/app/services/index.ts
new file mode 100644
index 0000000..f1bad6e
--- /dev/null
+++ b/client/app/services/index.ts
@@ -0,0 +1,2 @@
+export * from './axios.service';
+export * from './storage.service';
diff --git a/client/app/storage.ts b/client/app/services/storage.service.ts
similarity index 66%
rename from client/app/storage.ts
rename to client/app/services/storage.service.ts
index 7a43284..191d17a 100644
--- a/client/app/storage.ts
+++ b/client/app/services/storage.service.ts
@@ -1,3 +1,7 @@
+const clear = () => {
+ localStorage.clear();
+};
+
const setJWT = (token: string) => {
localStorage.setItem('jwt', token);
};
@@ -6,7 +10,8 @@ const getJWT = (): string | null => {
return localStorage.getItem('jwt');
};
-export const storage = {
+export const StorageService = {
+ clear,
getJWT,
setJWT,
};
diff --git a/client/package-lock.json b/client/package-lock.json
index 73ebab7..b4b7dcd 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -9,6 +9,11 @@
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz",
"integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow=="
},
+ "@types/chart.js": {
+ "version": "2.7.11",
+ "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.7.11.tgz",
+ "integrity": "sha512-56jTNUUBLJ7zr+CSN3S3SBBPYPzPOgAtmoBhscANfCQ7F+AUnXbdS2c7RX0KyGyMzJu3LMfVKWU6lS+JDug/Vw=="
+ },
"@types/history": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.6.2.tgz",
@@ -42,6 +47,14 @@
"csstype": "2.1.1"
}
},
+ "@types/react-chartjs-2": {
+ "version": "2.5.7",
+ "resolved": "https://registry.npmjs.org/@types/react-chartjs-2/-/react-chartjs-2-2.5.7.tgz",
+ "integrity": "sha512-waqYqiNULIVUqaKO7MGUpFmWrVtH7gVPOzqwV4y4zgUyu/JiDwC005PpveO442HKnby9kLgp3t1SB2sld+ACLw==",
+ "requires": {
+ "react-chartjs-2": "2.7.0"
+ }
+ },
"@types/react-dom": {
"version": "16.0.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.0.4.tgz",
@@ -1761,6 +1774,39 @@
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
"integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I="
},
+ "chart.js": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.7.2.tgz",
+ "integrity": "sha512-90wl3V9xRZ8tnMvMlpcW+0Yg13BelsGS9P9t0ClaDxv/hdypHDr/YAGf+728m11P5ljwyB0ZHfPKCapZFqSqYA==",
+ "requires": {
+ "chartjs-color": "2.2.0",
+ "moment": "2.22.0"
+ }
+ },
+ "chartjs-color": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.2.0.tgz",
+ "integrity": "sha1-hKL7dVeH7YXDndbdjHsdiEKbrq4=",
+ "requires": {
+ "chartjs-color-string": "0.5.0",
+ "color-convert": "0.5.3"
+ },
+ "dependencies": {
+ "color-convert": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz",
+ "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0="
+ }
+ }
+ },
+ "chartjs-color-string": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz",
+ "integrity": "sha512-amWNvCOXlOUYxZVDSa0YOab5K/lmEhbFNKI55PWc4mlv28BDzA7zaoQTGxSBgJMHIW+hGX8YUrvw/FH4LyhwSQ==",
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
"cheerio": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.19.0.tgz",
@@ -6799,6 +6845,11 @@
"minimist": "0.0.8"
}
},
+ "moment": {
+ "version": "2.22.0",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.0.tgz",
+ "integrity": "sha512-1muXCh8jb1N/gHRbn9VDUBr0GYb8A/aVcHlII9QSB68a50spqEVLIGN6KVmCOnSvJrUhC0edGgKU5ofnGXdYdg=="
+ },
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -9166,6 +9217,15 @@
"prop-types": "15.6.1"
}
},
+ "react-chartjs-2": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-2.7.0.tgz",
+ "integrity": "sha512-DkT9TVi+/wKaEWL2iI2ka2L3Q0kq+y7TrAqlWNETAqE5CTPphPg1Af3w9X2uQCQ1ZA60Q3base9mK3dRtlb9bQ==",
+ "requires": {
+ "lodash": "4.17.5",
+ "prop-types": "15.6.1"
+ }
+ },
"react-dom": {
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.3.1.tgz",
diff --git a/client/package.json b/client/package.json
index 9d5ef55..ef19f2a 100644
--- a/client/package.json
+++ b/client/package.json
@@ -10,11 +10,13 @@
"author": "Mitchell Gerber",
"license": "MIT",
"dependencies": {
+ "@types/chart.js": "^2.7.11",
"@types/jwt-decode": "^2.2.1",
"@types/lodash": "^4.14.71",
"@types/node": "^9.4.6",
"@types/query-string": "^5.1.0",
"@types/react": "^16.0.0",
+ "@types/react-chartjs-2": "^2.5.7",
"@types/react-dom": "^16.0.4",
"@types/react-dropzone": "^4.2.0",
"@types/react-router-dom": "^4.2.6",
@@ -27,6 +29,7 @@
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"babel-preset-stage-0": "^6.16.0",
+ "chart.js": "^2.7.2",
"clean-webpack-plugin": "^0.1.14",
"css-loader": "^0.28.11",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
@@ -41,6 +44,7 @@
"postcss-loader": "^2.1.3",
"query-string": "^6.0.0",
"react": "^16.3.1",
+ "react-chartjs-2": "^2.7.0",
"react-dom": "^16.3.1",
"react-dropzone": "^4.2.9",
"react-router-dom": "^4.2.2",
diff --git a/server/logger/logger.go b/server/logger/model.go
similarity index 100%
rename from server/logger/logger.go
rename to server/logger/model.go
diff --git a/server/logger/operations.go b/server/logger/operations.go
new file mode 100644
index 0000000..2958160
--- /dev/null
+++ b/server/logger/operations.go
@@ -0,0 +1,73 @@
+package logger
+
+import (
+ "regexp"
+ "time"
+
+ "github.com/mgerb/go-discord-bot/server/db"
+)
+
+const urlRegexp = `https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)`
+
+var linkedPostsCacheTimeout time.Time
+var linkedPostsCache map[string]int
+
+// GetMessages - returns all messages - must use paging
+func GetMessages(page int) ([]Message, error) {
+ messages := []Message{}
+ err := db.Conn.Offset(page*100).Limit(100).Order("timestamp desc", true).Preload("User").Find(&messages).Error
+ return messages, err
+}
+
+// GetLinkedMessages - get count of discord comments that contain URL's - per user
+// cached for 10 minutes because there is a lot of data filtering
+func GetLinkedMessages() (map[string]int, error) {
+
+ if linkedPostsCacheTimeout.After(time.Now().Add(-10 * time.Minute)) {
+ return linkedPostsCache, nil
+ }
+
+ result := []map[string]interface{}{}
+ rows, err := db.Conn.Table("messages").
+ Select("users.username, messages.content").
+ Joins("join users on messages.user_id = users.id").
+ Rows()
+
+ if err != nil {
+ return map[string]int{}, err
+ }
+
+ for rows.Next() {
+ var username, content string
+ rows.Scan(&username, &content)
+ result = append(result, map[string]interface{}{
+ "username": username,
+ "content": content,
+ })
+ }
+
+ linkedPostsCacheTimeout = time.Now()
+ linkedPostsCache = groupPosts(result)
+
+ return linkedPostsCache, nil
+}
+
+// group posts by user and count
+func groupPosts(posts []map[string]interface{}) map[string]int {
+
+ result := map[string]int{}
+
+ for _, p := range posts {
+ match, _ := regexp.MatchString(urlRegexp, p["content"].(string))
+
+ if match {
+ if _, ok := result[p["username"].(string)]; ok {
+ result[p["username"].(string)]++
+ } else {
+ result[p["username"].(string)] = 1
+ }
+ }
+ }
+
+ return result
+}
diff --git a/server/webserver/handlers/logger.go b/server/webserver/handlers/logger.go
index cefe70a..f7fe4d3 100644
--- a/server/webserver/handlers/logger.go
+++ b/server/webserver/handlers/logger.go
@@ -4,20 +4,18 @@ import (
"strconv"
"github.com/gin-gonic/gin"
- "github.com/mgerb/go-discord-bot/server/db"
"github.com/mgerb/go-discord-bot/server/logger"
)
-// GetLogs - get all logs
-func GetLogs(c *gin.Context) {
+// GetMessages - get all messages
+func GetMessages(c *gin.Context) {
page, err := strconv.Atoi(c.Query("page"))
if err != nil {
page = 0
}
- messages := []logger.Message{}
- err = db.Conn.Offset(page*100).Limit(100).Order("timestamp desc", true).Preload("User").Find(&messages).Error
+ messages, err := logger.GetMessages(page)
if err != nil {
c.JSON(500, err)
@@ -26,3 +24,15 @@ func GetLogs(c *gin.Context) {
c.JSON(200, messages)
}
+
+// GetLinkedMessages -
+func GetLinkedMessages(c *gin.Context) {
+ posts, err := logger.GetLinkedMessages()
+
+ if err != nil {
+ c.JSON(500, err.Error())
+ return
+ }
+
+ c.JSON(200, posts)
+}
diff --git a/server/webserver/server.go b/server/webserver/server.go
index ac50dab..9797027 100644
--- a/server/webserver/server.go
+++ b/server/webserver/server.go
@@ -23,8 +23,9 @@ func getRouter() *gin.Engine {
api.GET("/ytdownloader", handlers.Downloader)
api.GET("/soundlist", handlers.SoundList)
api.GET("/cliplist", handlers.ClipList)
- api.GET("/logs", handlers.GetLogs)
api.POST("/oauth", handlers.Oauth)
+ api.GET("/logger/messages", handlers.GetMessages)
+ api.GET("/logger/linkedmessages", handlers.GetLinkedMessages)
authorizedAPI := router.Group("/api")
authorizedAPI.Use(middleware.AuthorizedJWT())