Migrate to rocket9labs.com
This commit is contained in:
parent
a17ada4291
commit
ae388d7cad
16 changed files with 580 additions and 185 deletions
|
@ -1,26 +0,0 @@
|
|||
image: golang:latest
|
||||
|
||||
stages:
|
||||
- validate
|
||||
- build
|
||||
|
||||
fmt:
|
||||
stage: validate
|
||||
script:
|
||||
- gofmt -l -s -e .
|
||||
- exit $(gofmt -l -s -e . | wc -l)
|
||||
|
||||
vet:
|
||||
stage: validate
|
||||
script:
|
||||
- go vet -composites=false ./...
|
||||
|
||||
test:
|
||||
stage: validate
|
||||
script:
|
||||
- go test -race -v ./...
|
||||
|
||||
build:
|
||||
stage: build
|
||||
script:
|
||||
- go build
|
|
@ -1,6 +1,5 @@
|
|||
0.1.2:
|
||||
0.1.2: (not yet released)
|
||||
- Add status page
|
||||
- Generate static community page
|
||||
|
||||
0.1.1:
|
||||
- Add TimeZone preference
|
||||
|
|
15
README.md
15
README.md
|
@ -1,9 +1,7 @@
|
|||
# MediNET
|
||||
[![GoDoc](https://godoc.org/gitlab.com/tslocum/medinet?status.svg)](https://godoc.org/gitlab.com/tslocum/medinet)
|
||||
[![CI status](https://gitlab.com/tslocum/medinet/badges/master/pipeline.svg)](https://gitlab.com/tslocum/medinet/commits/master)
|
||||
[![Donate](https://img.shields.io/liberapay/receives/rocketnine.space.svg?logo=liberapay)](https://liberapay.com/rocketnine.space)
|
||||
[![Donate via LiberaPay](https://img.shields.io/liberapay/receives/rocket9labs.com.svg?logo=liberapay)](https://liberapay.com/rocket9labs.com)
|
||||
|
||||
Session repository and community portal for [Meditation Assistant](https://gitlab.com/tslocum/meditationassistant)
|
||||
Session repository and community portal for [Meditation Assistant](https://code.rocket9labs.com/tslocum/meditationassistant)
|
||||
|
||||
## Configure
|
||||
|
||||
|
@ -12,8 +10,9 @@ Example `medinet.conf`:
|
|||
```yaml
|
||||
timezone: "America/Los_Angeles"
|
||||
om: "localhost:10800"
|
||||
dbdriver: "sqlite3"
|
||||
dbsource: "/home/medinet/data/medinet.db"
|
||||
htdocs: "/home/medinet/public_html"
|
||||
dbdriver: "mysql"
|
||||
dbsource: "user:password@localhost/dbname"
|
||||
```
|
||||
|
||||
## Run
|
||||
|
@ -24,11 +23,11 @@ medinet -c ~/.config/medinet/medinet.conf
|
|||
|
||||
## Get Support
|
||||
|
||||
[Open an issue](https://gitlab.com/tslocum/meditationassistant/issues) describing your problem.
|
||||
[Open an issue](https://code.rocket9labs.com/tslocum/meditationassistant/issues) describing your problem.
|
||||
|
||||
## Translate
|
||||
|
||||
Translation is handled [online](https://medinet.rocketnine.space/translate/).
|
||||
Translation is handled [online](https://medinet.rocket9labs.com/translate/).
|
||||
|
||||
## Contribute
|
||||
|
||||
|
|
140
database.go
140
database.go
|
@ -13,11 +13,8 @@ import (
|
|||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// Note: SQLite may not be compiled with support for UPDATE/DELETE LIMIT
|
||||
|
||||
const (
|
||||
databaseVersion = 1
|
||||
accountKeyLength = 32 // Was using MD5 hashes
|
||||
|
@ -77,8 +74,15 @@ var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|||
|
||||
type account struct {
|
||||
ID int
|
||||
Name string
|
||||
Email string
|
||||
Registered int
|
||||
Key string
|
||||
Streak int
|
||||
StreakBuffer int
|
||||
TopStreak int
|
||||
AllowContact int
|
||||
Announcement int
|
||||
}
|
||||
|
||||
type session struct {
|
||||
|
@ -99,6 +103,13 @@ type recentSession struct {
|
|||
AccountEmail string
|
||||
}
|
||||
|
||||
type group struct {
|
||||
ID int `json:"id"`
|
||||
Creator int `json:"creator"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func generateKey() string {
|
||||
b := make([]rune, accountKeyLength)
|
||||
for i := range b {
|
||||
|
@ -117,14 +128,6 @@ func connect(driver string, dataSource string) (*database, error) {
|
|||
}
|
||||
|
||||
d.FuncGreatest = "GREATEST"
|
||||
if config.DBDriver == "sqlite3" {
|
||||
d.FuncGreatest = "MAX"
|
||||
|
||||
_, err = d.db.Exec(`PRAGMA encoding="UTF-8"`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send PRAGMA: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = d.CreateTables()
|
||||
if err != nil {
|
||||
|
@ -278,24 +281,56 @@ func (d *database) authenticate(token string) (*account, error) {
|
|||
return nil, fmt.Errorf("failed to fetch account key: %s", err)
|
||||
}
|
||||
|
||||
account, err := d.getAccount(key)
|
||||
account, err := d.accountByKey(key)
|
||||
failOnError(err)
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (d *database) getAccount(key string) (*account, error) {
|
||||
func (d *database) scanAccount(rows *sql.Rows) (*account, error) {
|
||||
a := new(account)
|
||||
err := d.db.QueryRow("SELECT `id`, `key`, `streakbuffer` FROM accounts WHERE `key`=?", key).Scan(&a.ID, &a.Key, &a.StreakBuffer)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("getAccount error: %s", err)
|
||||
err := rows.Scan(&a.ID, &a.Name, &a.Email, &a.Registered, &a.Key, &a.Streak, &a.StreakBuffer, &a.TopStreak, &a.AllowContact, &a.Announcement)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan account: %s", err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (d *database) accountByID(id int) (*account, error) {
|
||||
rows, err := d.db.Query("SELECT `id`, `name`, `email`, `registered`, `key`, `streak`, `streakbuffer`, `topstreak`, `allowcontact`, `announcement` FROM accounts WHERE `id`=?", id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("accountByID error: %s", err)
|
||||
}
|
||||
for rows.Next() {
|
||||
a, err := d.scanAccount(rows)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("accountByID error: %s", err)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *database) accountByKey(key string) (*account, error) {
|
||||
rows, err := d.db.Query("SELECT `id`, `name`, `email`, `registered`, `key`, `streak`, `streakbuffer`, `topstreak`, `allowcontact`, `announcement` FROM accounts WHERE `key`=?", key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("accountByID error: %s", err)
|
||||
}
|
||||
for rows.Next() {
|
||||
a, err := d.scanAccount(rows)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("accountByID error: %s", err)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (d *database) getStreak(accountID int) (int64, int64, int64, error) {
|
||||
streakDay := int64(0)
|
||||
streakEnd := int64(0)
|
||||
|
@ -527,10 +562,14 @@ func (d *database) sessionExistsByDate(date time.Time, accountID int, streakBuff
|
|||
return sessionid > 0, nil
|
||||
}
|
||||
|
||||
func (d *database) getAllSessions(accountID int) ([]*session, error) {
|
||||
func (d *database) getAllSessions(accountID int, sortDescending bool) ([]*session, error) {
|
||||
var sessions []*session
|
||||
|
||||
rows, err := d.db.Query("SELECT `id`, `posted`, `started`, `streakday`, `length`, `completed`, `message`, `modified` FROM sessions WHERE `account`=?", accountID)
|
||||
querySort := "ASC"
|
||||
if sortDescending {
|
||||
querySort = "DESC"
|
||||
}
|
||||
rows, err := d.db.Query("SELECT `id`, `posted`, `started`, `streakday`, `length`, `completed`, `message`, `modified` FROM sessions WHERE `account`=? ORDER BY `completed` "+querySort, accountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch sessions: %s", err)
|
||||
}
|
||||
|
@ -581,3 +620,64 @@ func (d *database) deleteSession(started int, accountID int) (bool, error) {
|
|||
|
||||
return affected > 0, nil
|
||||
}
|
||||
|
||||
func (d *database) getAllGroups() ([]group, error) {
|
||||
var groups []group
|
||||
|
||||
rows, err := d.db.Query("SELECT `id`, `name`, `creator`, `description` FROM groups ORDER BY `name` ASC")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch groups: %s", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
g := group{}
|
||||
err = rows.Scan(&g.ID, &g.Name, &g.Creator, &g.Description)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan group: %s", err)
|
||||
}
|
||||
|
||||
groups = append(groups, g)
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (d *database) groupMemberCount(groupID int) (int, error) {
|
||||
rows, err := d.db.Query("SELECT COUNT(*) as c FROM `groupmembers` WHERE `group` =?", groupID)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to fetch member count: %s", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var memberCount int
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&memberCount)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to fetch member count: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return memberCount, nil
|
||||
}
|
||||
|
||||
func (d *database) showAnnouncement(a *account) (string, error) {
|
||||
rows, err := d.db.Query("SELECT `id`, `text` FROM announcements WHERE `active` = 1 AND `id` > ? ORDER BY `id` ASC limit 1", a.Announcement)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("showAnnouncement error: %s", err)
|
||||
}
|
||||
var id int
|
||||
var text string
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&id, &text)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to scan announcement: %s", err)
|
||||
}
|
||||
a.Announcement = id
|
||||
_, err = d.db.Exec("UPDATE accounts SET announcement = ? WHERE `id` = ? LIMIT 1", a.Announcement, a.ID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to update account: %s", err)
|
||||
}
|
||||
}
|
||||
return text, nil
|
||||
}
|
||||
|
|
12
go.mod
12
go.mod
|
@ -1,10 +1,10 @@
|
|||
module gitlab.com/tslocum/medinet
|
||||
module code.rocket9labs.com/tslocum/medinet
|
||||
|
||||
go 1.15
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/jessevdk/go-flags v1.4.0
|
||||
github.com/mattn/go-sqlite3 v1.14.4
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require filippo.io/edwards25519 v1.1.0 // indirect
|
||||
|
|
14
go.sum
14
go.sum
|
@ -1,10 +1,8 @@
|
|||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI=
|
||||
github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
|
22
main.go
22
main.go
|
@ -1,5 +1,5 @@
|
|||
// MediNET - session repository and community portal for Meditation Assistant
|
||||
// https://gitlab.com/tslocum/medinet
|
||||
// MediNET - Session repository and community portal for Meditation Assistant
|
||||
// https://code.rocket9labs.com/tslocum/medinet
|
||||
//
|
||||
// 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
|
||||
|
@ -17,6 +17,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
@ -25,7 +26,6 @@ import (
|
|||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
|
@ -113,18 +113,20 @@ func printStatistics() {
|
|||
|
||||
func main() {
|
||||
var opts struct {
|
||||
ConfigFile string `short:"c" long:"config" description:"Configuration file"`
|
||||
Debug bool `short:"d" long:"debug" description:"Print debug information"`
|
||||
ConfigFile string
|
||||
Debug bool
|
||||
}
|
||||
_, err := flags.Parse(&opts)
|
||||
failOnError(err)
|
||||
flag.StringVar(&opts.ConfigFile, "config", "", "Configuration file")
|
||||
flag.BoolVar(&opts.Debug, "debug", false, "Print debug information")
|
||||
flag.Parse()
|
||||
|
||||
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 {
|
||||
_, err := os.Stat(opts.ConfigFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Configuration file %s does not exist: %s", opts.ConfigFile, err)
|
||||
}
|
||||
|
||||
|
@ -145,7 +147,7 @@ func main() {
|
|||
log.Fatal("Specify Web directory in configuration file")
|
||||
}
|
||||
|
||||
tz := "Local"
|
||||
tz := "UTC"
|
||||
if config.TimeZone != "" {
|
||||
tz = config.TimeZone
|
||||
}
|
||||
|
@ -163,4 +165,6 @@ func main() {
|
|||
|
||||
initWeb()
|
||||
listenWeb()
|
||||
|
||||
log.Printf("Listening on %+v", config.Om)
|
||||
}
|
||||
|
|
399
web.go
399
web.go
|
@ -1,9 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -11,14 +14,46 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
var updateCommunity = make(chan struct{})
|
||||
//go:embed www
|
||||
var wwwFS embed.FS
|
||||
|
||||
func handleMediNET(w http.ResponseWriter, r *http.Request) {
|
||||
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.Header().Set("Location", "/signin")
|
||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||
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()
|
||||
|
||||
// Authenticate (signin)
|
||||
// Send key, client will then call (connect)
|
||||
// Handle logging in.
|
||||
token := r.FormValue("token")
|
||||
if token != "" {
|
||||
a, err := db.authenticate(token)
|
||||
|
@ -37,23 +72,11 @@ func handleMediNET(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
key := r.URL.Query().Get("x")
|
||||
if key == "" {
|
||||
key = r.FormValue("x")
|
||||
}
|
||||
|
||||
a, err := db.getAccount(key)
|
||||
if err != nil {
|
||||
log.Printf("ERROR! %v", err)
|
||||
// Require authentication.
|
||||
acc := authenticateUser(w, r)
|
||||
if acc == nil {
|
||||
return
|
||||
}
|
||||
if a == nil {
|
||||
w.Header().Set("x-MediNET", "signin")
|
||||
logDebugf("Asking to sign in %s %q", key, html.EscapeString(r.URL.RequestURI()))
|
||||
|
||||
return
|
||||
}
|
||||
go trackActiveAccount(a.ID)
|
||||
|
||||
data := make(map[string]interface{})
|
||||
data["status"] = "success"
|
||||
|
@ -80,7 +103,7 @@ func handleMediNET(w http.ResponseWriter, r *http.Request) {
|
|||
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)
|
||||
match = regexpNumbers.FindAllString(av[0:len(av)-len(match[1])], -1)
|
||||
if match != nil {
|
||||
appver, err = strconv.Atoi(strings.Join(match, ""))
|
||||
if err != nil {
|
||||
|
@ -102,14 +125,19 @@ func handleMediNET(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if streakbuffer >= 0 {
|
||||
if a.StreakBuffer != streakbuffer {
|
||||
a.StreakBuffer = streakbuffer
|
||||
go db.updateStreakBuffer(a.ID, streakbuffer)
|
||||
if acc.StreakBuffer != streakbuffer {
|
||||
acc.StreakBuffer = streakbuffer
|
||||
go db.updateStreakBuffer(acc.ID, streakbuffer)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: read from announcement table on successful connect
|
||||
//data["announce"] = "First line\n\nSecond line"
|
||||
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":
|
||||
|
@ -125,7 +153,7 @@ func handleMediNET(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
deleted, err := db.deleteSession(started, a.ID)
|
||||
deleted, err := db.deleteSession(started, acc.ID)
|
||||
if err != nil {
|
||||
log.Printf("ERROR! %v", err)
|
||||
return
|
||||
|
@ -133,7 +161,7 @@ func handleMediNET(w http.ResponseWriter, r *http.Request) {
|
|||
data["result"] = "deleted"
|
||||
}
|
||||
case "downloadsessions":
|
||||
sessions, err := db.getAllSessions(a.ID)
|
||||
sessions, err := db.getAllSessions(acc.ID, false)
|
||||
if err != nil {
|
||||
log.Printf("ERROR! %v", err)
|
||||
return
|
||||
|
@ -163,7 +191,7 @@ func handleMediNET(w http.ResponseWriter, r *http.Request) {
|
|||
sessionsuploaded := 0
|
||||
|
||||
for _, session := range uploadsessions {
|
||||
uploaded, err = db.addSession(session, updateSessionStarted, a.ID, av, appmarket)
|
||||
uploaded, err = db.addSession(session, updateSessionStarted, acc.ID, av, appmarket)
|
||||
if err != nil {
|
||||
log.Printf("ERROR! %v", err)
|
||||
return
|
||||
|
@ -193,7 +221,7 @@ func handleMediNET(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
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(a.ID, a.StreakBuffer, tz)
|
||||
streak, err := db.calculateStreak(acc.ID, acc.StreakBuffer, tz)
|
||||
if err != nil {
|
||||
log.Printf("ERROR! %v", err)
|
||||
return
|
||||
|
@ -204,14 +232,14 @@ func handleMediNET(w http.ResponseWriter, r *http.Request) {
|
|||
if streak < streakday {
|
||||
streak = streakday
|
||||
} else if streak > streakday {
|
||||
err = db.setSessionStreakDay(started, streak, a.ID)
|
||||
err = db.setSessionStreakDay(started, streak, acc.ID)
|
||||
if err != nil {
|
||||
log.Printf("ERROR! %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = db.setStreak(streak, a.ID, a.StreakBuffer, tz)
|
||||
err = db.setStreak(streak, acc.ID, acc.StreakBuffer, tz)
|
||||
if err != nil {
|
||||
log.Printf("ERROR! %v", err)
|
||||
return
|
||||
|
@ -237,7 +265,7 @@ func handleMediNET(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// Send streak
|
||||
if action == "connect" || action == "downloadsessions" || action == "uploadsessions" {
|
||||
streakday, streakend, topstreak, err := db.getStreak(a.ID)
|
||||
streakday, streakend, topstreak, err := db.getStreak(acc.ID)
|
||||
if err != nil {
|
||||
log.Printf("ERROR! %v", err)
|
||||
return
|
||||
|
@ -259,38 +287,295 @@ func handleMediNET(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
logDebugf("App: %d API: %d Action: %s - %q", appver, apiver, action, html.EscapeString(r.URL.RequestURI()))
|
||||
logDebugf("Account ID: %d, JSON: %s", a.ID, string(j))
|
||||
logDebugf("Account ID: %d, JSON: %s", acc.ID, string(j))
|
||||
}
|
||||
|
||||
if action == "uploadsessions" || action == "deletesession" {
|
||||
updateCommunity <- struct{}{}
|
||||
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() {
|
||||
http.HandleFunc("/om/sessions", func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
})
|
||||
for page, handler := range pages {
|
||||
http.HandleFunc(fmt.Sprintf("/om/%s", page), handler)
|
||||
}
|
||||
http.HandleFunc("/om", handleIndex)
|
||||
|
||||
http.HandleFunc("/om/account", func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
})
|
||||
// Handle legacy requests from pre-1.6.7 clients.
|
||||
http.HandleFunc("/client_android.php", handleLegacy)
|
||||
http.HandleFunc("/client_android2.php", handleLegacy) // TODO remove
|
||||
|
||||
http.HandleFunc("/om/forum", func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
})
|
||||
|
||||
http.HandleFunc("/om/groups", func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO
|
||||
})
|
||||
|
||||
http.HandleFunc("/om/status", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "MediNET ONLINE<br><br>Accounts: %d (%d new)<br>Sessions: %d<br>Top streak: %d", len(stats.ActiveAccounts), stats.AccountsCreated, stats.SessionsPosted, stats.TopStreak)
|
||||
})
|
||||
|
||||
http.HandleFunc("/om", handleMediNET)
|
||||
|
||||
go handleUpdateCommunity()
|
||||
updateCommunity <- struct{}{}
|
||||
// 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() {
|
||||
|
|
117
webpage.go
117
webpage.go
|
@ -1,45 +1,55 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"html"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
func pageHeader(light bool, title string) []byte {
|
||||
func isLightTheme(r *http.Request) bool {
|
||||
theme := r.URL.Query().Get("th")
|
||||
if theme == "light" || theme == "lightdark" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func pageHeader(lightTheme bool, title string) []byte {
|
||||
theme := "dark"
|
||||
if light {
|
||||
if lightTheme {
|
||||
theme = "light"
|
||||
}
|
||||
return []byte(`<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>` + title + `</title>
|
||||
<link rel="stylesheet" type="text/css" href="css/medinet.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/holo.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/holo-` + theme + `.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/medinet.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/holo.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/holo-` + theme + `.css">
|
||||
<script src="/js/medinet.js"></script>
|
||||
</head>
|
||||
<body class="holo-` + theme + `">
|
||||
<body class="holo holo-` + theme + `">
|
||||
<div class="holo-content">
|
||||
<ul class="holo-list">
|
||||
`)
|
||||
}
|
||||
|
||||
func pageFooter() []byte {
|
||||
return []byte(`</div>
|
||||
return []byte(`
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>`)
|
||||
</html>
|
||||
`)
|
||||
}
|
||||
|
||||
func gravatar(light bool, size int, email string) string {
|
||||
d := "https://medinet.ftp.sh/images/ic_om_sq_small_dark.png"
|
||||
d := "https://medinet.rocket9labs.com/images/om_dark.png"
|
||||
if light {
|
||||
d = "https://medinet.ftp.sh/images/ic_om_sq_small_light.png"
|
||||
d = "https://medinet.rocket9labs.com/images/om_light.png"
|
||||
}
|
||||
return fmt.Sprintf("https://www.gravatar.com/avatar/%x?s=%d&d=%s", md5.Sum([]byte(email)), size, url.QueryEscape(d))
|
||||
}
|
||||
|
@ -59,16 +69,21 @@ func formatRecentCompleted(completed int) string {
|
|||
return c.Format("3:04 Jan 2")
|
||||
}
|
||||
|
||||
func formatStreak(streakday int) string {
|
||||
if streakday == 0 {
|
||||
func formatStreak(streakDay int) string {
|
||||
if streakDay == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
s := ""
|
||||
if streakday > 1 {
|
||||
if streakDay > 1 {
|
||||
s = "s"
|
||||
}
|
||||
return fmt.Sprintf("<p>%d day%s of meditation</p>", streakday, s)
|
||||
formatted := fmt.Sprintf("%d day%s of meditation", streakDay, s)
|
||||
if streakDay < 10 {
|
||||
formatted = "<small>" + formatted + "</small>"
|
||||
}
|
||||
|
||||
return "<p>" + formatted + "</p>"
|
||||
}
|
||||
|
||||
func formatMessage(message string) string {
|
||||
|
@ -78,45 +93,49 @@ func formatMessage(message string) string {
|
|||
return "<p><small>" + html.EscapeString(message) + "</small></p>"
|
||||
}
|
||||
|
||||
func communityPage(light bool) []byte {
|
||||
var b bytes.Buffer
|
||||
|
||||
b.Write(pageHeader(light, "Community"))
|
||||
|
||||
recentSessions, err := db.getRecentSessions()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
func escapeName(name string) string {
|
||||
escapedName := html.EscapeString(name)
|
||||
if len(name) >= 15 {
|
||||
escapedName = "<small>" + escapedName + "</small>"
|
||||
}
|
||||
return escapedName
|
||||
}
|
||||
|
||||
format := `<li onclick="goToAccount('%d');">
|
||||
<table border="0" cellspacing="3px" cellpadding="0px" width="100%%">
|
||||
<tr>
|
||||
<td width="57px" height="57px" style="margin: 0px;padding: 0px;">
|
||||
func printSession(w http.ResponseWriter, lightTheme bool, s *recentSession) {
|
||||
showAccount := s.AccountID != 0
|
||||
|
||||
w.Write([]byte(`<table border="0" cellspacing="3px" cellpadding="0px" width="100%">
|
||||
<tr>`))
|
||||
|
||||
if showAccount {
|
||||
w.Write([]byte(fmt.Sprintf(`<td width="57px" height="57px" style="margin: 0px;padding: 0px;">
|
||||
<img src="%s" width="57px" height="57px" style="margin: 0px;padding: 0px;">
|
||||
</td>
|
||||
<td>
|
||||
<div style="padding-left: 4px;"><span style="font-size: 2em;font-weight: bold;">%s</span>
|
||||
%s
|
||||
<td>`, gravatar(lightTheme, 104, s.AccountEmail))))
|
||||
} else {
|
||||
w.Write([]byte(`<td colspan="2">`))
|
||||
}
|
||||
|
||||
w.Write([]byte(fmt.Sprintf(`<div style="padding-left: 4px;"><span style="font-size: 2em;font-weight: bold;">%s</span>`, formatLength(s.Length))))
|
||||
|
||||
if showAccount {
|
||||
w.Write([]byte("<br>" + escapeName(s.AccountName)))
|
||||
}
|
||||
|
||||
var completed []byte
|
||||
if showAccount {
|
||||
completed = []byte(formatRecentCompleted(s.Completed))
|
||||
} else {
|
||||
completed = []byte(formatCompleted(s.Completed))
|
||||
}
|
||||
w.Write([]byte(fmt.Sprintf(`
|
||||
</div></td>
|
||||
<td align="right" style="vertical-align:top;font-size: 0.85em;">
|
||||
%s
|
||||
</td></tr>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
%s
|
||||
%s
|
||||
</li>`
|
||||
for _, rc := range recentSessions {
|
||||
b.WriteString(fmt.Sprintf(format, rc.AccountID, gravatar(light, 104, rc.AccountEmail), formatLength(rc.Length), html.EscapeString(rc.AccountName), formatRecentCompleted(rc.Completed), formatStreak(rc.StreakDay), formatMessage(rc.Message)))
|
||||
}
|
||||
|
||||
b.Write(pageFooter())
|
||||
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func handleUpdateCommunity() {
|
||||
for range updateCommunity {
|
||||
ioutil.WriteFile(path.Join(config.Web, "community.html"), communityPage(false), 0655)
|
||||
ioutil.WriteFile(path.Join(config.Web, "community_light.html"), communityPage(true), 0655)
|
||||
}
|
||||
`, completed, formatStreak(s.StreakDay), formatMessage(s.Message))))
|
||||
}
|
||||
|
|
BIN
www/images/om_dark.png
Normal file
BIN
www/images/om_dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
BIN
www/images/om_light.png
Normal file
BIN
www/images/om_light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
17
www/js/medinet.js
Normal file
17
www/js/medinet.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
function goToAccount(id) {
|
||||
window.location.href = '?v=page=profile&account=' + id;
|
||||
}
|
||||
|
||||
function goToThread(id) {
|
||||
window.location.href = '?v=page=forum&thread=' + id;
|
||||
}
|
||||
|
||||
function deleteSession(id) {
|
||||
window.location.href = '?v=page=sessions&delete=' + id;
|
||||
}
|
||||
|
||||
function toggleOption(id) {
|
||||
var chkbox = document.getElementById(id);
|
||||
chkbox.checked = !chkbox.checked;
|
||||
return true;
|
||||
}
|
Loading…
Reference in a new issue