medinet/medinet.go

327 lines
8.0 KiB
Go

// MediNET - Meditation Assistant back-end
// https://gitlab.com/tslocum/medinet
// Written by Trevor 'tee' Slocum <tslocum@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"encoding/json"
"fmt"
"html"
"log"
"math/rand"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/BurntSushi/toml"
"github.com/jessevdk/go-flags"
"github.com/pkg/errors"
)
type Config struct {
Om int
SQLHost string
SQLUser string
SQLPassword string
SQLDatabase string
}
var config *Config
func midnight(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
}
func failOnError(err error) {
if err != nil {
log.Fatal(err)
}
}
func main() {
var opts struct {
ConfigFile string `short:"c" long:"config" description:"Configuration file"`
}
_, err := flags.Parse(&opts)
failOnError(err)
rand.Seed(time.Now().UTC().UnixNano())
if opts.ConfigFile == "" {
log.Fatal("Please specify configuration file with: medinet -c <config file>")
}
if _, err = os.Stat(opts.ConfigFile); err != nil {
log.Fatalf("Configuration file %s does not exist: %s", opts.ConfigFile, err)
}
config = new(Config)
if _, err = toml.DecodeFile(opts.ConfigFile, &config); err != nil {
log.Fatalf("Failed to read %s: %v", opts.ConfigFile, err)
}
if config.Om == 0 {
log.Fatal("Specify Om port in configuration file")
}
d, err := NewDatabase(config.SQLHost, config.SQLUser, config.SQLPassword, config.SQLDatabase)
failOnError(err)
http.HandleFunc("/om/community", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "COMMUNITY, %q", html.EscapeString(r.URL.RequestURI()))
})
http.HandleFunc("/om/sessions", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "COMMUNITY, %q", html.EscapeString(r.URL.RequestURI()))
})
http.HandleFunc("/om/account", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "COMMUNITY, %q", html.EscapeString(r.URL.RequestURI()))
})
http.HandleFunc("/om/forum", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "COMMUNITY, %q", html.EscapeString(r.URL.RequestURI()))
})
http.HandleFunc("/om/groups", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "COMMUNITY, %q", html.EscapeString(r.URL.RequestURI()))
})
http.HandleFunc("/om", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
r.ParseForm()
// Authenticate (signin)
// Send key, client will then call (connect)
token := r.FormValue("token")
if token != "" {
a, err := d.authenticate(token)
if err != nil {
log.Printf("ERROR! %v", errors.Wrap(err, "failed to authenticate token"))
return
} else if a == nil {
log.Printf("ERROR! %v", errors.New("failed to retrieve authenticated account"))
return
}
d.updateLastActive(a.ID)
w.Header().Set("x-MediNET", "connected")
w.Header().Set("x-MediNET-Key", a.Key)
return
}
key := r.URL.Query().Get("x")
if key == "" {
key = r.FormValue("x")
}
a, err := d.getAccount(key)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
if a == nil {
w.Header().Set("x-MediNET", "signin")
log.Printf("Asking to sign in %s %q", key, html.EscapeString(r.URL.RequestURI()))
return
}
go d.updateLastActive(a.ID)
data := make(map[string]interface{})
data["status"] = "success"
action := r.FormValue("action")
tz, err := time.LoadLocation(r.URL.Query().Get("tz"))
if err != nil {
tz, err = time.LoadLocation("UTC")
if err != nil {
log.Printf("ERROR! %v", err)
return
}
}
apiver, err := strconv.Atoi(r.URL.Query().Get("v"))
if err != nil {
apiver = 0
}
av := r.URL.Query().Get("avn")
if av == "" {
av = r.URL.Query().Get("av")
onlynum := regexp.MustCompile("[0-9]+")
match := onlynum.FindAllString(av, -1)
if match != nil {
av = strings.Join(match, "")
}
}
appver, err := strconv.Atoi(av)
if err != nil {
appver = 0
}
// TODO: read from announcement table on successful connect
//data["announce"] = "First line\n\nSecond line"
switch action {
case "deletesession":
data["result"] = "notdeleted"
st := r.FormValue("session")
if st == "" {
break
}
started, err := strconv.Atoi(st)
if err != nil || started == 0 {
log.Println(errors.Wrap(err, "failed to read session started when deleting session"))
return
}
deleted, err := d.deleteSession(started, a.ID)
if err != nil {
log.Printf("ERROR! %v", err)
return
} else if deleted {
data["result"] = "deleted"
}
case "downloadsessions": // Confirmed working
sessions, err := d.getSessions(a.ID)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
data["downloadsessions"] = sessions
case "uploadsessions":
data["result"] = "corrupt"
u := r.FormValue("uploadsessions")
if u == "" {
break
}
var uploadsessions []Session
err = json.Unmarshal([]byte(u), &uploadsessions)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
data["result"] = "uploaded"
sessionsuploaded := 0
for _, session := range uploadsessions {
added, err := d.addSession(session, a.ID)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
if added {
sessionsuploaded++
}
}
data["sessionsuploaded"] = sessionsuploaded
if sessionsuploaded > 0 {
started := 0
completed := 0
streakday := 0
for _, session := range uploadsessions {
if session.Started > started {
started = session.Started
completed = session.Completed
streakday = session.StreakDay
}
}
t := time.Now().In(tz)
if t.Hour() < 4 {
t = t.AddDate(0, 0, -1)
}
t = midnight(t).Add(STREAK_BUFFER * time.Second)
if int64(completed) >= t.Unix() { // Session was recently uploaded
streak, err := d.calculateStreak(a.ID, tz)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
log.Printf("NEW SESSION %v - CALCULATED: %d, SUBMITTED: %d", t, streak, streakday)
if streak < streakday {
streak = streakday
} else if streak > streakday {
err = d.setSessionStreakDay(started, streak, a.ID)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
}
err = d.setStreak(streak, a.ID, tz)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
}
}
// "postsession" is set when posting directly from complete screen or session list
p := r.FormValue("postsession")
if p != "" {
if sessionsuploaded > 0 {
data["result"] = "posted"
} else {
data["result"] = "alreadyposted"
}
}
}
w.Header().Set("x-MediNET", "connected")
// Send streak
if action == "connect" || action == "downloadsessions" || action == "uploadsessions" {
streakday, streakend, topstreak, err := d.getStreak(a.ID)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
w.Header().Set("x-MediNET-Streak", fmt.Sprintf("%d,%d", streakday, streakend))
w.Header().Set("x-MediNET-MaxStreak", fmt.Sprintf("%d", topstreak))
}
json.NewEncoder(w).Encode(data)
j, err := json.Marshal(data)
if err != nil {
log.Printf("ERROR! %v", err)
return
}
log.Printf("App: %d API: %d Action: %s - %q", appver, apiver, action, html.EscapeString(r.URL.RequestURI()))
log.Printf("Account ID: %d, JSON: %s", a.ID, string(j))
})
log.Fatal(http.ListenAndServe(fmt.Sprintf("localhost:%d", config.Om), nil))
}