2018-01-25 04:57:33 +00:00
|
|
|
package common
|
|
|
|
|
2018-01-26 05:53:34 +00:00
|
|
|
import (
|
|
|
|
"database/sql"
|
|
|
|
"encoding/json"
|
2018-02-15 13:15:27 +00:00
|
|
|
"errors"
|
|
|
|
"log"
|
|
|
|
"strconv"
|
2018-01-26 05:53:34 +00:00
|
|
|
|
2018-10-27 03:21:02 +00:00
|
|
|
"github.com/Azareal/Gosora/query_gen"
|
2018-01-26 05:53:34 +00:00
|
|
|
)
|
2018-01-25 04:57:33 +00:00
|
|
|
|
|
|
|
var Polls PollStore
|
|
|
|
|
|
|
|
type Poll struct {
|
2018-01-27 07:30:44 +00:00
|
|
|
ID int
|
|
|
|
ParentID int
|
|
|
|
ParentTable string
|
|
|
|
Type int // 0: Single choice, 1: Multiple choice, 2: Multiple choice w/ points
|
2018-01-25 04:57:33 +00:00
|
|
|
//AntiCheat bool // Apply various mitigations for cheating
|
|
|
|
// GroupPower map[gid]points // The number of points a group can spend in this poll, defaults to 1
|
|
|
|
|
2018-01-26 05:53:34 +00:00
|
|
|
Options map[int]string
|
|
|
|
Results map[int]int // map[optionIndex]points
|
|
|
|
QuickOptions []PollOption // TODO: Fix up the template transpiler so we don't need to use this hack anymore
|
|
|
|
VoteCount int
|
|
|
|
}
|
|
|
|
|
2018-01-27 07:30:44 +00:00
|
|
|
func (poll *Poll) CastVote(optionIndex int, uid int, ipaddress string) error {
|
|
|
|
return Polls.CastVote(optionIndex, poll.ID, uid, ipaddress) // TODO: Move the query into a pollStmts rather than having it in the store
|
|
|
|
}
|
|
|
|
|
2018-01-26 05:53:34 +00:00
|
|
|
func (poll *Poll) Copy() Poll {
|
|
|
|
return *poll
|
|
|
|
}
|
|
|
|
|
|
|
|
type PollOption struct {
|
|
|
|
ID int
|
|
|
|
Value string
|
2018-01-25 04:57:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type Pollable interface {
|
2018-01-27 07:30:44 +00:00
|
|
|
GetID() int
|
|
|
|
GetTable() string
|
2018-01-25 04:57:33 +00:00
|
|
|
SetPoll(pollID int) error
|
|
|
|
}
|
|
|
|
|
|
|
|
type PollStore interface {
|
|
|
|
Get(id int) (*Poll, error)
|
|
|
|
Exists(id int) bool
|
2018-01-26 05:53:34 +00:00
|
|
|
Create(parent Pollable, pollType int, pollOptions map[int]string) (int, error)
|
2018-01-27 07:30:44 +00:00
|
|
|
CastVote(optionIndex int, pollID int, uid int, ipaddress string) error
|
2018-01-25 04:57:33 +00:00
|
|
|
Reload(id int) error
|
2019-06-01 12:31:48 +00:00
|
|
|
//Count() int
|
2018-01-25 04:57:33 +00:00
|
|
|
|
|
|
|
SetCache(cache PollCache)
|
|
|
|
GetCache() PollCache
|
|
|
|
}
|
|
|
|
|
|
|
|
type DefaultPollStore struct {
|
|
|
|
cache PollCache
|
|
|
|
|
2018-01-28 14:30:24 +00:00
|
|
|
get *sql.Stmt
|
|
|
|
exists *sql.Stmt
|
|
|
|
createPoll *sql.Stmt
|
|
|
|
createPollOption *sql.Stmt
|
|
|
|
addVote *sql.Stmt
|
|
|
|
incrementVoteCount *sql.Stmt
|
|
|
|
incrementVoteCountForOption *sql.Stmt
|
|
|
|
delete *sql.Stmt
|
2019-06-01 12:31:48 +00:00
|
|
|
//count *sql.Stmt
|
2018-01-25 04:57:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewDefaultPollStore(cache PollCache) (*DefaultPollStore, error) {
|
2018-08-04 11:46:36 +00:00
|
|
|
acc := qgen.NewAcc()
|
2018-01-25 04:57:33 +00:00
|
|
|
if cache == nil {
|
|
|
|
cache = NewNullPollCache()
|
|
|
|
}
|
|
|
|
// TODO: Add an admin version of registerStmt with more flexibility?
|
|
|
|
return &DefaultPollStore{
|
2018-01-28 14:30:24 +00:00
|
|
|
cache: cache,
|
|
|
|
get: acc.Select("polls").Columns("parentID, parentTable, type, options, votes").Where("pollID = ?").Prepare(),
|
|
|
|
exists: acc.Select("polls").Columns("pollID").Where("pollID = ?").Prepare(),
|
|
|
|
createPoll: acc.Insert("polls").Columns("parentID, parentTable, type, options").Fields("?,?,?,?").Prepare(),
|
|
|
|
createPollOption: acc.Insert("polls_options").Columns("pollID, option, votes").Fields("?,?,0").Prepare(),
|
|
|
|
addVote: acc.Insert("polls_votes").Columns("pollID, uid, option, castAt, ipaddress").Fields("?,?,?,UTC_TIMESTAMP(),?").Prepare(),
|
|
|
|
incrementVoteCount: acc.Update("polls").Set("votes = votes + 1").Where("pollID = ?").Prepare(),
|
|
|
|
incrementVoteCountForOption: acc.Update("polls_options").Set("votes = votes + 1").Where("option = ? AND pollID = ?").Prepare(),
|
2019-06-01 12:31:48 +00:00
|
|
|
//count: acc.SimpleCount("polls", "", ""),
|
2018-01-25 04:57:33 +00:00
|
|
|
}, acc.FirstError()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (store *DefaultPollStore) Exists(id int) bool {
|
|
|
|
err := store.exists.QueryRow(id).Scan(&id)
|
|
|
|
if err != nil && err != ErrNoRows {
|
|
|
|
LogError(err)
|
|
|
|
}
|
|
|
|
return err != ErrNoRows
|
|
|
|
}
|
|
|
|
|
|
|
|
func (store *DefaultPollStore) Get(id int) (*Poll, error) {
|
|
|
|
poll, err := store.cache.Get(id)
|
|
|
|
if err == nil {
|
|
|
|
return poll, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
poll = &Poll{ID: id}
|
|
|
|
var optionTxt []byte
|
2018-01-27 07:30:44 +00:00
|
|
|
err = store.get.QueryRow(id).Scan(&poll.ParentID, &poll.ParentTable, &poll.Type, &optionTxt, &poll.VoteCount)
|
2018-01-26 05:53:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = json.Unmarshal(optionTxt, &poll.Options)
|
2018-01-25 04:57:33 +00:00
|
|
|
if err == nil {
|
2018-01-26 05:53:34 +00:00
|
|
|
poll.QuickOptions = store.unpackOptionsMap(poll.Options)
|
2018-01-25 04:57:33 +00:00
|
|
|
store.cache.Set(poll)
|
|
|
|
}
|
|
|
|
return poll, err
|
|
|
|
}
|
|
|
|
|
2018-02-15 13:15:27 +00:00
|
|
|
// TODO: Optimise the query to avoid preparing it on the spot? Maybe, use knowledge of the most common IN() parameter counts?
|
|
|
|
// TODO: ID of 0 should always error?
|
|
|
|
func (store *DefaultPollStore) BulkGetMap(ids []int) (list map[int]*Poll, err error) {
|
|
|
|
var idCount = len(ids)
|
|
|
|
list = make(map[int]*Poll)
|
|
|
|
if idCount == 0 {
|
|
|
|
return list, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var stillHere []int
|
|
|
|
sliceList := store.cache.BulkGet(ids)
|
|
|
|
for i, sliceItem := range sliceList {
|
|
|
|
if sliceItem != nil {
|
|
|
|
list[sliceItem.ID] = sliceItem
|
|
|
|
} else {
|
|
|
|
stillHere = append(stillHere, ids[i])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ids = stillHere
|
|
|
|
|
|
|
|
// If every user is in the cache, then return immediately
|
|
|
|
if len(ids) == 0 {
|
|
|
|
return list, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Add a function for the qlist stuff
|
|
|
|
var qlist string
|
|
|
|
var pollIDList []interface{}
|
|
|
|
for _, id := range ids {
|
|
|
|
pollIDList = append(pollIDList, strconv.Itoa(id))
|
|
|
|
qlist += "?,"
|
|
|
|
}
|
|
|
|
qlist = qlist[0 : len(qlist)-1]
|
|
|
|
|
2018-08-04 11:46:36 +00:00
|
|
|
rows, err := qgen.NewAcc().Select("polls").Columns("pollID, parentID, parentTable, type, options, votes").Where("pollID IN(" + qlist + ")").Query(pollIDList...)
|
2018-02-15 13:15:27 +00:00
|
|
|
if err != nil {
|
|
|
|
return list, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
poll := &Poll{ID: 0}
|
|
|
|
var optionTxt []byte
|
|
|
|
err := rows.Scan(&poll.ID, &poll.ParentID, &poll.ParentTable, &poll.Type, &optionTxt, &poll.VoteCount)
|
|
|
|
if err != nil {
|
|
|
|
return list, err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = json.Unmarshal(optionTxt, &poll.Options)
|
|
|
|
if err != nil {
|
|
|
|
return list, err
|
|
|
|
}
|
|
|
|
poll.QuickOptions = store.unpackOptionsMap(poll.Options)
|
|
|
|
store.cache.Set(poll)
|
|
|
|
|
|
|
|
list[poll.ID] = poll
|
|
|
|
}
|
|
|
|
|
|
|
|
// Did we miss any polls?
|
|
|
|
if idCount > len(list) {
|
|
|
|
var sidList string
|
|
|
|
for _, id := range ids {
|
|
|
|
_, ok := list[id]
|
|
|
|
if !ok {
|
|
|
|
sidList += strconv.Itoa(id) + ","
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We probably don't need this, but it might be useful in case of bugs in BulkCascadeGetMap
|
|
|
|
if sidList == "" {
|
2018-02-19 04:26:01 +00:00
|
|
|
// TODO: Bulk log this
|
2018-02-15 13:15:27 +00:00
|
|
|
if Dev.DebugMode {
|
|
|
|
log.Print("This data is sampled later in the BulkCascadeGetMap function, so it might miss the cached IDs")
|
|
|
|
log.Print("idCount", idCount)
|
|
|
|
log.Print("ids", ids)
|
|
|
|
log.Print("list", list)
|
|
|
|
}
|
|
|
|
return list, errors.New("We weren't able to find a poll, but we don't know which one")
|
|
|
|
}
|
|
|
|
sidList = sidList[0 : len(sidList)-1]
|
|
|
|
|
|
|
|
err = errors.New("Unable to find the polls with the following IDs: " + sidList)
|
|
|
|
}
|
|
|
|
|
|
|
|
return list, err
|
|
|
|
}
|
|
|
|
|
2018-01-25 04:57:33 +00:00
|
|
|
func (store *DefaultPollStore) Reload(id int) error {
|
|
|
|
poll := &Poll{ID: id}
|
|
|
|
var optionTxt []byte
|
2018-01-27 07:30:44 +00:00
|
|
|
err := store.get.QueryRow(id).Scan(&poll.ParentID, &poll.ParentTable, &poll.Type, &optionTxt, &poll.VoteCount)
|
2018-01-25 04:57:33 +00:00
|
|
|
if err != nil {
|
|
|
|
store.cache.Remove(id)
|
|
|
|
return err
|
|
|
|
}
|
2018-01-26 05:53:34 +00:00
|
|
|
|
|
|
|
err = json.Unmarshal(optionTxt, &poll.Options)
|
|
|
|
if err != nil {
|
|
|
|
store.cache.Remove(id)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
poll.QuickOptions = store.unpackOptionsMap(poll.Options)
|
2018-01-25 04:57:33 +00:00
|
|
|
_ = store.cache.Set(poll)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-01-26 05:53:34 +00:00
|
|
|
func (store *DefaultPollStore) unpackOptionsMap(rawOptions map[int]string) []PollOption {
|
|
|
|
options := make([]PollOption, len(rawOptions))
|
|
|
|
for id, option := range rawOptions {
|
|
|
|
options[id] = PollOption{id, option}
|
|
|
|
}
|
|
|
|
return options
|
|
|
|
}
|
|
|
|
|
2018-01-27 07:30:44 +00:00
|
|
|
// TODO: Use a transaction for this?
|
|
|
|
func (store *DefaultPollStore) CastVote(optionIndex int, pollID int, uid int, ipaddress string) error {
|
|
|
|
_, err := store.addVote.Exec(pollID, uid, optionIndex, ipaddress)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
_, err = store.incrementVoteCount.Exec(pollID)
|
2018-01-28 14:30:24 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
_, err = store.incrementVoteCountForOption.Exec(optionIndex, pollID)
|
2018-01-27 07:30:44 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-01-28 14:30:24 +00:00
|
|
|
// TODO: Use a transaction for this
|
2018-01-26 05:53:34 +00:00
|
|
|
func (store *DefaultPollStore) Create(parent Pollable, pollType int, pollOptions map[int]string) (id int, err error) {
|
|
|
|
pollOptionsTxt, err := json.Marshal(pollOptions)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
2018-01-28 14:30:24 +00:00
|
|
|
res, err := store.createPoll.Exec(parent.GetID(), parent.GetTable(), pollType, pollOptionsTxt)
|
2018-01-25 04:57:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
lastID, err := res.LastInsertId()
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
2018-01-28 14:30:24 +00:00
|
|
|
|
|
|
|
for i := 0; i < len(pollOptions); i++ {
|
|
|
|
_, err := store.createPollOption.Exec(lastID, i)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return int(lastID), parent.SetPoll(int(lastID)) // TODO: Delete the poll (and options) if SetPoll fails
|
2018-01-25 04:57:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (store *DefaultPollStore) SetCache(cache PollCache) {
|
|
|
|
store.cache = cache
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: We're temporarily doing this so that you can do ucache != nil in getTopicUser. Refactor it.
|
|
|
|
func (store *DefaultPollStore) GetCache() PollCache {
|
|
|
|
_, ok := store.cache.(*NullPollCache)
|
|
|
|
if ok {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return store.cache
|
|
|
|
}
|