584 lines
17 KiB
Go
584 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"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
|
|
MessageMaxLength = 4096
|
|
GoogleOAuthURL = "https://www.googleapis.com/oauth2/v3/userinfo?alt=json&access_token="
|
|
)
|
|
|
|
// TODO: Add indexes
|
|
var DatabaseTables = map[string][]string{
|
|
"accounts": {
|
|
"`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT",
|
|
"`key` VARCHAR(145) NOT NULL DEFAULT ''",
|
|
"`google_id` VARCHAR(200) NOT NULL DEFAULT ''",
|
|
"`facebook_id` VARCHAR(200) NOT NULL DEFAULT ''",
|
|
"`twitter_id` VARCHAR(200) NOT NULL DEFAULT ''",
|
|
"`openid_id` VARCHAR(200) NOT NULL DEFAULT ''",
|
|
"`email` VARCHAR(254) NOT NULL DEFAULT ''",
|
|
"`name` VARCHAR(50) NOT NULL DEFAULT ''",
|
|
"`registered` INTEGER UNSIGNED NOT NULL DEFAULT 0",
|
|
"`lastactive` INTEGER UNSIGNED NOT NULL DEFAULT 0",
|
|
"`streak` SMALLINT UNSIGNED NOT NULL DEFAULT 0",
|
|
"`topstreak` SMALLINT UNSIGNED NOT NULL DEFAULT 0",
|
|
"`streakend` INTEGER UNSIGNED NOT NULL DEFAULT 0",
|
|
"`streakbuffer` MEDIUMINT UNSIGNED NOT NULL DEFAULT 0",
|
|
"`announcement` SMALLINT UNSIGNED NOT NULL DEFAULT 0",
|
|
"`sessionspublic` TINYINT UNSIGNED NOT NULL DEFAULT 0",
|
|
"`allowcontact` TINYINT UNSIGNED NOT NULL DEFAULT 0"},
|
|
"announcements": {
|
|
"`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT",
|
|
"`posted` INTEGER UNSIGNED NOT NULL DEFAULT 0",
|
|
"`text` TEXT NOT NULL DEFAULT ''",
|
|
"`active` TINYINT UNSIGNED NOT NULL DEFAULT 0"},
|
|
"sessions": {
|
|
"`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT",
|
|
"`account` INTEGER NOT NULL DEFAULT 0",
|
|
"`api` VARCHAR(145) NOT NULL DEFAULT ''",
|
|
"`ip` VARCHAR(145) NOT NULL DEFAULT ''",
|
|
"`market` VARCHAR(145) NOT NULL DEFAULT ''",
|
|
"`app` VARCHAR(145) NOT NULL DEFAULT ''",
|
|
"`posted` INTEGER UNSIGNED NOT NULL DEFAULT 0",
|
|
"`started` INTEGER UNSIGNED NOT NULL DEFAULT 0",
|
|
"`length` MEDIUMINT UNSIGNED NOT NULL DEFAULT 0",
|
|
"`completed` INTEGER UNSIGNED NOT NULL DEFAULT 0",
|
|
"`message` TEXT NOT NULL DEFAULT ''",
|
|
"`streakday` SMALLINT UNSIGNED NOT NULL DEFAULT 0",
|
|
"`modified` INTEGER UNSIGNED NOT NULL DEFAULT 0"},
|
|
"meta": {
|
|
"`key` VARCHAR(50) NOT NULL PRIMARY KEY",
|
|
"`value` TEXT NOT NULL DEFAULT ''"}}
|
|
|
|
type Database struct {
|
|
db *sql.DB
|
|
FuncGreatest string
|
|
}
|
|
|
|
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
|
|
type Account struct {
|
|
ID int
|
|
Key string
|
|
StreakBuffer int
|
|
}
|
|
|
|
type Session struct {
|
|
ID int `json:"id"`
|
|
Posted int `json:"posted"`
|
|
Started int `json:"started"`
|
|
StreakDay int `json:"streakday"`
|
|
Length int `json:"length"`
|
|
Completed int `json:"completed"`
|
|
Message string `json:"message"`
|
|
Modified int `json:"modified"`
|
|
}
|
|
|
|
type RecentSession struct {
|
|
Session
|
|
AccountID int
|
|
AccountName string
|
|
AccountEmail string
|
|
}
|
|
|
|
func generateKey() string {
|
|
b := make([]rune, AccountKeyLength)
|
|
for i := range b {
|
|
b[i] = letters[rand.Intn(len(letters))]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func Connect(driver string, dataSource string) (*Database, error) {
|
|
var err error
|
|
d := new(Database)
|
|
|
|
d.db, err = sql.Open(driver, dataSource)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect to database: %s", err)
|
|
}
|
|
|
|
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 {
|
|
_ = d.db.Close()
|
|
return nil, fmt.Errorf("failed to create tables: %s", err)
|
|
}
|
|
|
|
err = d.Migrate()
|
|
if err != nil {
|
|
_ = d.db.Close()
|
|
return nil, fmt.Errorf("failed to migrate database: %s", err)
|
|
}
|
|
|
|
return d, nil
|
|
}
|
|
|
|
func (d *Database) CreateTables() error {
|
|
var (
|
|
tcolumns string
|
|
err error
|
|
)
|
|
|
|
createQueryExtra := ""
|
|
if config.DBDriver == "mysql" {
|
|
createQueryExtra = " ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_unicode_ci"
|
|
}
|
|
|
|
for tname, tcols := range DatabaseTables {
|
|
tcolumns = strings.Join(tcols, ",")
|
|
if config.DBDriver == "mysql" {
|
|
tcolumns = strings.Replace(tcolumns, "AUTOINCREMENT", "AUTO_INCREMENT", -1)
|
|
}
|
|
|
|
_, err = d.db.Exec(fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s` (%s)", tname, tcolumns) + createQueryExtra)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create table %s: %s", tname, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) Migrate() error {
|
|
rows, err := d.db.Query("SELECT `value` FROM meta WHERE `key`=?", "version")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch database version: %s", err)
|
|
}
|
|
|
|
version := 0
|
|
for rows.Next() {
|
|
v := ""
|
|
err = rows.Scan(&v)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to scan database meta: %s", err)
|
|
}
|
|
|
|
version, err = strconv.Atoi(v)
|
|
if err != nil {
|
|
version = -1
|
|
}
|
|
}
|
|
|
|
if version == -1 {
|
|
panic("Unable to migrate database: database version unknown")
|
|
} else if version == 0 {
|
|
_, err := d.db.Exec("UPDATE meta SET `value`=? WHERE `key`=?", strconv.Itoa(DatabaseVersion), "version")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save database version: %s", err)
|
|
}
|
|
}
|
|
|
|
migrated := false
|
|
for version < DatabaseVersion {
|
|
switch version {
|
|
case 1:
|
|
// DatabaseVersion 2 migration queries will go here
|
|
}
|
|
|
|
version++
|
|
migrated = true
|
|
}
|
|
|
|
if migrated {
|
|
_, err := d.db.Exec("UPDATE meta SET `value`=? WHERE `key`=?", strconv.Itoa(DatabaseVersion), "version")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save updated database version: %s", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) authenticate(token string) (*Account, error) {
|
|
key := ""
|
|
|
|
resp, err := http.Get(GoogleOAuthURL + token)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get userinfo from Google: %s", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
data, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read userinfo response from Google: %s", err)
|
|
}
|
|
|
|
var userinfo map[string]interface{}
|
|
err = json.Unmarshal(data, &userinfo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal userinfo response from Google: %s", err)
|
|
}
|
|
|
|
googleid := ""
|
|
email := ""
|
|
name := ""
|
|
|
|
if v, ok := userinfo["sub"]; ok {
|
|
googleid = v.(string)
|
|
}
|
|
|
|
if googleid == "" || googleid == "0" {
|
|
logDebugf("Userinfo: %+v", userinfo)
|
|
logDebugf("Access token: %+v", googleid)
|
|
return nil, errors.New("invalid access token")
|
|
}
|
|
|
|
if v, ok := userinfo["email"]; ok {
|
|
email = v.(string)
|
|
if len(email) > 75 {
|
|
email = ""
|
|
}
|
|
}
|
|
|
|
if v, ok := userinfo["name"]; ok {
|
|
name = v.(string)
|
|
if len(name) > 50 {
|
|
name = name[0:50]
|
|
}
|
|
}
|
|
|
|
err = d.db.QueryRow("SELECT `key` FROM accounts WHERE google_id=?", googleid).Scan(&key)
|
|
if err == sql.ErrNoRows {
|
|
key = generateKey()
|
|
|
|
_, err = d.db.Exec("INSERT INTO accounts (`key`, `google_id`, `email`, `name`, `registered`) VALUES(?, ?, ?, ?, ?)", key, googleid, email, name, time.Now().Unix())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to insert account: %s", err)
|
|
}
|
|
stats.AccountsCreated++
|
|
} else if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch account key: %s", err)
|
|
}
|
|
|
|
account, err := d.getAccount(key)
|
|
failOnError(err)
|
|
|
|
return account, nil
|
|
}
|
|
|
|
func (d *Database) getAccount(key string) (*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)
|
|
}
|
|
|
|
return a, nil
|
|
}
|
|
|
|
func (d *Database) getStreak(accountID int) (int64, int64, int64, error) {
|
|
streakDay := int64(0)
|
|
streakEnd := int64(0)
|
|
topStreak := int64(0)
|
|
|
|
err := d.db.QueryRow("SELECT `streak`, `streakend`, `topstreak` FROM accounts WHERE `id`=?", accountID).Scan(&streakDay, &streakEnd, &topStreak)
|
|
if err == sql.ErrNoRows {
|
|
return 0, 0, 0, errors.New("invalid account ID")
|
|
} else if err != nil {
|
|
return 0, 0, 0, fmt.Errorf("getStreak error: %s", err)
|
|
}
|
|
|
|
// Expire streak
|
|
if streakEnd <= time.Now().Unix() {
|
|
streakDay = 0
|
|
streakEnd = 0
|
|
|
|
_, err := d.db.Exec("UPDATE accounts SET `streak`=?, `streakend`=? WHERE `id`=?", streakDay, streakEnd, accountID)
|
|
if err != nil {
|
|
return 0, 0, 0, fmt.Errorf("failed to expire streak: %s", err)
|
|
}
|
|
}
|
|
|
|
return streakDay, streakEnd, topStreak, nil
|
|
}
|
|
|
|
func (d *Database) updateLastActive(accountID int) error {
|
|
_, err := d.db.Exec("UPDATE accounts SET `lastactive`=? WHERE `id`=?", time.Now().Unix(), accountID)
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to update last active: %s", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (d *Database) updateTopStreak(accountID int) error {
|
|
_, err := d.db.Exec("UPDATE accounts SET `topstreak`="+d.FuncGreatest+"(`streak`, `topstreak`) WHERE `id`=?", accountID)
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to update top streak: %s", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (d *Database) updateStreakBuffer(accountID int, streakBuffer int) error {
|
|
_, err := d.db.Exec("UPDATE accounts SET `streakbuffer`=? WHERE `id`=?", streakBuffer, accountID)
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to update streak buffer: %s", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (d *Database) calculateStreak(accountID int, streakBuffer int, tz *time.Location) (int, error) {
|
|
streak := 0
|
|
|
|
t := time.Now().In(tz)
|
|
logDebugf("calculate start %v", t)
|
|
if beforeWindowStart(t, streakBuffer) {
|
|
t = t.AddDate(0, 0, -1)
|
|
logDebugf("calculate added %v", t)
|
|
}
|
|
|
|
for {
|
|
exists, err := d.sessionExistsByDate(t, accountID, streakBuffer)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to check if session exists for date: %s", err)
|
|
} else if exists {
|
|
streak++
|
|
t = t.AddDate(0, 0, -1)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
logDebugf("calculated streak as %d", streak)
|
|
return streak, nil
|
|
}
|
|
|
|
func (d *Database) setStreak(streakDay int, accountID int, streakBuffer int, tz *time.Location) error {
|
|
t := time.Now().In(tz)
|
|
if beforeWindowStart(t, streakBuffer) {
|
|
t = t.AddDate(0, 0, 1)
|
|
} else {
|
|
t = t.AddDate(0, 0, 2)
|
|
}
|
|
t = atWindowStart(t, streakBuffer)
|
|
|
|
logDebugf("SETTING STREAK Account %d, Day %d, TZ %s, Streak end: %d", accountID, streakDay, tz.String(), t.Unix())
|
|
|
|
_, err := d.db.Exec("UPDATE accounts SET `streak`=?, `streakend`=? WHERE `id`=?", streakDay, t.Unix(), accountID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update streak: %s", err)
|
|
}
|
|
|
|
err = d.updateTopStreak(accountID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update top streak: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) setSessionStreakDay(started int, streakDay int, accountID int) error {
|
|
_, err := d.db.Exec("UPDATE sessions SET `streakday`=? WHERE `account`=? AND `started`=?", streakDay, accountID, started)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set session streak day: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) scanSession(rows *sql.Rows) (*Session, error) {
|
|
s := new(Session)
|
|
err := rows.Scan(&s.ID, &s.Posted, &s.Started, &s.StreakDay, &s.Length, &s.Completed, &s.Message, &s.Modified)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan session: %s", err)
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
func (d *Database) scanRecentSession(rows *sql.Rows) (*RecentSession, error) {
|
|
s := new(RecentSession)
|
|
err := rows.Scan(&s.ID, &s.Posted, &s.Started, &s.StreakDay, &s.Length, &s.Completed, &s.Message, &s.Modified, &s.AccountID, &s.AccountName, &s.AccountEmail)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan session: %s", err)
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
func (d *Database) addSession(s Session, updateSessionStarted int, accountID int, appVer string, appMarket string) (bool, error) {
|
|
var (
|
|
existingSession *Session
|
|
updateSession *Session
|
|
keepSession *Session
|
|
|
|
err error
|
|
)
|
|
|
|
existingSession, err = d.getSessionByStarted(s.Started, accountID)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to fetch session: %s", err)
|
|
}
|
|
|
|
if updateSessionStarted > 0 && updateSessionStarted != s.Started {
|
|
updateSession, err = d.getSessionByStarted(updateSessionStarted, accountID)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to fetch session: %s", err)
|
|
}
|
|
}
|
|
|
|
if (existingSession != nil && existingSession.Modified >= s.Modified) || (updateSession != nil && updateSession.Modified >= s.Modified) {
|
|
return false, nil
|
|
}
|
|
|
|
if len(s.Message) > MessageMaxLength {
|
|
s.Message = s.Message[:MessageMaxLength]
|
|
}
|
|
|
|
// Fix zero completed from older versions of the app
|
|
if s.Completed == 0 {
|
|
s.Completed = s.Started + s.Length
|
|
}
|
|
|
|
if existingSession == nil && updateSession == nil {
|
|
_, err = d.db.Exec("INSERT INTO sessions (`account`, `market`, `app`, `posted`, `started`, `streakday`, `length`, `completed`, `message`, `modified`) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", accountID, appMarket, appVer, time.Now().Unix(), s.Started, s.StreakDay, s.Length, s.Completed, s.Message, s.Modified)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to add session: %s", err)
|
|
}
|
|
} else {
|
|
keepSession = updateSession
|
|
if keepSession == nil {
|
|
keepSession = existingSession
|
|
} else if existingSession != nil {
|
|
_, err = db.deleteSession(existingSession.Started, accountID)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to delete existing session: %s", err)
|
|
}
|
|
}
|
|
|
|
_, err = d.db.Exec("UPDATE sessions SET `started`=?, `length`=?, `completed`=?, `message`=?, `modified`=? WHERE `account`=? AND `started`=?", s.Started, s.Length, s.Completed, s.Message, s.Modified, accountID, keepSession.Started)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to update session: %s", err)
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (d *Database) getSessionByID(sessionID int, accountID int) (*Session, error) {
|
|
rows, err := d.db.Query("SELECT `id`, `posted`, `started`, `streakday`, `length`, `completed`, `message`, `modified` FROM sessions WHERE `account`=? AND `id`=?", accountID, sessionID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch session: %s", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
return d.scanSession(rows)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (d *Database) getSessionByStarted(started int, accountID int) (*Session, error) {
|
|
rows, err := d.db.Query("SELECT `id`, `posted`, `started`, `streakday`, `length`, `completed`, `message`, `modified` FROM sessions WHERE `account`=? AND `started`=?", accountID, started)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch session: %s", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
return d.scanSession(rows)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (d *Database) sessionExistsByDate(date time.Time, accountID int, streakBuffer int) (bool, error) {
|
|
windowStart := atWindowStart(date, streakBuffer)
|
|
windowEnd := atWindowStart(windowStart.AddDate(0, 0, 1), streakBuffer)
|
|
|
|
logDebugf("SESSION EXISTS %v - START %v END %v", date, windowStart.Unix(), windowEnd.Unix())
|
|
sessionid := 0
|
|
|
|
err := d.db.QueryRow("SELECT `id` FROM sessions WHERE `account`=? AND `started`>=? AND `started`<? LIMIT 1", accountID, windowStart.Unix(), windowEnd.Unix()).Scan(&sessionid)
|
|
if err != nil && err != sql.ErrNoRows {
|
|
return false, fmt.Errorf("sessionExistsByDate failed: %s", err)
|
|
}
|
|
|
|
return sessionid > 0, nil
|
|
}
|
|
|
|
func (d *Database) getAllSessions(accountID int) ([]*Session, error) {
|
|
var sessions []*Session
|
|
|
|
rows, err := d.db.Query("SELECT `id`, `posted`, `started`, `streakday`, `length`, `completed`, `message`, `modified` FROM sessions WHERE `account`=?", accountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch sessions: %s", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
s, err := d.scanSession(rows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan session: %s", err)
|
|
}
|
|
|
|
sessions = append(sessions, s)
|
|
}
|
|
|
|
return sessions, nil
|
|
}
|
|
|
|
func (d *Database) getRecentSessions() ([]*RecentSession, error) {
|
|
var sessions []*RecentSession
|
|
|
|
rows, err := d.db.Query("SELECT `sessions`.`id`, `sessions`.`posted`, `sessions`.`started`, `sessions`.`streakday`, `sessions`.`length`, `sessions`.`completed`, `sessions`.`message`, `sessions`.`modified`, `accounts`.`id` AS `accountid`, `accounts`.`name`, `accounts`.`email` FROM `sessions` LEFT OUTER JOIN `accounts` ON `sessions`.`account` = `accounts`.`id` WHERE `accounts`.`sessionspublic` = 1 AND `sessions`.`length` > 110 ORDER BY `sessions`.`completed` DESC LIMIT 50")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch recent sessions: %s", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
rs, err := d.scanRecentSession(rows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan recent session: %s", err)
|
|
}
|
|
|
|
sessions = append(sessions, rs)
|
|
}
|
|
|
|
return sessions, nil
|
|
}
|
|
|
|
func (d *Database) deleteSession(started int, accountID int) (bool, error) {
|
|
r, err := d.db.Exec("DELETE FROM sessions WHERE `account`=? AND `started`=?", accountID, started)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to delete session: %s", err)
|
|
}
|
|
affected, err := r.RowsAffected()
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to fetch number of deleted sessions: %s", err)
|
|
}
|
|
|
|
return affected > 0, nil
|
|
}
|