Add Daily Email reports feature (#4)
This commit is contained in:
parent
903dab597f
commit
85f16f1d0e
|
@ -1,2 +1,3 @@
|
||||||
.idea/
|
.idea/
|
||||||
data/
|
data/
|
||||||
|
config.json
|
||||||
|
|
55
README.md
55
README.md
|
@ -6,27 +6,41 @@
|
||||||
|
|
||||||
KISSS is really easy to install via Docker.
|
KISSS is really easy to install via Docker.
|
||||||
|
|
||||||
docker run -d --name kis3 -e ENVVAR=VARVALUE -e ... -p 8080:8080 -v kis3:/app/data kis3/kis3
|
docker run -d --name kis3 -p 8080:8080 -v kis3:/app/data -v ${pwd}/config.json:/app/config.json kis3/kis3
|
||||||
|
|
||||||
Replace ENVVAR and VARVALUE with the environment variables from the configuration.
|
|
||||||
|
|
||||||
Depending on your setup, replace `-p 8080:8080` with your custom port configuration. KISSS listens to port 8080 by default, but you can also change this via the configuration.
|
Depending on your setup, replace `-p 8080:8080` with your custom port configuration. KISSS listens to port 8080 by default, but you can also change this via the configuration.
|
||||||
|
|
||||||
To persist the data KISSS collects, you should mount a volume or a folder to `/app/data`. When mounting an folder, give writing permissions to UID 100, because it is a non-root image to make it more secure.
|
To persist the data KISSS collects, you should mount a volume or a folder to `/app/data`. When mounting an folder, give writing permissions to UID 100, because it is a non-root image to make it more secure.
|
||||||
|
|
||||||
|
You should also mount a configuration file to `/app/config.json`.
|
||||||
|
|
||||||
It's also possible to use KISSS without Docker, but for that you need to compile it yourself. In the future there will be executables without dependencies available.
|
It's also possible to use KISSS without Docker, but for that you need to compile it yourself. In the future there will be executables without dependencies available.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
You can configure some settings using environment variables:
|
You can configure some settings using a `config.json` file in the working directory or provide a custom path to it using the `-c` CLI flag:
|
||||||
|
|
||||||
`PORT` (`8080`): Set the port to which KISSS should listen
|
`port` (`8080`): Set the port to which KISSS should listen
|
||||||
|
|
||||||
`DNT` (`true`): Set whether or not KISSS should respect Do-Not-Track headers some browsers send
|
`dnt` (`true`): Set whether or not KISSS should respect Do-Not-Track headers some browsers send
|
||||||
|
|
||||||
`DB_PATH` (`data/kis3.db`): Set the path for the SQLite database (relative to the working directory - in the Docker container it's `/app`).
|
`dbPath` (`data/kis3.db`): Set the path for the SQLite database (relative to the working directory - in the Docker container it's `/app`).
|
||||||
|
|
||||||
You can make the statistics private and only accessible with authentication by setting both `STATS_USERNAME` and `STATS_PASSWORD` to a username and password. If only one or none is set, the statistics are accessible without authorization and public to anyone.
|
You can make the statistics private and only accessible with authentication by setting both `statsUsername` and `statsPassword` to a username and password. If only one or none is set, the statistics are accessible without authorization and public to anyone.
|
||||||
|
|
||||||
|
The configuration file can look like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"port": 8080,
|
||||||
|
"dnt": true,
|
||||||
|
"dbPath": "data/kis3.db",
|
||||||
|
"statsUsername": "myusername",
|
||||||
|
"statsPassword": "mysecretpassword"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you specify an environment variable (`PORT`, `DNT`, `DB_PATH`, `STATS_USERNAME`, `STATS_PASSWORD`), that will override the settings from the configuration file.
|
||||||
|
|
||||||
## Add to website
|
## Add to website
|
||||||
|
|
||||||
|
@ -56,6 +70,31 @@ The following filters are available:
|
||||||
|
|
||||||
`format`: the format to represent the data, default is `plain` for a simple plain text list, `json` for a JSON response or `chart` for a chart generated with ChartJS in the browser
|
`format`: the format to represent the data, default is `plain` for a simple plain text list, `json` for a JSON response or `chart` for a chart generated with ChartJS in the browser
|
||||||
|
|
||||||
|
## Daily email reports
|
||||||
|
|
||||||
|
KISSS has a feature that can send you daily email reports. It basically requests the statistics and sends the response via email. You can configure it by adding report configurations to the configuration file:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
// Other configurations...
|
||||||
|
"reports": [
|
||||||
|
{
|
||||||
|
"name": "Daily stats from KISSS",
|
||||||
|
"time": "15:00",
|
||||||
|
"query": "view=pages&orderrow=second&order=desc",
|
||||||
|
"from": "myemailaddress@mydomain.tld",
|
||||||
|
"to": "myemailaddress@mydomain.tld",
|
||||||
|
"smtpHost": "mail.mydomain.tld:587",
|
||||||
|
"smtpUser": "myemailaddress@mydomain.tld",
|
||||||
|
"smtpPassword": "mysecretpassword"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Additional reports...
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
KISSS is licensed under the MIT license, so you can do basically everything with it, but nevertheless, please contribute your improvements to make KISSS better for everyone. See the LICENSE.txt file.
|
KISSS is licensed under the MIT license, so you can do basically everything with it, but nevertheless, please contribute your improvements to make KISSS better for everyone. See the LICENSE.txt file.
|
||||||
|
|
105
config.go
105
config.go
|
@ -1,68 +1,87 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
port string
|
Port string `json:"port"`
|
||||||
dnt bool
|
Dnt bool `json:"dnt"`
|
||||||
dbPath string
|
DbPath string `json:"dbPath"`
|
||||||
statsAuth bool
|
StatsUsername string `json:"statsUsername"`
|
||||||
statsUsername string
|
StatsPassword string `json:"statsPassword"`
|
||||||
statsPassword string
|
Reports []report `json:"reports"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type report struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
To string `json:"to"`
|
||||||
|
SmtpUser string `json:"smtpUser"`
|
||||||
|
SmtpPassword string `json:"smtpPassword"`
|
||||||
|
SmtpHost string `json:"smtpHost"`
|
||||||
|
From string `json:"from"`
|
||||||
|
Query string `json:"query"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
appConfig = &config{}
|
appConfig = &config{
|
||||||
|
Port: "8080",
|
||||||
|
Dnt: true,
|
||||||
|
DbPath: "data/kis3.db",
|
||||||
|
StatsUsername: "",
|
||||||
|
StatsPassword: "",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
appConfig.port = port()
|
parseConfigFile(appConfig)
|
||||||
appConfig.dnt = dnt()
|
// Replace values that are set via environment vars (to make it compatible with old method)
|
||||||
appConfig.dbPath = dbPath()
|
overwriteEnvVarValues(appConfig)
|
||||||
appConfig.statsUsername = statsUsername()
|
|
||||||
appConfig.statsPassword = statsPassword()
|
|
||||||
appConfig.statsAuth = statsAuth(appConfig)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func port() string {
|
func parseConfigFile(appConfig *config) {
|
||||||
port := os.Getenv("PORT")
|
configFile := flag.String("c", "config.json", "Config file")
|
||||||
if len(port) != 0 {
|
flag.Parse()
|
||||||
return port
|
configJson, e := ioutil.ReadFile(*configFile)
|
||||||
} else {
|
|
||||||
return "8080"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dnt() bool {
|
|
||||||
dnt := os.Getenv("DNT")
|
|
||||||
dntBool, e := strconv.ParseBool(dnt)
|
|
||||||
if e != nil {
|
if e != nil {
|
||||||
dntBool = true
|
return
|
||||||
}
|
}
|
||||||
return dntBool
|
e = json.Unmarshal([]byte(configJson), appConfig)
|
||||||
}
|
if e != nil {
|
||||||
|
return
|
||||||
func dbPath() (dbPath string) {
|
|
||||||
dbPath = os.Getenv("DB_PATH")
|
|
||||||
if len(dbPath) == 0 {
|
|
||||||
dbPath = "data/kis3.db"
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func statsUsername() (username string) {
|
func overwriteEnvVarValues(appConfig *config) {
|
||||||
username = os.Getenv("STATS_USERNAME")
|
port, set := os.LookupEnv("PORT")
|
||||||
return
|
if set {
|
||||||
|
appConfig.Port = port
|
||||||
|
}
|
||||||
|
dntString, set := os.LookupEnv("DNT")
|
||||||
|
dntBool, e := strconv.ParseBool(dntString)
|
||||||
|
if set && e == nil {
|
||||||
|
appConfig.Dnt = dntBool
|
||||||
|
}
|
||||||
|
dbPath, set := os.LookupEnv("DB_PATH")
|
||||||
|
if set {
|
||||||
|
appConfig.DbPath = dbPath
|
||||||
|
}
|
||||||
|
username, set := os.LookupEnv("STATS_USERNAME")
|
||||||
|
if set {
|
||||||
|
appConfig.StatsUsername = username
|
||||||
|
}
|
||||||
|
password, set := os.LookupEnv("STATS_PASSWORD")
|
||||||
|
if set {
|
||||||
|
appConfig.StatsPassword = password
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func statsPassword() (password string) {
|
func (ac *config) statsAuth() bool {
|
||||||
password = os.Getenv("STATS_PASSWORD")
|
return len(ac.StatsUsername) > 0 && len(ac.StatsPassword) > 0
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func statsAuth(ac *config) bool {
|
|
||||||
return len(ac.statsUsername) > 0 && len(ac.statsPassword) > 0
|
|
||||||
}
|
}
|
||||||
|
|
131
config_test.go
131
config_test.go
|
@ -1,131 +1,32 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_port(t *testing.T) {
|
func Test_config_statsAuth(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
StatsUsername string
|
||||||
|
StatsPassword string
|
||||||
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
envVar string
|
fields fields
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{name: "default", envVar: "", want: "8080"},
|
|
||||||
{name: "custom", envVar: "1234", want: "1234"},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
_ = os.Setenv("PORT", tt.envVar)
|
|
||||||
if got := port(); got != tt.want {
|
|
||||||
t.Errorf("port() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_dnt(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
envVar string
|
|
||||||
want bool
|
want bool
|
||||||
}{
|
}{
|
||||||
{name: "default", envVar: "", want: true},
|
{"No username nor password", fields{"", ""}, false},
|
||||||
{envVar: "true", want: true},
|
{"Only username", fields{"abc", ""}, false},
|
||||||
{envVar: "t", want: true},
|
{"Only password", fields{"", "abc"}, false},
|
||||||
{envVar: "TRUE", want: true},
|
{"Username and password", fields{"abc", "abc"}, true},
|
||||||
{envVar: "1", want: true},
|
|
||||||
{envVar: "false", want: false},
|
|
||||||
{envVar: "f", want: false},
|
|
||||||
{envVar: "0", want: false},
|
|
||||||
{envVar: "abc", want: true},
|
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
_ = os.Setenv("DNT", tt.envVar)
|
ac := &config{
|
||||||
if got := dnt(); got != tt.want {
|
StatsUsername: tt.fields.StatsUsername,
|
||||||
t.Errorf("dnt() = %v, want %v", got, tt.want)
|
StatsPassword: tt.fields.StatsPassword,
|
||||||
}
|
}
|
||||||
})
|
if got := ac.statsAuth(); got != tt.want {
|
||||||
}
|
t.Errorf("config.statsAuth() = %v, want %v", got, tt.want)
|
||||||
}
|
|
||||||
|
|
||||||
func Test_dbPath(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
envVar string
|
|
||||||
wantDbPath string
|
|
||||||
}{
|
|
||||||
{name: "default", envVar: "", wantDbPath: "data/kis3.db"},
|
|
||||||
{envVar: "kis3.db", wantDbPath: "kis3.db"},
|
|
||||||
{envVar: "data.db", wantDbPath: "data.db"},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
_ = os.Setenv("DB_PATH", tt.envVar)
|
|
||||||
if gotDbPath := dbPath(); gotDbPath != tt.wantDbPath {
|
|
||||||
t.Errorf("dbPath() = %v, want %v", gotDbPath, tt.wantDbPath)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_statsUsername(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
envVar string
|
|
||||||
wantUsername string
|
|
||||||
}{
|
|
||||||
{name: "default", envVar: "", wantUsername: ""},
|
|
||||||
{envVar: "abc", wantUsername: "abc"},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
_ = os.Setenv("STATS_USERNAME", tt.envVar)
|
|
||||||
if gotUsername := statsUsername(); gotUsername != tt.wantUsername {
|
|
||||||
t.Errorf("statsUsername() = %v, want %v", gotUsername, tt.wantUsername)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_statsPassword(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
envVar string
|
|
||||||
wantPassword string
|
|
||||||
}{
|
|
||||||
{name: "default", envVar: "", wantPassword: ""},
|
|
||||||
{envVar: "def", wantPassword: "def"},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
_ = os.Setenv("STATS_PASSWORD", tt.envVar)
|
|
||||||
if gotPassword := statsPassword(); gotPassword != tt.wantPassword {
|
|
||||||
t.Errorf("statsPassword() = %v, want %v", gotPassword, tt.wantPassword)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_statsAuth(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
ac *config
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{name: "default", args: struct{ ac *config }{ac: &config{}}, want: false},
|
|
||||||
{name: "only username set", args: struct{ ac *config }{ac: &config{statsUsername: "abc"}}, want: false},
|
|
||||||
{name: "only password set", args: struct{ ac *config }{ac: &config{statsPassword: "def"}}, want: false},
|
|
||||||
{name: "username and password set", args: struct{ ac *config }{ac: &config{statsUsername: "abc", statsPassword: "def"}}, want: true},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := statsAuth(tt.args.ac); got != tt.want {
|
|
||||||
t.Errorf("statsAuth() = %v, want %v", got, tt.want)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,10 +19,10 @@ type Database struct {
|
||||||
|
|
||||||
func initDatabase() (database *Database, e error) {
|
func initDatabase() (database *Database, e error) {
|
||||||
database = &Database{}
|
database = &Database{}
|
||||||
if _, err := os.Stat(appConfig.dbPath); os.IsNotExist(err) {
|
if _, err := os.Stat(appConfig.DbPath); os.IsNotExist(err) {
|
||||||
_ = os.MkdirAll(filepath.Dir(appConfig.dbPath), os.ModePerm)
|
_ = os.MkdirAll(filepath.Dir(appConfig.DbPath), os.ModePerm)
|
||||||
}
|
}
|
||||||
database.sqlDB, e = sql.Open("sqlite3", appConfig.dbPath)
|
database.sqlDB, e = sql.Open("sqlite3", appConfig.DbPath)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -6,12 +6,15 @@ require (
|
||||||
github.com/go-sql-driver/mysql v1.4.1 // indirect
|
github.com/go-sql-driver/mysql v1.4.1 // indirect
|
||||||
github.com/gobuffalo/packr v1.25.0 // indirect
|
github.com/gobuffalo/packr v1.25.0 // indirect
|
||||||
github.com/gobuffalo/packr/v2 v2.2.0
|
github.com/gobuffalo/packr/v2 v2.2.0
|
||||||
|
github.com/google/uuid v1.1.1 // indirect
|
||||||
github.com/gorilla/handlers v1.4.0
|
github.com/gorilla/handlers v1.4.0
|
||||||
github.com/gorilla/mux v1.7.1
|
github.com/gorilla/mux v1.7.1
|
||||||
|
github.com/jordan-wright/email v0.0.0-20190218024454-3ea4d25e7cf8
|
||||||
github.com/lib/pq v1.1.0 // indirect
|
github.com/lib/pq v1.1.0 // indirect
|
||||||
github.com/mattn/go-sqlite3 v0.0.0-20190424093727-5994cc52dfa8
|
github.com/mattn/go-sqlite3 v0.0.0-20190424093727-5994cc52dfa8
|
||||||
github.com/mssola/user_agent v0.5.0
|
github.com/mssola/user_agent v0.5.0
|
||||||
github.com/rubenv/sql-migrate v0.0.0-20190327083759-54bad0a9b051
|
github.com/rubenv/sql-migrate v0.0.0-20190327083759-54bad0a9b051
|
||||||
|
github.com/whiteshtef/clockwork v0.0.0-20190417075149-ecf7d9abe8ec
|
||||||
github.com/ziutek/mymysql v1.5.4 // indirect
|
github.com/ziutek/mymysql v1.5.4 // indirect
|
||||||
google.golang.org/appengine v1.5.0 // indirect
|
google.golang.org/appengine v1.5.0 // indirect
|
||||||
gopkg.in/gorp.v1 v1.7.2 // indirect
|
gopkg.in/gorp.v1 v1.7.2 // indirect
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -24,10 +24,14 @@ github.com/gobuffalo/packr/v2 v2.2.0 h1:Ir9W9XIm9j7bhhkKE9cokvtTl1vBm62A/fene/ZC
|
||||||
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
|
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
|
||||||
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||||
github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
|
github.com/jordan-wright/email v0.0.0-20190218024454-3ea4d25e7cf8 h1:XMe1IsRiRx3E3M50BhP7327VYF4A9RpCFfhHUFW+IeE=
|
||||||
|
github.com/jordan-wright/email v0.0.0-20190218024454-3ea4d25e7cf8/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||||
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
|
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
@ -55,6 +59,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/whiteshtef/clockwork v0.0.0-20190417075149-ecf7d9abe8ec h1:4mCJZnO75zjolpdsj/ToKe7X1oLWm+JJHwS1ez8BkXY=
|
||||||
|
github.com/whiteshtef/clockwork v0.0.0-20190417075149-ecf7d9abe8ec/go.mod h1:6o8H8sci2q3QxZ4p/U88ggqZuhY3mg34+WE5BuazLsU=
|
||||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
|
26
main.go
26
main.go
|
@ -21,7 +21,9 @@ type kis3 struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
app = &kis3{}
|
app = &kis3{
|
||||||
|
staticBox: packr.New("staticFiles", "./static"),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -30,7 +32,7 @@ func init() {
|
||||||
log.Fatal("Database setup failed:", e)
|
log.Fatal("Database setup failed:", e)
|
||||||
}
|
}
|
||||||
setupRouter()
|
setupRouter()
|
||||||
app.staticBox = packr.New("staticFiles", "./static")
|
setupReports()
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -49,29 +51,29 @@ func setupRouter() {
|
||||||
|
|
||||||
viewRouter := app.router.PathPrefix("/view").Subrouter()
|
viewRouter := app.router.PathPrefix("/view").Subrouter()
|
||||||
viewRouter.Use(corsHandler)
|
viewRouter.Use(corsHandler)
|
||||||
viewRouter.Path("").HandlerFunc(trackView)
|
viewRouter.Path("").HandlerFunc(TrackingHandler)
|
||||||
|
|
||||||
app.router.HandleFunc("/stats", requestStats)
|
app.router.HandleFunc("/stats", StatsHandler)
|
||||||
|
|
||||||
staticRouter := app.router.PathPrefix("").Subrouter()
|
staticRouter := app.router.PathPrefix("").Subrouter()
|
||||||
staticRouter.Use(corsHandler)
|
staticRouter.Use(corsHandler)
|
||||||
staticRouter.HandleFunc("/kis3.js", serveTrackingScript)
|
staticRouter.HandleFunc("/kis3.js", TrackingScriptHandler)
|
||||||
staticRouter.PathPrefix("").Handler(http.HandlerFunc(HelloResponseHandler))
|
staticRouter.PathPrefix("").Handler(http.HandlerFunc(HelloResponseHandler))
|
||||||
}
|
}
|
||||||
|
|
||||||
func startListening() {
|
func startListening() {
|
||||||
port := appConfig.port
|
port := appConfig.Port
|
||||||
addr := ":" + port
|
addr := ":" + port
|
||||||
fmt.Printf("Listening to %s\n", addr)
|
fmt.Printf("Listening to %s\n", addr)
|
||||||
log.Fatal(http.ListenAndServe(addr, app.router))
|
log.Fatal(http.ListenAndServe(addr, app.router))
|
||||||
}
|
}
|
||||||
|
|
||||||
func trackView(w http.ResponseWriter, r *http.Request) {
|
func TrackingHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0")
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0")
|
||||||
url := r.URL.Query().Get("url")
|
url := r.URL.Query().Get("url")
|
||||||
ref := r.URL.Query().Get("ref")
|
ref := r.URL.Query().Get("ref")
|
||||||
ua := r.Header.Get("User-Agent")
|
ua := r.Header.Get("User-Agent")
|
||||||
if !(r.Header.Get("DNT") == "1" && appConfig.dnt) {
|
if !(r.Header.Get("DNT") == "1" && appConfig.Dnt) {
|
||||||
go app.db.trackView(url, ref, ua) // run with goroutine for awesome speed!
|
go app.db.trackView(url, ref, ua) // run with goroutine for awesome speed!
|
||||||
_, _ = fmt.Fprint(w, "true")
|
_, _ = fmt.Fprint(w, "true")
|
||||||
}
|
}
|
||||||
|
@ -81,7 +83,7 @@ func HelloResponseHandler(w http.ResponseWriter, _ *http.Request) {
|
||||||
_, _ = fmt.Fprint(w, "Hello from KISSS")
|
_, _ = fmt.Fprint(w, "Hello from KISSS")
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveTrackingScript(w http.ResponseWriter, r *http.Request) {
|
func TrackingScriptHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/javascript")
|
w.Header().Set("Content-Type", "application/javascript")
|
||||||
w.Header().Set("Cache-Control", "public, max-age=432000") // 5 days
|
w.Header().Set("Cache-Control", "public, max-age=432000") // 5 days
|
||||||
filename := "kis3.js"
|
filename := "kis3.js"
|
||||||
|
@ -97,10 +99,10 @@ func serveTrackingScript(w http.ResponseWriter, r *http.Request) {
|
||||||
http.ServeContent(w, r, filename, stat.ModTime(), file)
|
http.ServeContent(w, r, filename, stat.ModTime(), file)
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestStats(w http.ResponseWriter, r *http.Request) {
|
func StatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Require authentication
|
// Require authentication
|
||||||
if appConfig.statsAuth {
|
if appConfig.statsAuth() {
|
||||||
if !helpers.CheckAuth(w, r, appConfig.statsUsername, appConfig.statsPassword) {
|
if !helpers.CheckAuth(w, r, appConfig.StatsUsername, appConfig.StatsPassword) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/jordan-wright/email"
|
||||||
|
"github.com/whiteshtef/clockwork"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/smtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupReports() {
|
||||||
|
scheduler := clockwork.NewScheduler()
|
||||||
|
for _, r := range appConfig.Reports {
|
||||||
|
scheduler.Schedule().Every().Day().At(r.Time).Do(func() {
|
||||||
|
executeReport(&r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
go scheduler.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeReport(r *report) {
|
||||||
|
fmt.Println("Execute report:", r.Name)
|
||||||
|
req, e := http.NewRequest("GET", "http://localhost:"+appConfig.Port+"/stats?"+r.Query, nil)
|
||||||
|
if e != nil {
|
||||||
|
fmt.Println("Executing report failed:", e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.SetBasicAuth(appConfig.StatsUsername, appConfig.StatsPassword)
|
||||||
|
res, e := http.DefaultClient.Do(req)
|
||||||
|
if e != nil {
|
||||||
|
fmt.Println("Executing report failed:", e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, e := ioutil.ReadAll(res.Body)
|
||||||
|
if e != nil {
|
||||||
|
fmt.Println("Executing report failed:", e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sendMail(r, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendMail(r *report, content []byte) {
|
||||||
|
smtpHostNoPort, _, _ := net.SplitHostPort(r.SmtpHost)
|
||||||
|
mail := email.NewEmail()
|
||||||
|
mail.From = r.From
|
||||||
|
mail.To = []string{r.To}
|
||||||
|
mail.Subject = "KISSS report: " + r.Name
|
||||||
|
mail.Text = content
|
||||||
|
e := mail.Send(r.SmtpHost, smtp.PlainAuth("", r.SmtpUser, r.SmtpPassword, smtpHostNoPort))
|
||||||
|
if e != nil {
|
||||||
|
fmt.Println("Sending report failed:", e)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
fmt.Println("Report sent")
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue