593 lines
15 KiB
Go
593 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html"
|
|
"io/fs"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
//go:embed www
|
|
var wwwFS embed.FS
|
|
|
|
var signInPage = []byte(`
|
|
<body onload="askToSignIn()">
|
|
<script type="text/javascript">
|
|
function askToSignIn() {
|
|
MA.askToSignIn();
|
|
}
|
|
</script>
|
|
</body>
|
|
`)
|
|
|
|
var pages = map[string]http.HandlerFunc{
|
|
"community": handleCommunity,
|
|
"sessions": handleSessions,
|
|
"account": handleAccount,
|
|
"forum": handleForum,
|
|
"groups": handleGroups,
|
|
"profile": handleProfile,
|
|
"status": handleStatus,
|
|
}
|
|
|
|
func authenticateUser(w http.ResponseWriter, r *http.Request) *account {
|
|
key := r.URL.Query().Get("x")
|
|
if key == "" {
|
|
key = r.FormValue("x")
|
|
}
|
|
|
|
acc, err := db.accountByKey(key)
|
|
if err != nil {
|
|
log.Printf("ERROR! %v", err)
|
|
return nil
|
|
} else if acc == nil {
|
|
w.Header().Set("x-MediNET", "signin")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write(signInPage)
|
|
logDebugf("Asking to sign in %s %q", key, html.EscapeString(r.URL.RequestURI()))
|
|
return nil
|
|
}
|
|
|
|
go trackActiveAccount(acc.ID)
|
|
return acc
|
|
}
|
|
|
|
func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = r.ParseForm()
|
|
|
|
// Handle logging in.
|
|
token := r.FormValue("token")
|
|
if token != "" {
|
|
a, err := db.authenticate(token)
|
|
if err != nil {
|
|
log.Printf("ERROR! failed to authenticate token: %s", err)
|
|
return
|
|
} else if a == nil {
|
|
log.Printf("ERROR! failed to retrieve authenticated account")
|
|
return
|
|
}
|
|
|
|
go trackActiveAccount(a.ID)
|
|
w.Header().Set("x-MediNET", "connected")
|
|
w.Header().Set("x-MediNET-Key", a.Key)
|
|
|
|
return
|
|
}
|
|
|
|
// Require authentication.
|
|
acc := authenticateUser(w, r)
|
|
if acc == nil {
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
appver := 0
|
|
streakbuffer := -1
|
|
|
|
av := r.URL.Query().Get("av")
|
|
match := regexpMarket.FindStringSubmatch(av)
|
|
if len(match) == 2 {
|
|
match = regexpNumbers.FindAllString(av[0:len(av)-len(match[1])], -1)
|
|
if match != nil {
|
|
appver, err = strconv.Atoi(strings.Join(match, ""))
|
|
if err != nil {
|
|
appver = 0
|
|
}
|
|
}
|
|
} else {
|
|
av = "x-" + av
|
|
}
|
|
|
|
appmarket := r.URL.Query().Get("am")
|
|
|
|
sb := r.URL.Query().Get("buf")
|
|
if sb != "" {
|
|
streakbuffer, err = strconv.Atoi(sb)
|
|
if err != nil {
|
|
streakbuffer = -1
|
|
}
|
|
}
|
|
|
|
if streakbuffer >= 0 {
|
|
if acc.StreakBuffer != streakbuffer {
|
|
acc.StreakBuffer = streakbuffer
|
|
go db.updateStreakBuffer(acc.ID, streakbuffer)
|
|
}
|
|
}
|
|
|
|
announcement, err := db.showAnnouncement(acc)
|
|
if err != nil {
|
|
log.Printf("ERROR! failed to show announcement: %s", err)
|
|
return
|
|
} else if announcement != "" {
|
|
data["announce"] = announcement
|
|
}
|
|
|
|
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.Printf("failed to read session started when deleting session: %s", err)
|
|
return
|
|
}
|
|
|
|
deleted, err := db.deleteSession(started, acc.ID)
|
|
if err != nil {
|
|
log.Printf("ERROR! %v", err)
|
|
return
|
|
} else if deleted {
|
|
data["result"] = "deleted"
|
|
}
|
|
case "downloadsessions":
|
|
sessions, err := db.getAllSessions(acc.ID, false)
|
|
if err != nil {
|
|
log.Printf("ERROR! %v", err)
|
|
return
|
|
}
|
|
|
|
data["downloadsessions"] = sessions
|
|
case "uploadsessions":
|
|
data["result"] = "corrupt"
|
|
|
|
u := r.FormValue("uploadsessions")
|
|
if u == "" {
|
|
break
|
|
}
|
|
|
|
postsession := r.FormValue("postsession")
|
|
updateSessionStarted, _ := strconv.Atoi(r.FormValue("editstarted"))
|
|
|
|
var uploadsessions []session
|
|
err = json.Unmarshal([]byte(u), &uploadsessions)
|
|
if err != nil {
|
|
log.Printf("ERROR! %v", err)
|
|
return
|
|
}
|
|
|
|
data["result"] = "uploaded"
|
|
uploaded := false
|
|
sessionsuploaded := 0
|
|
|
|
for _, session := range uploadsessions {
|
|
uploaded, err = db.addSession(session, updateSessionStarted, acc.ID, av, appmarket)
|
|
if err != nil {
|
|
log.Printf("ERROR! %v", err)
|
|
return
|
|
}
|
|
|
|
if uploaded {
|
|
sessionsuploaded++
|
|
}
|
|
}
|
|
data["sessionsuploaded"] = sessionsuploaded
|
|
|
|
if sessionsuploaded > 0 {
|
|
started := 0
|
|
streakday := 0
|
|
for _, session := range uploadsessions {
|
|
if session.Started > started {
|
|
started = session.Started
|
|
if action != "editposting" {
|
|
streakday = session.StreakDay
|
|
}
|
|
}
|
|
}
|
|
|
|
t := time.Now().In(tz)
|
|
if beforeWindowStart(t, streakbuffer) {
|
|
t = t.AddDate(0, 0, -1)
|
|
}
|
|
t = atWindowStart(t, streakbuffer)
|
|
if int64(started) >= t.Unix() { // A session was recorded after the start of today's streak window
|
|
streak, err := db.calculateStreak(acc.ID, acc.StreakBuffer, tz)
|
|
if err != nil {
|
|
log.Printf("ERROR! %v", err)
|
|
return
|
|
}
|
|
|
|
logDebugf("NEW SESSION %v - CALCULATED: %d, SUBMITTED: %d", t, streak, streakday)
|
|
|
|
if streak < streakday {
|
|
streak = streakday
|
|
} else if streak > streakday {
|
|
err = db.setSessionStreakDay(started, streak, acc.ID)
|
|
if err != nil {
|
|
log.Printf("ERROR! %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
err = db.setStreak(streak, acc.ID, acc.StreakBuffer, tz)
|
|
if err != nil {
|
|
log.Printf("ERROR! %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
stats.SessionsPosted++
|
|
if streakday > stats.TopStreak {
|
|
stats.TopStreak = streakday
|
|
}
|
|
}
|
|
|
|
if postsession != "" {
|
|
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 := db.getStreak(acc.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))
|
|
}
|
|
|
|
err = json.NewEncoder(w).Encode(data)
|
|
if err != nil {
|
|
log.Printf("ERROR! %v", err)
|
|
return
|
|
}
|
|
|
|
j, err := json.Marshal(data)
|
|
if err != nil {
|
|
log.Printf("ERROR! %v", err)
|
|
return
|
|
}
|
|
|
|
logDebugf("App: %d API: %d Action: %s - %q", appver, apiver, action, html.EscapeString(r.URL.RequestURI()))
|
|
logDebugf("Account ID: %d, JSON: %s", acc.ID, string(j))
|
|
}
|
|
|
|
func handleCommunity(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
|
|
lightTheme := isLightTheme(r)
|
|
w.Write(pageHeader(lightTheme, "Community"))
|
|
|
|
recentSessions, err := db.getRecentSessions()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
for _, recent := range recentSessions {
|
|
w.Write([]byte(fmt.Sprintf(`<li onclick="goToAccount('%d');">`, recent.AccountID)))
|
|
printSession(w, lightTheme, recent)
|
|
w.Write([]byte(`</li>`))
|
|
}
|
|
|
|
w.Write(pageFooter())
|
|
}
|
|
|
|
func handleAccount(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
|
|
acc := authenticateUser(w, r)
|
|
if acc == nil {
|
|
return
|
|
}
|
|
|
|
lightTheme := isLightTheme(r)
|
|
w.Write(pageHeader(lightTheme, "Account"))
|
|
|
|
// ...
|
|
|
|
w.Write(pageFooter())
|
|
}
|
|
|
|
func handleSessions(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
|
|
acc := authenticateUser(w, r)
|
|
if acc == nil {
|
|
return
|
|
}
|
|
|
|
lightTheme := isLightTheme(r)
|
|
w.Write(pageHeader(lightTheme, "Sessions"))
|
|
|
|
sessions, err := db.getAllSessions(acc.ID, true)
|
|
if err != nil {
|
|
log.Printf("ERROR! %v", err)
|
|
return
|
|
}
|
|
if len(sessions) > 0 {
|
|
for _, s := range sessions {
|
|
w.Write([]byte(fmt.Sprintf(`<li onclick="deleteSession('%d');">`, s.ID)))
|
|
printSession(w, lightTheme, &recentSession{session: *s})
|
|
w.Write([]byte(`</li>`))
|
|
}
|
|
} else {
|
|
w.Write([]byte(`<li><h1>No sessions</h1></li>`))
|
|
}
|
|
|
|
w.Write(pageFooter())
|
|
}
|
|
|
|
func handleGroups(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
|
|
acc := authenticateUser(w, r)
|
|
if acc == nil || acc.ID == 0 {
|
|
return
|
|
}
|
|
|
|
lightTheme := isLightTheme(r)
|
|
w.Write(pageHeader(lightTheme, "Groups"))
|
|
|
|
groups, err := db.getAllGroups()
|
|
if err != nil {
|
|
log.Printf("ERROR! %v", err)
|
|
return
|
|
}
|
|
|
|
format := `<li>
|
|
<table border="0" width="100%%">
|
|
<tr>
|
|
<td><span style="font-size: 1.5em;">%s</span></td>
|
|
<td align="right">%d member%s</td></tr>
|
|
</table>
|
|
<small>%s</small>
|
|
</li>`
|
|
for _, grp := range groups {
|
|
memberCount, err := db.groupMemberCount(grp.ID)
|
|
if err != nil {
|
|
log.Printf("ERROR! %v", err)
|
|
return
|
|
}
|
|
|
|
pluralMembers := "s"
|
|
if memberCount == 1 {
|
|
pluralMembers = ""
|
|
}
|
|
|
|
w.Write([]byte(fmt.Sprintf(format, html.EscapeString(grp.Name), memberCount, pluralMembers, html.EscapeString(grp.Description))))
|
|
}
|
|
|
|
w.Write([]byte("<br><br><br><h1>Not yet completed</h1>"))
|
|
|
|
w.Write(pageFooter())
|
|
}
|
|
|
|
func handleForum(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
|
|
acc := authenticateUser(w, r)
|
|
if acc == nil {
|
|
return
|
|
}
|
|
|
|
lightTheme := isLightTheme(r)
|
|
w.Write(pageHeader(lightTheme, "Forum"))
|
|
|
|
// ...
|
|
|
|
w.Write(pageFooter())
|
|
}
|
|
|
|
func handleProfile(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html")
|
|
|
|
acc := authenticateUser(w, r)
|
|
if acc == nil {
|
|
return
|
|
}
|
|
|
|
var profile *account
|
|
profileID, err := strconv.Atoi(r.URL.Query().Get("account"))
|
|
if err == nil {
|
|
profile, err = db.accountByID(profileID)
|
|
if err != nil {
|
|
log.Printf("ERROR! %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
profileName := "Unknown Account"
|
|
if profile != nil {
|
|
profileName = profile.Name
|
|
if profileName == "" {
|
|
profileName = "Anonymous"
|
|
}
|
|
}
|
|
|
|
lightTheme := isLightTheme(r)
|
|
w.Write(pageHeader(lightTheme, html.EscapeString(profileName)))
|
|
|
|
if profile == nil {
|
|
w.Write([]byte("<li><h1>Account not found</h1></li>"))
|
|
w.Write(pageFooter())
|
|
return
|
|
}
|
|
|
|
w.Write([]byte(`
|
|
<table border="0" cellspacing="5px" width="100%">
|
|
<tr>
|
|
<td width="145" height="145"><a href="https://gravatar.com/` + fmt.Sprintf("%x", md5.Sum([]byte(profile.Email))) + `" target="_blank"><img src="` + gravatar(lightTheme, 200, profile.Email) + `" width="139" height="139" alt="Avatar" border="0"></a></td>
|
|
<td align="left" style="vertical-align: top;"><span style="font-size:1.7em;">` + html.EscapeString(profileName) + `</span><br>
|
|
<small>Joined ` + formatCompleted(profile.Registered) + `</small>`))
|
|
if profile.AllowContact == 1 && strings.TrimSpace(profile.Email) != "" {
|
|
w.Write([]byte(`<br><br><a href="mailto:` + html.EscapeString(strings.TrimSpace(profile.Email)) + `">Send email</a>`))
|
|
}
|
|
w.Write([]byte(`
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<div width="100%" style="position: absolute;bottom: 0;left: 0;right: 0;text-align: center;" align="center">`))
|
|
|
|
/*
|
|
echo '<table border="0" cellspacing="5px" width="100%"><tr><td width="145" height="145" ><a href="https://gravatar.com/' . md5($profile_account["email"]) . '" target="_blank"><img src="' . gravatarFromEmail($profile_account["email"], '200', $lighttheme) . '" width="139" height="139" alt="Avatar" border="0"></a></td>';
|
|
echo '<td align="left" style="vertical-align: top;"><span style="font-size:1.7em;">' . ($profile_account["name"] == "" ? "Anonymous" : cleanString($profile_account["name"])) . '</span><br>' . "\n";
|
|
echo '<small>Joined ' . date("j M Y", $profile_account["registered"]) . "</small>";
|
|
|
|
if ($profile_account["allowcontact"] == 1 && trim($profile_account["email"]) != "") {
|
|
echo "<br><br><a href=\"mailto:" . cleanString(trim($profile_account["email"])) . "\">Send email";
|
|
//echo (trim($profile_account["name"]) != "") ? cleanString(trim($profile_account["name"])) : 'this member';
|
|
//echo 'this member';
|
|
echo "</a>";
|
|
}
|
|
|
|
echo '</td></tr></table>' . "\n";
|
|
|
|
echo '<div width="100%" style="position: absolute;bottom: 0;left: 0;right: 0;text-align: center;" align="center">';
|
|
*/
|
|
|
|
if profile.ID == 1 {
|
|
// TODO easter egg
|
|
}
|
|
|
|
/*
|
|
if ($profile_account["id"] == 1) {
|
|
$num_accounts = mysqli_result(mysqli_query($GLOBALS["___mysqli_ston"], "SELECT COUNT(*) as 'total' FROM `accounts`"), 0, 0);
|
|
$num_sessions = mysqli_result(mysqli_query($GLOBALS["___mysqli_ston"], "SELECT COUNT(*) FROM `sessions`"), 0, 0);
|
|
$sum_sessions_seconds = mysqli_result(mysqli_query($GLOBALS["___mysqli_ston"], "SELECT SUM(`length`) as totalsecs FROM `sessions`"), 0, 0);
|
|
echo '<span style="font-weight: bold;">' . number_format($num_accounts) . '</span> users have posted<br>
|
|
<span style="font-weight: bold;">' . number_format($num_sessions) . '</span> sessions totalling<br>
|
|
<span style="font-weight: bold;">' . $sum_sessions . '</span> of meditation.
|
|
<br><br><br>';
|
|
}
|
|
*/
|
|
if profile.Streak > 1 {
|
|
w.Write([]byte(fmt.Sprintf(`<span style="font-size:1.25em;">%d days of meditation`, profile.Streak)))
|
|
if profile.TopStreak > profile.Streak {
|
|
w.Write([]byte(fmt.Sprintf(`<br><small>(Longest: %d days)</small>`, profile.TopStreak)))
|
|
}
|
|
w.Write([]byte(`</span>`))
|
|
} else if profile.TopStreak > 1 {
|
|
w.Write([]byte(fmt.Sprintf(`<span style="font-size:1.25em;">Longest meditation streak: %d days</span>`, profile.TopStreak)))
|
|
}
|
|
|
|
w.Write([]byte(`</div>`))
|
|
/*
|
|
if ($profile_account["streak"] > 1) {
|
|
echo '<span style="font-size:1.25em;">';
|
|
echo $profile_account["streak"] . ' days of meditation';
|
|
if ($profile_account["topstreak"] > $profile_account["streak"]) {
|
|
echo '<br><small>(Longest: ' . $profile_account["topstreak"] . ' days)</small>';
|
|
}
|
|
echo '</span>';
|
|
} else if ($profile_account["topstreak"] > 1) {
|
|
echo '<span style="font-size:1.25em;">';
|
|
echo 'Longest meditation streak: ' . $profile_account["topstreak"] . ' days';
|
|
echo '</span>';
|
|
}
|
|
echo '</div>';
|
|
*/
|
|
|
|
w.Write(pageFooter())
|
|
}
|
|
|
|
func handleStatus(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
|
|
const statusText = `
|
|
MediNET ONLINE
|
|
|
|
Accounts: %d (%d new)
|
|
Sessions: %d
|
|
Top streak: %d
|
|
`
|
|
fmt.Fprintf(w, strings.TrimSpace(statusText), len(stats.ActiveAccounts), stats.AccountsCreated, stats.SessionsPosted, stats.TopStreak)
|
|
}
|
|
|
|
func handleLegacy(w http.ResponseWriter, r *http.Request) {
|
|
requestPage := r.URL.Query().Get("page")
|
|
for page, handler := range pages {
|
|
if page == requestPage {
|
|
handler(w, r)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Unknown page. Use default handler.
|
|
handleIndex(w, r)
|
|
}
|
|
|
|
func initWeb() {
|
|
for page, handler := range pages {
|
|
http.HandleFunc(fmt.Sprintf("/om/%s", page), handler)
|
|
}
|
|
http.HandleFunc("/om", handleIndex)
|
|
|
|
// Handle legacy requests from pre-1.6.7 clients.
|
|
http.HandleFunc("/client_android.php", handleLegacy)
|
|
http.HandleFunc("/client_android2.php", handleLegacy) // TODO remove
|
|
|
|
// Serve static files.
|
|
staticDirs := map[string]string{
|
|
"/css/": "www/css",
|
|
"/js/": "www/js",
|
|
"/images/": "www/images",
|
|
}
|
|
for webPath, embedPath := range staticDirs {
|
|
fileSystem, err := fs.Sub(wwwFS, embedPath)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
http.Handle(webPath, http.StripPrefix(webPath, http.FileServer(http.FS(fileSystem))))
|
|
}
|
|
}
|
|
|
|
func listenWeb() {
|
|
log.Fatal(http.ListenAndServe(config.Om, nil))
|
|
}
|