1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00

Compare commits

..

7 Commits
1.0.0 ... 1.1.0

Author SHA1 Message Date
7d36c4e111 Rename project.
Add build script.
2020-03-31 12:22:17 +02:00
1510bcaa91 Experimental Postgres support. 2020-03-31 12:03:49 +02:00
b858a02ecd Update Readme. 2020-03-31 11:25:24 +02:00
0e61870568 Resolve #7. 2020-03-31 11:24:44 +02:00
0664861f74 Fix #13. 2020-03-31 10:47:22 +02:00
bcb56a02ef Readme typo. 2020-03-31 09:17:00 +02:00
fd2e0d145d Update README.md 2020-03-11 10:19:55 +01:00
24 changed files with 1715 additions and 111 deletions

View File

@ -1,4 +1,5 @@
ENV=prod
WAKAPI_DB_TYPE=mysql
WAKAPI_DB_USER=myuser
WAKAPI_DB_PASSWORD=mysecretpassword
WAKAPI_DB_HOST=localhost

2
.github/FUNDING.yml vendored
View File

@ -1,2 +1,2 @@
github: n1try
github: muety
custom: ['https://paypal.me/ferdinandmuetsch', 'https://www.buymeacoffee.com/n1try']

3
.gitignore vendored
View File

@ -2,4 +2,5 @@ launch.json
.vscode
.env
wakapi
.idea
.idea
build

View File

@ -8,16 +8,16 @@
## Prerequisites
### Server
* Go >= 1.13 (with `$GOPATH` properly set)
* A MySQL database
* An SQL database (MySQL or Postgres)
### Client
* [WakaTime plugin](https://wakatime.com/plugins) for your editor / IDE
## Usage
* Create an empty MySQL database
* Create an empty database
* Enable Go module support: `export GO111MODULE=on`
* Get code: `go get github.com/n1try/wakapi`
* Go to project root: `cd "$GOPATH/src/github.com/n1try/wakapi"`
* Get code: `go get github.com/muety/wakapi`
* Go to project root: `cd "$GOPATH/src/github.com/muety/wakapi"`
* Copy `.env.example` to `.env` and set database credentials
* Set target port in `config.ini`
* Build executable: `go build`
@ -27,6 +27,8 @@
* `api_key = the_api_key_printed_to_the_console_after_starting_the_server`
* Open [http://localhost:3000](http://localhost:3000) in your browser
**As an alternative** to building from source or using `go get` you can also download one of the existing [pre-compiled binaries](https://github.com/muety/wakapi/releases).
### Run with Docker
* Edit `docker-compose.yml` file and change passwords for the DB
* Build the container `docker-compose build`
@ -34,7 +36,6 @@
* To get the api key look in the logs `docker-compose logs | grep "API key"`
* The application should now be running on `localhost:3000`
### User Accounts
* When starting wakapi for the first time, a default user _**admin**_ with password _**admin**_ is created. The corresponding API key is printed to the console.
* Additional users, at the moment, can be added only via SQL statements on your database, like this:
@ -62,12 +63,6 @@ For the above example, you would need to add two aliases, like this:
It is recommended to use wakapi behind a **reverse proxy**, like [Caddy](https://caddyserver.com) or _nginx_ to enable **TLS encryption** (HTTPS).
However, if you want to expose your wakapi instance to the public anyway, you need to set `listen = 0.0.0.0` in `config.ini`
## Todo
* User sign up and log in
* Additional endpoints for retrieving statistics data
* Support for SQLite database
* Unit tests
## Important Note
**This is not an alternative to using WakaTime.** It is just a custom, non-commercial, self-hosted application to collect coding statistics using the already existing editor plugins provided by the WakaTime community. It was created for personal use only and with the purpose of keeping the sovereignity of your own data. However, if you like the official product, **please support the authors and buy an official WakaTime subscription!**

16
build.sh Normal file
View File

@ -0,0 +1,16 @@
#!/bin/bash
OSLIST=( darwin linux windows )
ARCHLIST=( amd64 )
VERSION=$(cat version.txt)
for os in ${OSLIST[*]}
do
for arch in ${ARCHLIST[*]}
do
GOOS=$os
GOARCH=$arch
echo "Building $GOOS / $GOARCH"
GOOS=$GOOS GOARCH=$GOARCH go build -o "build/wakapi_${VERSION}_${GOOS}_${GOARCH}" "github.com/muety/wakapi"
done
done

1482
data/colors.json Normal file

File diff suppressed because it is too large Load Diff

2
go.mod
View File

@ -1,4 +1,4 @@
module github.com/n1try/wakapi
module github.com/muety/wakapi
go 1.13

19
go.sum
View File

@ -1,6 +1,5 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU=
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
@ -13,14 +12,11 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/codegangsta/negroni v1.0.0 h1:+aYywywx4bnKXWvoWtRfJ91vC59NbEhEY03sZjQhbVY=
github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA=
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@ -38,12 +34,10 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@ -57,25 +51,20 @@ github.com/jinzhu/gorm v1.9.11 h1:gaHGvE+UnWGlbWG4Y3FUwY1EcZ5n6S9WtqBA/uySMLE=
github.com/jinzhu/gorm v1.9.11/go.mod h1:bu/pK8szGZ2puuErfU0RwyeNdsf3e6nCX/noXaVxkfw=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
@ -89,7 +78,6 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
@ -104,21 +92,17 @@ github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/t-tiger/gorm-bulk-insert v0.0.0-20191014134946-beb77b81825f h1:Op5lFYUNE7tPxu6gJfwkgY8HMIWpLqiLApBJfGs71U8=
github.com/t-tiger/gorm-bulk-insert v0.0.0-20191014134946-beb77b81825f/go.mod h1:SK1RZT4TR1aMUNGtbk6YxTPgx2D/gfbxB571QGnAV+c=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
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-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -157,7 +141,6 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
@ -168,14 +151,12 @@ google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.50.0 h1:c/4YI/GUgB7d2yOkxdsQyYDhW67nWrTl6Zyd9vagYmg=
gopkg.in/ini.v1 v1.50.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

51
main.go
View File

@ -3,10 +3,13 @@ package main
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/codegangsta/negroni"
@ -17,21 +20,26 @@ import (
uuid "github.com/satori/go.uuid"
ini "gopkg.in/ini.v1"
"github.com/n1try/wakapi/middlewares"
"github.com/n1try/wakapi/models"
"github.com/n1try/wakapi/routes"
"github.com/n1try/wakapi/services"
"github.com/n1try/wakapi/utils"
"github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/routes"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/postgres"
)
// TODO: Refactor entire project to be structured after business domains
func readConfig() *models.Config {
if err := godotenv.Load(); err != nil {
log.Fatal(err)
}
// TODO: Use jinzhu/configor or so
env, _ := os.LookupEnv("ENV")
dbType, valid := os.LookupEnv("WAKAPI_DB_TYPE")
dbUser, valid := os.LookupEnv("WAKAPI_DB_USER")
dbPassword, valid := os.LookupEnv("WAKAPI_DB_PASSWORD")
dbHost, valid := os.LookupEnv("WAKAPI_DB_HOST")
@ -48,6 +56,10 @@ func readConfig() *models.Config {
log.Fatalf("Fail to read file: %v", err)
}
if dbType == "" {
dbType = "mysql"
}
dbMaxConn := cfg.Section("database").Key("max_connections").MustUint(1)
addr := cfg.Section("server").Key("listen").MustString("127.0.0.1")
port, err := strconv.Atoi(os.Getenv("PORT"))
@ -64,6 +76,27 @@ func readConfig() *models.Config {
customLangs[k.Name()] = k.MustString("unknown")
}
// Read language colors
// Source: https://raw.githubusercontent.com/ozh/github-colors/master/colors.json
var colors = make(map[string]string)
var rawColors map[string]struct {
Color string `json:"color"`
Url string `json:"url"`
}
data, err := ioutil.ReadFile("data/colors.json")
if err != nil {
log.Fatal(err)
}
if err := json.Unmarshal(data, &rawColors); err != nil {
log.Fatal(err)
}
for k, v := range rawColors {
colors[strings.ToLower(k)] = v.Color
}
return &models.Config{
Env: env,
Port: port,
@ -73,10 +106,11 @@ func readConfig() *models.Config {
DbUser: dbUser,
DbPassword: dbPassword,
DbName: dbName,
DbDialect: "mysql",
DbDialect: dbType,
DbMaxConn: dbMaxConn,
CleanUp: cleanUp,
CustomLanguages: customLangs,
LanguageColors: colors,
}
}
@ -92,6 +126,7 @@ func main() {
if err != nil {
log.Fatal("Could not connect to database.")
}
// TODO: Graceful shutdown
defer db.Close()
// Migrate database schema
@ -147,8 +182,8 @@ func main() {
mainRouter.Path("/").Methods(http.MethodGet).HandlerFunc(summaryHandler.Index)
// API Routes
apiRouter.Path("/heartbeat").Methods(http.MethodPost).HandlerFunc(heartbeatHandler.Post)
apiRouter.Path("/summary").Methods(http.MethodGet).HandlerFunc(summaryHandler.Get)
apiRouter.Path("/heartbeat").Methods(http.MethodPost).HandlerFunc(heartbeatHandler.ApiPost)
apiRouter.Path("/summary").Methods(http.MethodGet).HandlerFunc(summaryHandler.ApiGet)
// Static Routes
router.PathPrefix("/assets").Handler(negroni.Classic().With(negroni.Wrap(http.FileServer(http.Dir("./static")))))

View File

@ -13,8 +13,8 @@ import (
"github.com/patrickmn/go-cache"
"github.com/n1try/wakapi/models"
"github.com/n1try/wakapi/services"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/services"
)
type AuthenticateMiddleware struct {

View File

@ -13,6 +13,7 @@ type Config struct {
DbMaxConn uint
CleanUp bool
CustomLanguages map[string]string
LanguageColors map[string]string
}
func (c *Config) IsDev() bool {

View File

@ -35,3 +35,8 @@ type SummaryItemContainer struct {
Type uint8
Items []*SummaryItem
}
type SummaryViewModel struct {
*Summary
LanguageColors map[string]string
}

View File

@ -5,17 +5,17 @@ import (
"net/http"
"os"
"github.com/n1try/wakapi/services"
"github.com/n1try/wakapi/utils"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
"github.com/n1try/wakapi/models"
"github.com/muety/wakapi/models"
)
type HeartbeatHandler struct {
HeartbeatSrvc *services.HeartbeatService
}
func (h *HeartbeatHandler) Post(w http.ResponseWriter, r *http.Request) {
func (h *HeartbeatHandler) ApiPost(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return

View File

@ -7,9 +7,9 @@ import (
"path"
"time"
"github.com/n1try/wakapi/models"
"github.com/n1try/wakapi/services"
"github.com/n1try/wakapi/utils"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
)
const (
@ -44,7 +44,7 @@ func (m *SummaryHandler) loadTemplates() {
m.indexTemplate = indexTpl
}
func (h *SummaryHandler) Get(w http.ResponseWriter, r *http.Request) {
func (h *SummaryHandler) ApiGet(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
@ -91,7 +91,12 @@ func (h *SummaryHandler) Index(w http.ResponseWriter, r *http.Request) {
return
}
h.indexTemplate.Execute(w, summary)
vm := models.SummaryViewModel{
Summary: summary,
LanguageColors: utils.FilterLanguageColors(h.SummarySrvc.Config.LanguageColors, summary),
}
h.indexTemplate.Execute(w, vm)
}
func loadUserSummary(r *http.Request, summaryService *services.SummaryService) (*models.Summary, error, int) {

View File

@ -7,7 +7,7 @@ import (
"github.com/jasonlvhit/gocron"
"github.com/jinzhu/gorm"
"github.com/n1try/wakapi/models"
"github.com/muety/wakapi/models"
)
const (

View File

@ -5,7 +5,7 @@ import (
"sync"
"github.com/jinzhu/gorm"
"github.com/n1try/wakapi/models"
"github.com/muety/wakapi/models"
)
type AliasService struct {

View File

@ -2,12 +2,12 @@ package services
import (
"github.com/jasonlvhit/gocron"
"github.com/n1try/wakapi/utils"
"github.com/muety/wakapi/utils"
"log"
"time"
"github.com/jinzhu/gorm"
"github.com/n1try/wakapi/models"
"github.com/muety/wakapi/models"
gormbulk "github.com/t-tiger/gorm-bulk-insert"
)

View File

@ -10,7 +10,7 @@ import (
"time"
"github.com/jinzhu/gorm"
"github.com/n1try/wakapi/models"
"github.com/muety/wakapi/models"
)
type SummaryService struct {

View File

@ -2,7 +2,7 @@ package services
import (
"github.com/jinzhu/gorm"
"github.com/n1try/wakapi/models"
"github.com/muety/wakapi/models"
)
type UserService struct {

View File

@ -1,11 +1,13 @@
const SHOW_TOP_N = 10
const CHART_TARGET_SIZE = 170
const projectsCanvas = document.getElementById("chart-projects")
const osCanvas = document.getElementById("chart-os")
const editorsCanvas = document.getElementById("chart-editor")
const languagesCanvas = document.getElementById("chart-language")
const projectsCanvas = document.getElementById('chart-projects')
const osCanvas = document.getElementById('chart-os')
const editorsCanvas = document.getElementById('chart-editor')
const languagesCanvas = document.getElementById('chart-language')
let charts = []
let resizeCount = 0
String.prototype.toHHMMSS = function () {
var sec_num = parseInt(this, 10)
@ -64,7 +66,9 @@ function draw() {
tooltips: getTooltipOptions('projects', 'bar'),
legend: {
display: false
}
},
maintainAspectRatio: false,
onResize: onChartResize
}
})
@ -83,7 +87,9 @@ function draw() {
},
options: {
title: Object.assign(titleOptions, {text: `Operating Systems (top ${SHOW_TOP_N})`}),
tooltips: getTooltipOptions('operatingSystems', 'pie')
tooltips: getTooltipOptions('operatingSystems', 'pie'),
maintainAspectRatio: false,
onResize: onChartResize
}
})
@ -102,7 +108,9 @@ function draw() {
},
options: {
title: Object.assign(titleOptions, {text: `Editors (top ${SHOW_TOP_N})`}),
tooltips: getTooltipOptions('editors', 'pie')
tooltips: getTooltipOptions('editors', 'pie'),
maintainAspectRatio: false,
onResize: onChartResize
}
})
@ -113,7 +121,7 @@ function draw() {
data: wakapiData.languages
.slice(0, Math.min(SHOW_TOP_N, wakapiData.languages.length))
.map(p => parseInt(p.total)),
backgroundColor: wakapiData.languages.map(p => getRandomColor(p.key))
backgroundColor: wakapiData.languages.map(p => languageColors[p.key.toLowerCase()] || getRandomColor(p.key))
}],
labels: wakapiData.languages
.slice(0, Math.min(SHOW_TOP_N, wakapiData.languages.length))
@ -121,7 +129,9 @@ function draw() {
},
options: {
title: Object.assign(titleOptions, {text: `Languages (top ${SHOW_TOP_N})`}),
tooltips: getTooltipOptions('languages', 'pie')
tooltips: getTooltipOptions('languages', 'pie'),
maintainAspectRatio: false,
onResize: onChartResize
}
})
@ -129,39 +139,78 @@ function draw() {
document.getElementById('grid-container').style.visibility = 'visible'
charts = [projectChart, osChart, editorChart, languageChart]
charts.forEach(c => c.options.onResize(c.chart))
equalizeHeights()
}
function getContainer(chart) {
return chart.canvas.parentNode
}
function onChartResize(chart) {
let container = getContainer(chart)
let targetHeight = Math.min(chart.width, CHART_TARGET_SIZE)
let actualHeight = chart.height - chart.chartArea.top
let containerTargetHeight = container.clientHeight += (targetHeight - actualHeight)
container.style.height = parseInt(containerTargetHeight) + 'px'
resizeCount++
watchEqualize()
}
function watchEqualize() {
if (resizeCount === charts.length) {
equalizeHeights()
resizeCount = 0
}
}
function equalizeHeights() {
let maxHeight = 0
charts.forEach(c => {
let container = getContainer(c)
if (maxHeight < container.clientHeight) {
maxHeight = container.clientHeight
}
})
charts.forEach(c => {
let container = getContainer(c)
container.style.height = parseInt(maxHeight) + 'px'
})
}
function getTotal(data) {
let total = data.reduce((acc, d) => acc + d.total, 0)
document.getElementById("total-span").innerText = total.toString().toHHMMSS()
document.getElementById('total-span').innerText = total.toString().toHHMMSS()
}
function getRandomColor(seed) {
seed = seed ? seed : '1234567';
Math.seedrandom(seed);
var letters = '0123456789ABCDEF'.split('');
var color = '#';
seed = seed ? seed : '1234567'
Math.seedrandom(seed)
var letters = '0123456789ABCDEF'.split('')
var color = '#'
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
color += letters[Math.floor(Math.random() * 16)]
}
return color;
return color
}
// https://koddsson.com/posts/emoji-favicon/
const favicon = document.querySelector("link[rel=icon]");
const favicon = document.querySelector('link[rel=icon]')
if (favicon) {
const emoji = favicon.getAttribute("data-emoji");
const emoji = favicon.getAttribute('data-emoji')
if (emoji) {
const canvas = document.createElement("canvas");
canvas.height = 64;
canvas.width = 64;
const ctx = canvas.getContext("2d");
ctx.font = "64px serif";
ctx.fillText(emoji, 0, 64);
favicon.href = canvas.toDataURL();
const canvas = document.createElement('canvas')
canvas.height = 64
canvas.width = 64
const ctx = canvas.getContext('2d')
ctx.font = '64px serif'
ctx.fillText(emoji, 0, 64)
favicon.href = canvas.toDataURL()
}
}
window.addEventListener('load', function() {
window.addEventListener('load', function () {
draw()
})

16
utils/color.go Normal file
View File

@ -0,0 +1,16 @@
package utils
import (
"github.com/muety/wakapi/models"
"strings"
)
func FilterLanguageColors(all map[string]string, summary *models.Summary) map[string]string {
subset := make(map[string]string)
for _, item := range summary.Languages {
if c, ok := all[strings.ToLower(item.Key)]; ok {
subset[strings.ToLower(item.Key)] = c
}
}
return subset
}

View File

@ -2,12 +2,11 @@ package utils
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"time"
"github.com/n1try/wakapi/models"
"github.com/muety/wakapi/models"
)
func ParseDate(date string) (time.Time, error) {
@ -32,18 +31,34 @@ func ParseUserAgent(ua string) (string, string, error) {
}
func MakeConnectionString(config *models.Config) string {
location, _ := time.LoadLocation("Local")
str := strings.Builder{}
str.WriteString(config.DbUser)
str.WriteString(":")
str.WriteString(config.DbPassword)
str.WriteString("@tcp(")
str.WriteString(config.DbHost)
str.WriteString(":")
str.WriteString(strconv.Itoa(int(config.DbPort)))
str.WriteString(")/")
str.WriteString(config.DbName)
str.WriteString("?charset=utf8&parseTime=true&loc=")
str.WriteString(location.String())
return str.String()
switch config.DbDialect {
case "mysql":
return mySqlConnectionString(config)
case "postgres":
return postgresConnectionString(config)
}
return ""
}
func mySqlConnectionString(config *models.Config) string {
location, _ := time.LoadLocation("Local")
return fmt.Sprintf(
"%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=true&loc=%s",
config.DbUser,
config.DbPassword,
config.DbHost,
config.DbPort,
config.DbName,
location.String(),
)
}
func postgresConnectionString(config *models.Config) string {
return fmt.Sprintf("host=%s port=%d user=%s dbname=%s password=%s sslmode=disable",
config.DbHost,
config.DbPort,
config.DbUser,
config.DbName,
config.DbPassword,
)
}

1
version.txt Normal file
View File

@ -0,0 +1 @@
1.1.0

View File

@ -10,7 +10,7 @@
<link href="assets/app.css" rel="stylesheet">
</head>
<body class="bg-gray-800 text-gray-700 p-4 pt-12 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
<body class="bg-gray-800 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
<div class="flex items-center justify-center">
<h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Your Coding Statistics 🤓</h1>
</div>
@ -22,7 +22,7 @@
<a href="?interval=year" class="m-1 border-b border-green-700">This Year</a>
<a href="?interval=any" class="m-1 border-b border-green-700">All Time</a>
</div>
<main class="mt-12 flex-grow" id="grid-container">
<main class="mt-10 flex-grow" id="grid-container">
<div class="flex justify-center">
<div class="p-1">
<div class="flex justify-center p-4 bg-white rounded shadow">
@ -34,29 +34,29 @@
</div>
<div class="flex flex-wrap justify-center">
<div class="w-full lg:w-1/2 p-1">
<div class="p-4 bg-white rounded shadow m-2" id="projects-container">
<div class="p-4 bg-white rounded shadow m-2" id="projects-container" style="height: 300px">
<canvas id="chart-projects"></canvas>
</div>
</div>
<div class="w-full lg:w-1/2 p-1">
<div class="p-4 bg-white rounded shadow m-2" id="os-container">
<div class="p-4 bg-white rounded shadow m-2" id="os-container" style="height: 300px">
<canvas id="chart-os"></canvas>
</div>
</div>
<div class="w-full lg:w-1/2 p-1">
<div class="p-4 bg-white rounded shadow m-2" id="editor-container">
<canvas id="chart-editor"></canvas>
<div class="p-4 bg-white rounded shadow m-2 relative" id="language-container" style="height: 300px">
<canvas id="chart-language"></canvas>
</div>
</div>
<div class="w-full lg:w-1/2 p-1">
<div class="p-4 bg-white rounded shadow m-2" id="language-container">
<canvas id="chart-language"></canvas>
<div class="p-4 bg-white rounded shadow m-2" id="editor-container" style="height: 300px">
<canvas id="chart-editor"></canvas>
</div>
</div>
</div>
</main>
<footer class="w-full text-center text-gray-300 text-xs mt-12">
Made by <a href="https://muetsch.io" class="border-b border-green-700">Ferdinand Mütsch</a> as <a href="https://github.com/n1try/wakapi" class="border-b border-green-700">open-source</a>.
Made by <a href="https://muetsch.io" class="border-b border-green-700">Ferdinand Mütsch</a> as <a href="https://github.com/muety/wakapi" class="border-b border-green-700">open-source</a>.
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.4/seedrandom.min.js"></script>
@ -64,7 +64,8 @@
<script>
let wakapiData = {}
wakapiData.projects = {{ json .Projects }}
let languageColors = {{ .LanguageColors | json }}
wakapiData.projects = {{ .Projects | json }}
wakapiData.operatingSystems = {{ .OperatingSystems | json }}
wakapiData.editors = {{ .Editors | json }}
wakapiData.languages = {{ .Languages | json }}