Cascade delete attachments properly.

Cascade delete replied to topic events for replies properly.
Cascade delete likes on topic posts properly.
Cascade delete replies and their children properly.
Recalculate user stats properly when items are deleted.
Users can now unlike topic opening posts.
Add a recalculator to fix abnormalities across upgrades.
Try fixing a last_ip daily update bug.

Add Existable interface.
Add Delete method to LikeStore.
Add Each, Exists, Create, CountUser, CountMegaUser and CountBigUser methods to ReplyStore.
Add CountUser, CountMegaUser, CountBigUser methods to TopicStore.
Add Each method to UserStore.
Add Add, Delete and DeleteResource methods to SubscriptionStore.
Add Delete, DeleteByParams, DeleteByParamsExtra and AidsByParamsExtra methods to ActivityStream.
Add Exists method to ProfileReplyStore.

Add DropColumn, RenameColumn and ChangeColumn to the database adapters.

Shorten ipaddress column names to ip.
- topics table.
- replies table
- users_replies table.
- polls_votes table.
Add extra column to activity_stream table.
Fix an issue upgrading sites to MariaDB 10.3 from older versions of Gosora. Please report any other issues you find.

You need to run the updater / patcher for this commit.
This commit is contained in:
Azareal 2020-01-31 17:22:08 +10:00
parent e1702687f7
commit 6935637867
75 changed files with 1488 additions and 600 deletions

View File

@ -197,7 +197,7 @@ func setupData(client *elastic.Client) error {
} }
oi := 0 oi := 0
err := qgen.NewAcc().Select("topics").Cols("tid, title, content, createdBy, ipaddress").Each(func(rows *sql.Rows) error { err := qgen.NewAcc().Select("topics").Cols("tid,title,content,createdBy,ip").Each(func(rows *sql.Rows) error {
t := ESTopic{} t := ESTopic{}
err := rows.Scan(&t.ID, &t.Title, &t.Content, &t.CreatedBy, &t.IP) err := rows.Scan(&t.ID, &t.Title, &t.Content, &t.CreatedBy, &t.IP)
if err != nil { if err != nil {
@ -233,7 +233,7 @@ func setupData(client *elastic.Client) error {
rf(rin[i]) rf(rin[i])
} }
oi := 0 oi := 0
err := qgen.NewAcc().Select("replies").Cols("rid, tid, content, createdBy, ipaddress").Each(func(rows *sql.Rows) error { err := qgen.NewAcc().Select("replies").Cols("rid,tid,content,createdBy,ip").Each(func(rows *sql.Rows) error {
r := ESReply{} r := ESReply{}
err := rows.Scan(&r.ID, &r.TID, &r.Content, &r.CreatedBy, &r.IP) err := rows.Scan(&r.ID, &r.TID, &r.Content, &r.CreatedBy, &r.IP)
if err != nil { if err != nil {

View File

@ -204,9 +204,9 @@ func seedTables(a qgen.Adapter) error {
// //
qgen.Install.SimpleInsert("topics", "title, content, parsed_content, createdAt, lastReplyAt, lastReplyBy, createdBy, parentID, ipaddress", "'Test Topic','A topic automatically generated by the software.','A topic automatically generated by the software.',UTC_TIMESTAMP(),UTC_TIMESTAMP(),1,1,2,'::1'") qgen.Install.SimpleInsert("topics", "title, content, parsed_content, createdAt, lastReplyAt, lastReplyBy, createdBy, parentID, ip", "'Test Topic','A topic automatically generated by the software.','A topic automatically generated by the software.',UTC_TIMESTAMP(),UTC_TIMESTAMP(),1,1,2,'::1'")
qgen.Install.SimpleInsert("replies", "tid, content, parsed_content, createdAt, createdBy, lastUpdated, lastEdit, lastEditBy, ipaddress", "1,'A reply!','A reply!',UTC_TIMESTAMP(),1,UTC_TIMESTAMP(),0,0,'::1'") qgen.Install.SimpleInsert("replies", "tid, content, parsed_content, createdAt, createdBy, lastUpdated, lastEdit, lastEditBy, ip", "1,'A reply!','A reply!',UTC_TIMESTAMP(),1,UTC_TIMESTAMP(),0,0,'::1'")
qgen.Install.SimpleInsert("menus", "", "") qgen.Install.SimpleInsert("menus", "", "")

View File

@ -39,7 +39,8 @@ func createTables(adapter qgen.Adapter) (err error) {
// TODO: Drop these columns? // TODO: Drop these columns?
tC{"url_prefix", "varchar", 20, false, false, "''"}, tC{"url_prefix", "varchar", 20, false, false, "''"},
tC{"url_name", "varchar", 100, false, false, "''"}, tC{"url_name", "varchar", 100, false, false, "''"},
//tC{"pub_key", "text", 0, false, false, "''"},
tC{"level", "smallint", 0, false, false, "0"}, tC{"level", "smallint", 0, false, false, "0"},
tC{"score", "int", 0, false, false, "0"}, tC{"score", "int", 0, false, false, "0"},
tC{"posts", "int", 0, false, false, "0"}, tC{"posts", "int", 0, false, false, "0"},
@ -253,7 +254,7 @@ func createTables(adapter qgen.Adapter) (err error) {
tC{"sticky", "boolean", 0, false, false, "0"}, tC{"sticky", "boolean", 0, false, false, "0"},
// TODO: Add an index for this // TODO: Add an index for this
tC{"parentID", "int", 0, false, false, "2"}, tC{"parentID", "int", 0, false, false, "2"},
tC{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, tC{"ip", "varchar", 200, false, false, "''"},
tC{"postCount", "int", 0, false, false, "1"}, tC{"postCount", "int", 0, false, false, "1"},
tC{"likeCount", "int", 0, false, false, "0"}, tC{"likeCount", "int", 0, false, false, "0"},
tC{"attachCount", "int", 0, false, false, "0"}, tC{"attachCount", "int", 0, false, false, "0"},
@ -286,7 +287,7 @@ func createTables(adapter qgen.Adapter) (err error) {
tC{"lastEdit", "int", 0, false, false, "0"}, tC{"lastEdit", "int", 0, false, false, "0"},
tC{"lastEditBy", "int", 0, false, false, "0"}, tC{"lastEditBy", "int", 0, false, false, "0"},
tC{"lastUpdated", "datetime", 0, false, false, ""}, tC{"lastUpdated", "datetime", 0, false, false, ""},
tC{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, tC{"ip", "varchar", 200, false, false, "''"},
tC{"likeCount", "int", 0, false, false, "0"}, tC{"likeCount", "int", 0, false, false, "0"},
tC{"attachCount", "int", 0, false, false, "0"}, tC{"attachCount", "int", 0, false, false, "0"},
tC{"words", "int", 0, false, false, "1"}, // ? - replies has a default of 1 and topics has 0? why? tC{"words", "int", 0, false, false, "1"}, // ? - replies has a default of 1 and topics has 0? why?
@ -357,7 +358,7 @@ func createTables(adapter qgen.Adapter) (err error) {
tC{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key tC{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key
tC{"option", "int", 0, false, false, "0"}, tC{"option", "int", 0, false, false, "0"},
tC{"castAt", "createdAt", 0, false, false, ""}, tC{"castAt", "createdAt", 0, false, false, ""},
tC{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, tC{"ip", "varchar", 200, false, false, "''"},
}, nil, }, nil,
) )
@ -371,7 +372,7 @@ func createTables(adapter qgen.Adapter) (err error) {
tC{"createdBy", "int", 0, false, false, ""}, // TODO: Make this a foreign key tC{"createdBy", "int", 0, false, false, ""}, // TODO: Make this a foreign key
tC{"lastEdit", "int", 0, false, false, "0"}, tC{"lastEdit", "int", 0, false, false, "0"},
tC{"lastEditBy", "int", 0, false, false, "0"}, tC{"lastEditBy", "int", 0, false, false, "0"},
tC{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, tC{"ip", "varchar", 200, false, false, "''"},
}, },
[]tblKey{ []tblKey{
tblKey{"rid", "primary", "", false}, tblKey{"rid", "primary", "", false},
@ -464,6 +465,7 @@ func createTables(adapter qgen.Adapter) (err error) {
tC{"elementType", "varchar", 50, false, false, ""}, /* topic, post (calling it post here to differentiate it from the 'reply' event), forum, user */ tC{"elementType", "varchar", 50, false, false, ""}, /* topic, post (calling it post here to differentiate it from the 'reply' event), forum, user */
tC{"elementID", "int", 0, false, false, ""}, /* the ID of the element being acted upon */ tC{"elementID", "int", 0, false, false, ""}, /* the ID of the element being acted upon */
tC{"createdAt", "createdAt", 0, false, false, ""}, tC{"createdAt", "createdAt", 0, false, false, ""},
tC{"extra", "varchar", 200, false, false, "''"},
}, },
[]tblKey{ []tblKey{
tblKey{"asid", "primary", "", false}, tblKey{"asid", "primary", "", false},

View File

@ -1,47 +1,92 @@
package common package common
import "database/sql" import (
import "github.com/Azareal/Gosora/query_gen" "database/sql"
qgen "github.com/Azareal/Gosora/query_gen"
)
var Activity ActivityStream var Activity ActivityStream
type ActivityStream interface { type ActivityStream interface {
Add(a Alert) (int, error) Add(a Alert) (int, error)
Get(id int) (Alert, error) Get(id int) (Alert, error)
Delete(id int) error
DeleteByParams(event string, targetID int, targetType string) error
DeleteByParamsExtra(event string, targetID int, targetType, extra string) error
AidsByParamsExtra(event string, elementID int, elementType, extra string) (aids []int, err error)
Count() (count int) Count() (count int)
} }
type DefaultActivityStream struct { type DefaultActivityStream struct {
add *sql.Stmt add *sql.Stmt
get *sql.Stmt get *sql.Stmt
count *sql.Stmt delete *sql.Stmt
deleteByParams *sql.Stmt
deleteByParamsExtra *sql.Stmt
aidsByParamsExtra *sql.Stmt
count *sql.Stmt
} }
func NewDefaultActivityStream(acc *qgen.Accumulator) (*DefaultActivityStream, error) { func NewDefaultActivityStream(acc *qgen.Accumulator) (*DefaultActivityStream, error) {
as := "activity_stream" as := "activity_stream"
return &DefaultActivityStream{ return &DefaultActivityStream{
add: acc.Insert(as).Columns("actor, targetUser, event, elementType, elementID, createdAt").Fields("?,?,?,?,?,UTC_TIMESTAMP()").Prepare(), add: acc.Insert(as).Columns("actor,targetUser,event,elementType,elementID,createdAt,extra").Fields("?,?,?,?,?,UTC_TIMESTAMP(),?").Prepare(),
get: acc.Select(as).Columns("actor, targetUser, event, elementType, elementID, createdAt").Where("asid = ?").Prepare(), get: acc.Select(as).Columns("actor,targetUser,event,elementType,elementID,createdAt,extra").Where("asid=?").Prepare(),
count: acc.Count(as).Prepare(), delete: acc.Delete(as).Where("asid=?").Prepare(),
deleteByParams: acc.Delete(as).Where("event=? AND elementID=? AND elementType=?").Prepare(),
deleteByParamsExtra: acc.Delete(as).Where("event=? AND elementID=? AND elementType=? AND extra=?").Prepare(),
aidsByParamsExtra: acc.Select(as).Columns("asid").Where("event=? AND elementID=? AND elementType=? AND extra=?").Prepare(),
count: acc.Count(as).Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }
func (s *DefaultActivityStream) Add(a Alert) (int, error) { func (s *DefaultActivityStream) Add(a Alert) (int, error) {
res, err := s.add.Exec(a.ActorID, a.TargetUserID, a.Event, a.ElementType, a.ElementID) res, err := s.add.Exec(a.ActorID, a.TargetUserID, a.Event, a.ElementType, a.ElementID, a.Extra)
if err != nil { if err != nil {
return 0, err return 0, err
} }
lastID, err := res.LastInsertId() lastID, err := res.LastInsertId()
return int(lastID), err return int(lastID), err
} }
func (s *DefaultActivityStream) Get(id int) (Alert, error) { func (s *DefaultActivityStream) Get(id int) (Alert, error) {
a := Alert{ASID: id} a := Alert{ASID: id}
err := s.get.QueryRow(id).Scan(&a.ActorID, &a.TargetUserID, &a.Event, &a.ElementType, &a.ElementID, &a.CreatedAt) err := s.get.QueryRow(id).Scan(&a.ActorID, &a.TargetUserID, &a.Event, &a.ElementType, &a.ElementID, &a.CreatedAt, &a.Extra)
return a, err return a, err
} }
func (s *DefaultActivityStream) Delete(id int) error {
_, err := s.delete.Exec(id)
return err
}
func (s *DefaultActivityStream) DeleteByParams(event string, elementID int, elementType string) error {
_, err := s.deleteByParams.Exec(event, elementID, elementType)
return err
}
func (s *DefaultActivityStream) DeleteByParamsExtra(event string, elementID int, elementType, extra string) error {
_, err := s.deleteByParamsExtra.Exec(event, elementID, elementType, extra)
return err
}
func (s *DefaultActivityStream) AidsByParamsExtra(event string, elementID int, elementType, extra string) (aids []int, err error) {
rows, err := s.aidsByParamsExtra.Query(event, elementID, elementType, extra)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var aid int
if err := rows.Scan(&aid); err != nil {
return nil, err
}
aids = append(aids, aid)
}
return aids, rows.Err()
}
// TODO: Write a test for this // TODO: Write a test for this
// Count returns the total number of activity stream items // Count returns the total number of activity stream items
func (s *DefaultActivityStream) Count() (count int) { func (s *DefaultActivityStream) Count() (count int) {
@ -50,4 +95,4 @@ func (s *DefaultActivityStream) Count() (count int) {
LogError(err) LogError(err)
} }
return count return count
} }

View File

@ -12,10 +12,11 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
//"fmt" //"fmt"
"github.com/Azareal/Gosora/common/phrases" "github.com/Azareal/Gosora/common/phrases"
"github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
type Alert struct { type Alert struct {
@ -25,15 +26,16 @@ type Alert struct {
Event string Event string
ElementType string ElementType string
ElementID int ElementID int
CreatedAt time.Time CreatedAt time.Time
Extra string
Actor *User Actor *User
} }
type AlertStmts struct { type AlertStmts struct {
notifyWatchers *sql.Stmt notifyWatchers *sql.Stmt
notifyOne *sql.Stmt notifyOne *sql.Stmt
getWatchers *sql.Stmt getWatchers *sql.Stmt
} }
var alertStmts AlertStmts var alertStmts AlertStmts
@ -45,10 +47,10 @@ func init() {
alertStmts = AlertStmts{ alertStmts = AlertStmts{
notifyWatchers: acc.SimpleInsertInnerJoin( notifyWatchers: acc.SimpleInsertInnerJoin(
qgen.DBInsert{"activity_stream_matches", "watcher,asid", ""}, qgen.DBInsert{"activity_stream_matches", "watcher,asid", ""},
qgen.DBJoin{"activity_stream", "activity_subscriptions", "activity_subscriptions.user, activity_stream.asid", "activity_subscriptions.targetType = activity_stream.elementType AND activity_subscriptions.targetID = activity_stream.elementID AND activity_subscriptions.user != activity_stream.actor", "asid = ?", "", ""}, qgen.DBJoin{"activity_stream", "activity_subscriptions", "activity_subscriptions.user, activity_stream.asid", "activity_subscriptions.targetType = activity_stream.elementType AND activity_subscriptions.targetID = activity_stream.elementID AND activity_subscriptions.user != activity_stream.actor", "asid=?", "", ""},
), ),
notifyOne: acc.Insert("activity_stream_matches").Columns("watcher,asid").Fields("?,?").Prepare(), notifyOne: acc.Insert("activity_stream_matches").Columns("watcher,asid").Fields("?,?").Prepare(),
getWatchers: acc.SimpleInnerJoin("activity_stream", "activity_subscriptions", "activity_subscriptions.user", "activity_subscriptions.targetType = activity_stream.elementType AND activity_subscriptions.targetID = activity_stream.elementID AND activity_subscriptions.user != activity_stream.actor", "asid = ?", "", ""), getWatchers: acc.SimpleInnerJoin("activity_stream", "activity_subscriptions", "activity_subscriptions.user", "activity_subscriptions.targetType = activity_stream.elementType AND activity_subscriptions.targetID = activity_stream.elementID AND activity_subscriptions.user != activity_stream.actor", "asid=?", "", ""),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -123,6 +125,7 @@ func BuildAlert(alert Alert, user User /* The current user */) (out string, err
case "post": case "post":
topic, err := TopicByReplyID(alert.ElementID) topic, err := TopicByReplyID(alert.ElementID)
if err != nil { if err != nil {
DebugLogf("Unable to find linked topic by reply ID %d", alert.ElementID)
return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic_by_reply")) return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic_by_reply"))
} }
url = topic.Link url = topic.Link
@ -146,49 +149,49 @@ func BuildAlert(alert Alert, user User /* The current user */) (out string, err
return buildAlertString(phraseName, []string{alert.Actor.Name, area}, url, alert.Actor.Avatar, alert.ASID), nil return buildAlertString(phraseName, []string{alert.Actor.Name, area}, url, alert.Actor.Avatar, alert.ASID), nil
} }
func buildAlertString(msg string, sub []string, path string, avatar string, asid int) string { func buildAlertString(msg string, sub []string, path, avatar string, asid int) string {
var substring string var subString string
for _, item := range sub { for _, item := range sub {
substring += "\"" + escapeTextInJson(item) + "\"," subString += "\"" + escapeTextInJson(item) + "\","
} }
if len(substring) > 0 { if len(subString) > 0 {
substring = substring[:len(substring)-1] subString = subString[:len(subString)-1]
} }
return `{"msg":"` + escapeTextInJson(msg) + `","sub":[` + substring + `],"path":"` + escapeTextInJson(path) + `","avatar":"` + escapeTextInJson(avatar) + `","id":` + strconv.Itoa(asid) + `}` return `{"msg":"` + escapeTextInJson(msg) + `","sub":[` + subString + `],"path":"` + escapeTextInJson(path) + `","avatar":"` + escapeTextInJson(avatar) + `","id":` + strconv.Itoa(asid) + `}`
} }
func AddActivityAndNotifyAll(actor int, targetUser int, event string, elementType string, elementID int) error { func AddActivityAndNotifyAll(a Alert) error {
id, err := Activity.Add(Alert{ActorID: actor, TargetUserID: targetUser, Event: event, ElementType: elementType, ElementID: elementID}) id, err := Activity.Add(a)
if err != nil { if err != nil {
return err return err
} }
return NotifyWatchers(id) return NotifyWatchers(id)
} }
func AddActivityAndNotifyTarget(alert Alert) error { func AddActivityAndNotifyTarget(a Alert) error {
id, err := Activity.Add(alert) id, err := Activity.Add(a)
if err != nil { if err != nil {
return err return err
} }
err = NotifyOne(alert.TargetUserID, id) err = NotifyOne(a.TargetUserID, id)
if err != nil { if err != nil {
return err return err
} }
alert.ASID = id a.ASID = id
// Live alerts, if the target is online and WebSockets is enabled // Live alerts, if the target is online and WebSockets is enabled
if EnableWebsockets { if EnableWebsockets {
go func() { go func() {
_ = WsHub.pushAlert(alert.TargetUserID, alert) _ = WsHub.pushAlert(a.TargetUserID, a)
//fmt.Println("err:",err) //fmt.Println("err:",err)
}() }()
} }
return nil return nil
} }
func NotifyOne(watcher int, asid int) error { func NotifyOne(watcher, asid int) error {
_, err := alertStmts.notifyOne.Exec(watcher, asid) _, err := alertStmts.notifyOne.Exec(watcher, asid)
return err return err
} }
@ -236,3 +239,7 @@ func notifyWatchers(asid int) {
} }
_ = WsHub.pushAlerts(uids, alert) _ = WsHub.pushAlerts(uids, alert)
} }
func DismissAlert(uid, aid int) {
_ = WsHub.PushMessage(uid, `{"event":"dismiss-alert","id":`+strconv.Itoa(aid)+`}`)
}

View File

@ -3,8 +3,8 @@ package common
import ( import (
"database/sql" "database/sql"
"errors" "errors"
"strings"
"os" "os"
"strings"
qgen "github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
@ -28,12 +28,12 @@ type AttachmentStore interface {
MiniGetList(originTable string, originID int) (alist []*MiniAttachment, err error) MiniGetList(originTable string, originID int) (alist []*MiniAttachment, err error)
BulkMiniGetList(originTable string, ids []int) (amap map[int][]*MiniAttachment, err error) BulkMiniGetList(originTable string, ids []int) (amap map[int][]*MiniAttachment, err error)
Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path, extra string) (int, error) Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path, extra string) (int, error)
MoveTo(sectionID int, originID int, originTable string) error MoveTo(sectionID, originID int, originTable string) error
MoveToByExtra(sectionID int, originTable, extra string) error MoveToByExtra(sectionID int, originTable, extra string) error
Count() int Count() int
CountIn(originTable string, oid int) int CountIn(originTable string, oid int) int
CountInPath(path string) int CountInPath(path string) int
Delete(aid int) error Delete(id int) error
} }
type DefaultAttachmentStore struct { type DefaultAttachmentStore struct {
@ -55,10 +55,10 @@ func NewDefaultAttachmentStore(acc *qgen.Accumulator) (*DefaultAttachmentStore,
getByObj: acc.Select(a).Columns("attachID, sectionID, uploadedBy, path, extra").Where("originTable = ? AND originID = ?").Prepare(), getByObj: acc.Select(a).Columns("attachID, sectionID, uploadedBy, path, extra").Where("originTable = ? AND originID = ?").Prepare(),
add: acc.Insert(a).Columns("sectionID, sectionTable, originID, originTable, uploadedBy, path, extra").Fields("?,?,?,?,?,?,?").Prepare(), add: acc.Insert(a).Columns("sectionID, sectionTable, originID, originTable, uploadedBy, path, extra").Fields("?,?,?,?,?,?,?").Prepare(),
count: acc.Count(a).Prepare(), count: acc.Count(a).Prepare(),
countIn: acc.Count(a).Where("originTable = ? and originID = ?").Prepare(), countIn: acc.Count(a).Where("originTable=? and originID=?").Prepare(),
countInPath: acc.Count(a).Where("path = ?").Prepare(), countInPath: acc.Count(a).Where("path = ?").Prepare(),
move: acc.Update(a).Set("sectionID = ?").Where("originID = ? AND originTable = ?").Prepare(), move: acc.Update(a).Set("sectionID=?").Where("originID=? AND originTable=?").Prepare(),
moveByExtra: acc.Update(a).Set("sectionID = ?").Where("originTable = ? AND extra = ?").Prepare(), moveByExtra: acc.Update(a).Set("sectionID=?").Where("originTable=? AND extra=?").Prepare(),
delete: acc.Delete(a).Where("attachID=?").Prepare(), delete: acc.Delete(a).Where("attachID=?").Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }
@ -151,7 +151,7 @@ func (s *DefaultAttachmentStore) Add(sectionID int, sectionTable string, originI
return int(lid), err return int(lid), err
} }
func (s *DefaultAttachmentStore) MoveTo(sectionID int, originID int, originTable string) error { func (s *DefaultAttachmentStore) MoveTo(sectionID, originID int, originTable string) error {
_, err := s.move.Exec(sectionID, originID, originTable) _, err := s.move.Exec(sectionID, originID, originTable)
return err return err
} }
@ -185,8 +185,8 @@ func (s *DefaultAttachmentStore) CountInPath(path string) (count int) {
return count return count
} }
func (s *DefaultAttachmentStore) Delete(aid int) error { func (s *DefaultAttachmentStore) Delete(id int) error {
_, err := s.delete.Exec(aid) _, err := s.delete.Exec(id)
return err return err
} }
@ -208,6 +208,6 @@ func DeleteAttachment(aid int) error {
return err return err
} }
} }
return nil return nil
} }

View File

@ -21,8 +21,8 @@ type LogItem struct {
} }
type LogStore interface { type LogStore interface {
Create(action string, elementID int, elementType string, ip string, actorID int) (err error) Create(action string, elementID int, elementType, ip string, actorID int) (err error)
CreateExtra(action string, elementID int, elementType string, ip string, actorID int, extra string) (err error) CreateExtra(action string, elementID int, elementType, ip string, actorID int, extra string) (err error)
Count() int Count() int
GetOffset(offset, perPage int) (logs []LogItem, err error) GetOffset(offset, perPage int) (logs []LogItem, err error)
} }
@ -43,11 +43,11 @@ func NewModLogStore(acc *qgen.Accumulator) (*SQLModLogStore, error) {
} }
// TODO: Make a store for this? // TODO: Make a store for this?
func (s *SQLModLogStore) Create(action string, elementID int, elementType string, ip string, actorID int) (err error) { func (s *SQLModLogStore) Create(action string, elementID int, elementType, ip string, actorID int) (err error) {
return s.CreateExtra(action, elementID, elementType, ip, actorID, "") return s.CreateExtra(action, elementID, elementType, ip, actorID, "")
} }
func (s *SQLModLogStore) CreateExtra(action string, elementID int, elementType string, ip string, actorID int, extra string) (err error) { func (s *SQLModLogStore) CreateExtra(action string, elementID int, elementType, ip string, actorID int, extra string) (err error) {
_, err = s.create.Exec(action, elementID, elementType, ip, actorID, extra) _, err = s.create.Exec(action, elementID, elementType, ip, actorID, extra)
return err return err
} }
@ -99,11 +99,11 @@ func NewAdminLogStore(acc *qgen.Accumulator) (*SQLAdminLogStore, error) {
} }
// TODO: Make a store for this? // TODO: Make a store for this?
func (s *SQLAdminLogStore) Create(action string, elementID int, elementType string, ip string, actorID int) (err error) { func (s *SQLAdminLogStore) Create(action string, elementID int, elementType, ip string, actorID int) (err error) {
return s.CreateExtra(action, elementID, elementType, ip, actorID, "") return s.CreateExtra(action, elementID, elementType, ip, actorID, "")
} }
func (s *SQLAdminLogStore) CreateExtra(action string, elementID int, elementType string, ip string, actorID int, extra string) (err error) { func (s *SQLAdminLogStore) CreateExtra(action string, elementID int, elementType, ip string, actorID int, extra string) (err error) {
_, err = s.create.Exec(action, elementID, elementType, ip, actorID, extra) _, err = s.create.Exec(action, elementID, elementType, ip, actorID, extra)
return err return err
} }

View File

@ -14,11 +14,14 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
meta "github.com/Azareal/Gosora/common/meta"
) )
var SoftwareVersion = Version{Major: 0, Minor: 3, Patch: 0, Tag: "dev"} var SoftwareVersion = Version{Major: 0, Minor: 3, Patch: 0, Tag: "dev"}
var Meta meta.MetaStore
// nolint I don't want to write comments for each of these o.o // nolint I don't want to write comments for each of these o.o
const Hour int = 60 * 60 const Hour int = 60 * 60
const Day int = Hour * 24 const Day int = Hour * 24
@ -57,7 +60,7 @@ type StringList []string
// TODO: Let admins manage this from the Control Panel // TODO: Let admins manage this from the Control Panel
// apng is commented out for now, as we have no way of re-encoding it into a smaller file // apng is commented out for now, as we have no way of re-encoding it into a smaller file
var AllowedFileExts = StringList{ var AllowedFileExts = StringList{
"png", "jpg", "jpe","jpeg","jif","jfi","jfif", "svg", "bmp", "gif", "tiff","tif", "webp", /*"apng",*/ // images "png", "jpg", "jpe", "jpeg", "jif", "jfi", "jfif", "svg", "bmp", "gif", "tiff", "tif", "webp", /*"apng",*/ // images
"txt", "xml", "json", "yaml", "toml", "ini", "md", "html", "rtf", "js", "py", "rb", "css", "scss", "less", "eqcss", "pcss", "java", "ts", "cs", "c", "cc", "cpp", "cxx", "C", "c++", "h", "hh", "hpp", "hxx", "h++", "rs", "rlib", "htaccess", "gitignore", /*"go","php",*/ // text "txt", "xml", "json", "yaml", "toml", "ini", "md", "html", "rtf", "js", "py", "rb", "css", "scss", "less", "eqcss", "pcss", "java", "ts", "cs", "c", "cc", "cpp", "cxx", "C", "c++", "h", "hh", "hpp", "hxx", "h++", "rs", "rlib", "htaccess", "gitignore", /*"go","php",*/ // text
@ -68,7 +71,7 @@ var AllowedFileExts = StringList{
"bz2", "zip", "zipx", "gz", "7z", "tar", "cab", "rar", "kgb", "pea", "xz", "zz", // archives "bz2", "zip", "zipx", "gz", "7z", "tar", "cab", "rar", "kgb", "pea", "xz", "zz", // archives
} }
var ImageFileExts = StringList{ var ImageFileExts = StringList{
"png", "jpg", "jpe","jpeg","jif","jfi","jfif", "svg", "bmp", "gif", "tiff","tif", "webp", /* "apng",*/ "png", "jpg", "jpe", "jpeg", "jif", "jfi", "jfif", "svg", "bmp", "gif", "tiff", "tif", "webp", /* "apng",*/
} }
var ArchiveFileExts = StringList{ var ArchiveFileExts = StringList{
"bz2", "zip", "zipx", "gz", "7z", "tar", "cab", "rar", "kgb", "pea", "xz", "zz", "bz2", "zip", "zipx", "gz", "7z", "tar", "cab", "rar", "kgb", "pea", "xz", "zz",
@ -152,3 +155,25 @@ func Log(args ...interface{}) {
func Logf(str string, args ...interface{}) { func Logf(str string, args ...interface{}) {
log.Printf(str, args...) log.Printf(str, args...)
} }
func Countf(stmt *sql.Stmt, args ...interface{}) (count int) {
err := stmt.QueryRow(args...).Scan(&count)
if err != nil {
LogError(err)
}
return count
}
func eachall(stmt *sql.Stmt, f func(r *sql.Rows) error) error {
rows, err := stmt.Query()
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
if err := f(rows); err != nil {
return err
}
}
return rows.Err()
}

View File

@ -32,17 +32,18 @@ type ConvoStmts struct {
func init() { func init() {
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
cpo := "conversations_posts"
convoStmts = ConvoStmts{ convoStmts = ConvoStmts{
fetchPost: acc.Select("conversations_posts").Columns("cid, body, post, createdBy").Where("pid = ?").Prepare(), fetchPost: acc.Select(cpo).Columns("cid,body,post,createdBy").Where("pid=?").Prepare(),
getPosts: acc.Select("conversations_posts").Columns("pid, body, post, createdBy").Where("cid = ?").Limit("?,?").Prepare(), getPosts: acc.Select(cpo).Columns("pid,body,post,createdBy").Where("cid=?").Limit("?,?").Prepare(),
countPosts: acc.Count("conversations_posts").Where("cid = ?").Prepare(), countPosts: acc.Count(cpo).Where("cid=?").Prepare(),
edit: acc.Update("conversations").Set("lastReplyBy = ?, lastReplyAt = ?").Where("cid = ?").Prepare(), edit: acc.Update("conversations").Set("lastReplyBy=?,lastReplyAt=?").Where("cid=?").Prepare(),
create: acc.Insert("conversations").Columns("createdAt, lastReplyAt").Fields("UTC_TIMESTAMP(),UTC_TIMESTAMP()").Prepare(), create: acc.Insert("conversations").Columns("createdAt,lastReplyAt").Fields("UTC_TIMESTAMP(),UTC_TIMESTAMP()").Prepare(),
has: acc.Count("conversations_participants").Where("uid = ? AND cid = ?").Prepare(), has: acc.Count("conversations_participants").Where("uid=? AND cid=?").Prepare(),
editPost: acc.Update("conversations_posts").Set("body = ?, post = ?").Where("pid = ?").Prepare(), editPost: acc.Update(cpo).Set("body=?,post=?").Where("pid=?").Prepare(),
createPost: acc.Insert("conversations_posts").Columns("cid, body, post, createdBy").Fields("?,?,?,?").Prepare(), createPost: acc.Insert(cpo).Columns("cid,body,post,createdBy").Fields("?,?,?,?").Prepare(),
deletePost: acc.Delete("conversations_posts").Where("pid = ?").Prepare(), deletePost: acc.Delete(cpo).Where("pid=?").Prepare(),
getUsers: acc.Select("conversations_participants").Columns("uid").Where("cid = ?").Prepare(), getUsers: acc.Select("conversations_participants").Columns("uid").Where("cid = ?").Prepare(),
} }
@ -138,7 +139,7 @@ type ConversationExtra struct {
type ConversationStore interface { type ConversationStore interface {
Get(id int) (*Conversation, error) Get(id int) (*Conversation, error)
GetUser(uid int, offset int) (cos []*Conversation, err error) GetUser(uid, offset int) (cos []*Conversation, err error)
GetUserExtra(uid int, offset int) (cos []*ConversationExtra, err error) GetUserExtra(uid int, offset int) (cos []*ConversationExtra, err error)
GetUserCount(uid int) (count int) GetUserCount(uid int) (count int)
Delete(id int) error Delete(id int) error
@ -160,12 +161,12 @@ type DefaultConversationStore struct {
func NewDefaultConversationStore(acc *qgen.Accumulator) (*DefaultConversationStore, error) { func NewDefaultConversationStore(acc *qgen.Accumulator) (*DefaultConversationStore, error) {
return &DefaultConversationStore{ return &DefaultConversationStore{
get: acc.Select("conversations").Columns("createdBy, createdAt, lastReplyBy, lastReplyAt").Where("cid = ?").Prepare(), get: acc.Select("conversations").Columns("createdBy, createdAt, lastReplyBy, lastReplyAt").Where("cid=?").Prepare(),
getUser: acc.SimpleInnerJoin("conversations_participants AS cp", "conversations AS c", "cp.cid, c.createdBy, c.createdAt, c.lastReplyBy, c.lastReplyAt", "cp.cid = c.cid", "cp.uid = ?", "c.lastReplyAt DESC, c.createdAt DESC, c.cid DESC", "?,?"), getUser: acc.SimpleInnerJoin("conversations_participants AS cp", "conversations AS c", "cp.cid, c.createdBy, c.createdAt, c.lastReplyBy, c.lastReplyAt", "cp.cid=c.cid", "cp.uid=?", "c.lastReplyAt DESC, c.createdAt DESC, c.cid DESC", "?,?"),
getUserCount: acc.Count("conversations_participants").Where("uid = ?").Prepare(), getUserCount: acc.Count("conversations_participants").Where("uid=?").Prepare(),
delete: acc.Delete("conversations").Where("cid = ?").Prepare(), delete: acc.Delete("conversations").Where("cid=?").Prepare(),
deletePosts: acc.Delete("conversations_posts").Where("cid = ?").Prepare(), deletePosts: acc.Delete("conversations_posts").Where("cid=?").Prepare(),
deleteParticipants: acc.Delete("conversations_participants").Where("cid = ?").Prepare(), deleteParticipants: acc.Delete("conversations_participants").Where("cid=?").Prepare(),
create: acc.Insert("conversations").Columns("createdBy, createdAt, lastReplyAt").Fields("?,UTC_TIMESTAMP(),UTC_TIMESTAMP()").Prepare(), create: acc.Insert("conversations").Columns("createdBy, createdAt, lastReplyAt").Fields("?,UTC_TIMESTAMP(),UTC_TIMESTAMP()").Prepare(),
addParticipant: acc.Insert("conversations_participants").Columns("uid, cid").Fields("?,?").Prepare(), addParticipant: acc.Insert("conversations_participants").Columns("uid, cid").Fields("?,?").Prepare(),
count: acc.Count("conversations").Prepare(), count: acc.Count("conversations").Prepare(),
@ -178,7 +179,7 @@ func (s *DefaultConversationStore) Get(id int) (*Conversation, error) {
return co, err return co, err
} }
func (s *DefaultConversationStore) GetUser(uid int, offset int) (cos []*Conversation, err error) { func (s *DefaultConversationStore) GetUser(uid, offset int) (cos []*Conversation, err error) {
rows, err := s.getUser.Query(uid, offset, Config.ItemsPerPage) rows, err := s.getUser.Query(uid, offset, Config.ItemsPerPage)
if err != nil { if err != nil {
return nil, err return nil, err
@ -197,7 +198,7 @@ func (s *DefaultConversationStore) GetUser(uid int, offset int) (cos []*Conversa
return cos, rows.Err() return cos, rows.Err()
} }
func (s *DefaultConversationStore) GetUserExtra(uid int, offset int) (cos []*ConversationExtra, err error) { func (s *DefaultConversationStore) GetUserExtra(uid, offset int) (cos []*ConversationExtra, err error) {
raw, err := s.GetUser(uid, offset) raw, err := s.GetUser(uid, offset)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -6,11 +6,11 @@ import (
"net/mail" "net/mail"
"net/smtp" "net/smtp"
"strings" "strings"
p "github.com/Azareal/Gosora/common/phrases" p "github.com/Azareal/Gosora/common/phrases"
) )
func SendActivationEmail(username string, email string, token string) error { func SendActivationEmail(username, email, token string) error {
schema := "http" schema := "http"
if Config.SslSchema { if Config.SslSchema {
schema += "s" schema += "s"
@ -21,30 +21,30 @@ func SendActivationEmail(username string, email string, token string) error {
return SendEmail(email, subject, msg) return SendEmail(email, subject, msg)
} }
func SendValidationEmail(username string, email string, token string) error { func SendValidationEmail(username, email, token string) error {
schema := "http" schema := "http"
if Config.SslSchema { if Config.SslSchema {
schema += "s" schema += "s"
} }
r := func(body *string) func(name, val string) { r := func(body *string) func(name, val string) {
return func(name, val string) { return func(name, val string) {
*body = strings.Replace(*body,"{{"+name+"}}",val,-1) *body = strings.Replace(*body, "{{"+name+"}}", val, -1)
} }
} }
subject := p.GetAccountPhrase("ValidateEmailSubject") subject := p.GetAccountPhrase("ValidateEmailSubject")
r1 := r(&subject) r1 := r(&subject)
r1("name",Site.Name) r1("name", Site.Name)
body := p.GetAccountPhrase("ValidateEmailBody") body := p.GetAccountPhrase("ValidateEmailBody")
r2 := r(&body) r2 := r(&body)
r2("username",username) r2("username", username)
r2("schema",schema) r2("schema", schema)
r2("url",Site.URL) r2("url", Site.URL)
r2("token",token) r2("token", token)
return SendEmail(email, subject, body) return SendEmail(email, subject, body)
} }
// TODO: Refactor this // TODO: Refactor this
func SendEmail(email string, subject string, msg string) (err error) { func SendEmail(email, subject, msg string) (err error) {
// This hook is useful for plugin_sendmail or for testing tools. Possibly to hook it into some sort of mail server? // This hook is useful for plugin_sendmail or for testing tools. Possibly to hook it into some sort of mail server?
ret, hasHook := GetHookTable().VhookNeedHook("email_send_intercept", email, subject, msg) ret, hasHook := GetHookTable().VhookNeedHook("email_send_intercept", email, subject, msg)
if hasHook { if hasHook {

View File

@ -18,28 +18,28 @@ type Email struct {
type EmailStore interface { type EmailStore interface {
// TODO: Add an autoincrement key // TODO: Add an autoincrement key
Get(user *User, email string) (Email, error) Get(u *User, email string) (Email, error)
GetEmailsByUser(user *User) (emails []Email, err error) GetEmailsByUser(u *User) (emails []Email, err error)
Add(uid int, email, token string) error Add(uid int, email, token string) error
Delete(uid int, email string) error Delete(uid int, email string) error
VerifyEmail(email string) error VerifyEmail(email string) error
} }
type DefaultEmailStore struct { type DefaultEmailStore struct {
get *sql.Stmt get *sql.Stmt
getEmailsByUser *sql.Stmt getEmailsByUser *sql.Stmt
add *sql.Stmt add *sql.Stmt
delete *sql.Stmt delete *sql.Stmt
verifyEmail *sql.Stmt verifyEmail *sql.Stmt
} }
func NewDefaultEmailStore(acc *qgen.Accumulator) (*DefaultEmailStore, error) { func NewDefaultEmailStore(acc *qgen.Accumulator) (*DefaultEmailStore, error) {
e := "emails" e := "emails"
return &DefaultEmailStore{ return &DefaultEmailStore{
get: acc.Select(e).Columns("email,validated,token").Where("uid=? AND email=?").Prepare(), get: acc.Select(e).Columns("email,validated,token").Where("uid=? AND email=?").Prepare(),
getEmailsByUser: acc.Select(e).Columns("email,validated,token").Where("uid=?").Prepare(), getEmailsByUser: acc.Select(e).Columns("email,validated,token").Where("uid=?").Prepare(),
add: acc.Insert(e).Columns("uid,email,validated,token").Fields("?,?,?,?").Prepare(), add: acc.Insert(e).Columns("uid,email,validated,token").Fields("?,?,?,?").Prepare(),
delete: acc.Delete(e).Where("uid=? AND email=?").Prepare(), delete: acc.Delete(e).Where("uid=? AND email=?").Prepare(),
// Need to fix this: Empty string isn't working, it gets set to 1 instead x.x -- Has this been fixed? // Need to fix this: Empty string isn't working, it gets set to 1 instead x.x -- Has this been fixed?
verifyEmail: acc.Update(e).Set("validated=1,token=''").Where("email=?").Prepare(), verifyEmail: acc.Update(e).Set("validated=1,token=''").Where("email=?").Prepare(),
@ -47,7 +47,7 @@ func NewDefaultEmailStore(acc *qgen.Accumulator) (*DefaultEmailStore, error) {
} }
func (s *DefaultEmailStore) Get(user *User, email string) (Email, error) { func (s *DefaultEmailStore) Get(user *User, email string) (Email, error) {
e := Email{UserID:user.ID, Primary:email !="" && user.Email==email} e := Email{UserID: user.ID, Primary: email != "" && user.Email == email}
err := s.get.QueryRow(user.ID, email).Scan(&e.Email, &e.Validated, &e.Token) err := s.get.QueryRow(user.ID, email).Scan(&e.Email, &e.Validated, &e.Token)
return e, err return e, err
} }
@ -72,13 +72,13 @@ func (s *DefaultEmailStore) GetEmailsByUser(user *User) (emails []Email, err err
return emails, rows.Err() return emails, rows.Err()
} }
func (s *DefaultEmailStore) Add(uid int, email string, token string) error { func (s *DefaultEmailStore) Add(uid int, email, token string) error {
_, err := s.add.Exec(uid, email, 0, token) _, err := s.add.Exec(uid, email, 0, token)
return err return err
} }
func (s *DefaultEmailStore) Delete(uid int, email string) error { func (s *DefaultEmailStore) Delete(uid int, email string) error {
_, err := s.delete.Exec(uid,email) _, err := s.delete.Exec(uid, email)
return err return err
} }

View File

@ -59,7 +59,7 @@ func (list SFileList) JSTmplInit() error {
tmplName := strings.TrimSuffix(path, ".jgo") tmplName := strings.TrimSuffix(path, ".jgo")
shortName := strings.TrimPrefix(tmplName, "template_") shortName := strings.TrimPrefix(tmplName, "template_")
replace := func(data []byte, replaceThis string, withThis string) []byte { replace := func(data []byte, replaceThis, withThis string) []byte {
return bytes.Replace(data, []byte(replaceThis), []byte(withThis), -1) return bytes.Replace(data, []byte(replaceThis), []byte(withThis), -1)
} }
@ -107,7 +107,7 @@ func (list SFileList) JSTmplInit() error {
}*/ }*/
// ? Can we just use a regex? I'm thinking of going more efficient, or just outright rolling wasm, this is a temp hack in a place where performance doesn't particularly matter // ? Can we just use a regex? I'm thinking of going more efficient, or just outright rolling wasm, this is a temp hack in a place where performance doesn't particularly matter
each := func(phrase string, handle func(index int)) { each := func(phrase string, h func(index int)) {
//fmt.Println("find each '" + phrase + "'") //fmt.Println("find each '" + phrase + "'")
index := endBrace index := endBrace
if index < 0 { if index < 0 {
@ -121,7 +121,7 @@ func (list SFileList) JSTmplInit() error {
if !foundIt { if !foundIt {
break break
} }
handle(index) h(index)
} }
} }
each("strconv.Itoa(", func(index int) { each("strconv.Itoa(", func(index int) {
@ -292,7 +292,7 @@ func (list SFileList) Init() error {
}) })
} }
func (list SFileList) Add(path string, prefix string) error { func (list SFileList) Add(path, prefix string) error {
data, err := ioutil.ReadFile(path) data, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
return err return err

View File

@ -7,7 +7,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
) )
@ -27,9 +27,9 @@ type Forum struct {
Link string Link string
Name string Name string
Desc string Desc string
Tmpl string Tmpl string
Active bool Active bool
Order int Order int
Preset string Preset string
ParentID int ParentID int
ParentType string ParentType string
@ -60,8 +60,8 @@ var forumStmts ForumStmts
func init() { func init() {
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
forumStmts = ForumStmts{ forumStmts = ForumStmts{
update: acc.Update("forums").Set("name = ?, desc = ?, active = ?, preset = ?").Where("fid = ?").Prepare(), update: acc.Update("forums").Set("name=?,desc=?,active=?,preset=?").Where("fid=?").Prepare(),
setPreset: acc.Update("forums").Set("preset = ?").Where("fid = ?").Prepare(), setPreset: acc.Update("forums").Set("preset=?").Where("fid=?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -74,7 +74,7 @@ func (f *Forum) Copy() (fcopy Forum) {
} }
// TODO: Write tests for this // TODO: Write tests for this
func (f *Forum) Update(name string, desc string, active bool, preset string) error { func (f *Forum) Update(name, desc string, active bool, preset string) error {
if name == "" { if name == "" {
name = f.Name name = f.Name
} }
@ -137,6 +137,7 @@ func (sf SortForum) Len() int {
func (sf SortForum) Swap(i, j int) { func (sf SortForum) Swap(i, j int) {
sf[i], sf[j] = sf[j], sf[i] sf[i], sf[j] = sf[j], sf[i]
} }
/*func (sf SortForum) Less(i,j int) bool { /*func (sf SortForum) Less(i,j int) bool {
l := sf.less(i,j) l := sf.less(i,j)
if l { if l {
@ -156,7 +157,7 @@ func (sf SortForum) Less(i, j int) bool {
} }
// ! Don't use this outside of tests and possibly template_init.go // ! Don't use this outside of tests and possibly template_init.go
func BlankForum(fid int, link string, name string, desc string, active bool, preset string, parentID int, parentType string, topicCount int) *Forum { func BlankForum(fid int, link, name, desc string, active bool, preset string, parentID int, parentType string, topicCount int) *Forum {
return &Forum{ID: fid, Link: link, Name: name, Desc: desc, Active: active, Preset: preset, ParentID: parentID, ParentType: parentType, TopicCount: topicCount} return &Forum{ID: fid, Link: link, Name: name, Desc: desc, Active: active, Preset: preset, ParentID: parentID, ParentType: parentType, TopicCount: topicCount}
} }

View File

@ -25,9 +25,9 @@ func NewDefaultIPSearcher() (*DefaultIPSearcher, error) {
uu := "users" uu := "users"
return &DefaultIPSearcher{ return &DefaultIPSearcher{
searchUsers: acc.Select(uu).Columns("uid").Where("last_ip=? OR last_ip LIKE CONCAT('%-',?)").Prepare(), searchUsers: acc.Select(uu).Columns("uid").Where("last_ip=? OR last_ip LIKE CONCAT('%-',?)").Prepare(),
searchTopics: acc.Select(uu).Columns("uid").InQ("uid", acc.Select("topics").Columns("createdBy").Where("ipaddress=?")).Prepare(), searchTopics: acc.Select(uu).Columns("uid").InQ("uid", acc.Select("topics").Columns("createdBy").Where("ip=?")).Prepare(),
searchReplies: acc.Select(uu).Columns("uid").InQ("uid", acc.Select("replies").Columns("createdBy").Where("ipaddress=?")).Prepare(), searchReplies: acc.Select(uu).Columns("uid").InQ("uid", acc.Select("replies").Columns("createdBy").Where("ip=?")).Prepare(),
searchUsersReplies: acc.Select(uu).Columns("uid").InQ("uid", acc.Select("users_replies").Columns("createdBy").Where("ipaddress=?")).Prepare(), searchUsersReplies: acc.Select(uu).Columns("uid").InQ("uid", acc.Select("users_replies").Columns("createdBy").Where("ip=?")).Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }

View File

@ -10,22 +10,25 @@ var Likes LikeStore
type LikeStore interface { type LikeStore interface {
BulkExists(ids []int, sentBy int, targetType string) ([]int, error) BulkExists(ids []int, sentBy int, targetType string) ([]int, error)
Delete(targetID int, targetType string) error
Count() (count int) Count() (count int)
} }
type DefaultLikeStore struct { type DefaultLikeStore struct {
count *sql.Stmt count *sql.Stmt
delete *sql.Stmt
} }
func NewDefaultLikeStore(acc *qgen.Accumulator) (*DefaultLikeStore, error) { func NewDefaultLikeStore(acc *qgen.Accumulator) (*DefaultLikeStore, error) {
return &DefaultLikeStore{ return &DefaultLikeStore{
count: acc.Count("likes").Prepare(), count: acc.Count("likes").Prepare(),
delete: acc.Delete("likes").Where("targetItem=? AND targetType=?").Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }
// TODO: Write a test for this // TODO: Write a test for this
func (s *DefaultLikeStore) BulkExists(ids []int, sentBy int, targetType string) (eids []int, err error) { func (s *DefaultLikeStore) BulkExists(ids []int, sentBy int, targetType string) (eids []int, err error) {
rows, err := qgen.NewAcc().Select("likes").Columns("targetItem").Where("sentBy = ? AND targetType = ?").In("targetItem", ids).Query(sentBy, targetType) rows, err := qgen.NewAcc().Select("likes").Columns("targetItem").Where("sentBy=? AND targetType=?").In("targetItem", ids).Query(sentBy, targetType)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, nil return nil, nil
} else if err != nil { } else if err != nil {
@ -43,6 +46,11 @@ func (s *DefaultLikeStore) BulkExists(ids []int, sentBy int, targetType string)
return eids, rows.Err() return eids, rows.Err()
} }
func (s *DefaultLikeStore) Delete(targetID int, targetType string) error {
_, err := s.delete.Exec(targetID, targetType)
return err
}
// TODO: Write a test for this // TODO: Write a test for this
// Count returns the total number of likes globally // Count returns the total number of likes globally
func (s *DefaultLikeStore) Count() (count int) { func (s *DefaultLikeStore) Count() (count int) {

View File

@ -5,7 +5,7 @@ import (
"strconv" "strconv"
"sync/atomic" "sync/atomic"
"github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
var Menus *DefaultMenuStore var Menus *DefaultMenuStore
@ -40,7 +40,7 @@ func (s *DefaultMenuStore) Get(mid int) (*MenuListHolder, error) {
} }
func (s *DefaultMenuStore) Items(mid int) (mlist MenuItemList, err error) { func (s *DefaultMenuStore) Items(mid int) (mlist MenuItemList, err error) {
err = qgen.NewAcc().Select("menu_items").Columns("miid,name,htmlID,cssClass,position,path,aria,tooltip,order,tmplName,guestOnly,memberOnly,staffOnly,adminOnly").Where("mid = " + strconv.Itoa(mid)).Orderby("order ASC").Each(func(rows *sql.Rows) error { err = qgen.NewAcc().Select("menu_items").Columns("miid,name,htmlID,cssClass,position,path,aria,tooltip,order,tmplName,guestOnly,memberOnly,staffOnly,adminOnly").Where("mid=" + strconv.Itoa(mid)).Orderby("order ASC").Each(func(rows *sql.Rows) error {
i := MenuItem{MenuID: mid} i := MenuItem{MenuID: mid}
err := rows.Scan(&i.ID, &i.Name, &i.HTMLID, &i.CSSClass, &i.Position, &i.Path, &i.Aria, &i.Tooltip, &i.Order, &i.TmplName, &i.GuestOnly, &i.MemberOnly, &i.SuperModOnly, &i.AdminOnly) err := rows.Scan(&i.ID, &i.Name, &i.HTMLID, &i.CSSClass, &i.Position, &i.Path, &i.Aria, &i.Tooltip, &i.Order, &i.TmplName, &i.GuestOnly, &i.MemberOnly, &i.SuperModOnly, &i.AdminOnly)
if err != nil { if err != nil {

View File

@ -1,14 +1,15 @@
package common package common
import "database/sql" import (
import "github.com/Azareal/Gosora/query_gen" "database/sql"
var Meta MetaStore qgen "github.com/Azareal/Gosora/query_gen"
)
// MetaStore is a simple key-value store for the system to stash things in when needed // MetaStore is a simple key-value store for the system to stash things in when needed
type MetaStore interface { type MetaStore interface {
Get(name string) (val string, err error) Get(name string) (val string, err error)
Set(name string, val string) error Set(name, val string) error
} }
type DefaultMetaStore struct { type DefaultMetaStore struct {
@ -32,7 +33,7 @@ func (s *DefaultMetaStore) Get(name string) (val string, err error) {
} }
// TODO: Use timestamped rows as a more robust method of ensuring data integrity // TODO: Use timestamped rows as a more robust method of ensuring data integrity
func (s *DefaultMetaStore) Set(name string, val string) error { func (s *DefaultMetaStore) Set(name, val string) error {
_, err := s.Get(name) _, err := s.Get(name)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
_, err := s.add.Exec(name) _, err := s.add.Exec(name)

View File

@ -31,7 +31,7 @@ func init() {
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
rl := "registration_logs" rl := "registration_logs"
regLogStmts = RegLogStmts{ regLogStmts = RegLogStmts{
update: acc.Update(rl).Set("username = ?, email = ?, failureReason = ?, success = ?").Where("rlid = ?").Prepare(), update: acc.Update(rl).Set("username=?, email=?, failureReason=?, success=?").Where("rlid=?").Prepare(),
create: acc.Insert(rl).Columns("username, email, failureReason, success, ipaddress, doneAt").Fields("?,?,?,?,?,UTC_TIMESTAMP()").Prepare(), create: acc.Insert(rl).Columns("username, email, failureReason, success, ipaddress, doneAt").Fields("?,?,?,?,?,UTC_TIMESTAMP()").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
@ -57,7 +57,7 @@ func (l *RegLogItem) Create() (id int, err error) {
type RegLogStore interface { type RegLogStore interface {
Count() (count int) Count() (count int)
GetOffset(offset int, perPage int) (logs []RegLogItem, err error) GetOffset(offset, perPage int) (logs []RegLogItem, err error)
} }
type SQLRegLogStore struct { type SQLRegLogStore struct {
@ -120,8 +120,8 @@ func init() {
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
ll := "login_logs" ll := "login_logs"
loginLogStmts = LoginLogStmts{ loginLogStmts = LoginLogStmts{
update: acc.Update(ll).Set("uid = ?, success = ?").Where("lid = ?").Prepare(), update: acc.Update(ll).Set("uid=?,success=?").Where("lid=?").Prepare(),
create: acc.Insert(ll).Columns("uid, success, ipaddress, doneAt").Fields("?,?,?,UTC_TIMESTAMP()").Prepare(), create: acc.Insert(ll).Columns("uid,success,ipaddress,doneAt").Fields("?,?,?,UTC_TIMESTAMP()").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -147,7 +147,7 @@ func (l *LoginLogItem) Create() (id int, err error) {
type LoginLogStore interface { type LoginLogStore interface {
Count() (count int) Count() (count int)
CountUser(uid int) (count int) CountUser(uid int) (count int)
GetOffset(uid int, offset int, perPage int) (logs []LoginLogItem, err error) GetOffset(uid, offset, perPage int) (logs []LoginLogItem, err error)
} }
type SQLLoginLogStore struct { type SQLLoginLogStore struct {
@ -160,8 +160,8 @@ func NewLoginLogStore(acc *qgen.Accumulator) (*SQLLoginLogStore, error) {
ll := "login_logs" ll := "login_logs"
return &SQLLoginLogStore{ return &SQLLoginLogStore{
count: acc.Count(ll).Prepare(), count: acc.Count(ll).Prepare(),
countForUser: acc.Count(ll).Where("uid = ?").Prepare(), countForUser: acc.Count(ll).Where("uid=?").Prepare(),
getOffsetByUser: acc.Select(ll).Columns("lid, success, ipaddress, doneAt").Where("uid = ?").Orderby("doneAt DESC").Limit("?,?").Prepare(), getOffsetByUser: acc.Select(ll).Columns("lid,success,ipaddress,doneAt").Where("uid=?").Orderby("doneAt DESC").Limit("?,?").Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }
@ -181,7 +181,7 @@ func (s *SQLLoginLogStore) CountUser(uid int) (count int) {
return count return count
} }
func (s *SQLLoginLogStore) GetOffset(uid int, offset int, perPage int) (logs []LoginLogItem, err error) { func (s *SQLLoginLogStore) GetOffset(uid, offset, perPage int) (logs []LoginLogItem, err error) {
rows, err := s.getOffsetByUser.Query(uid, offset, perPage) rows, err := s.getOffsetByUser.Query(uid, offset, perPage)
if err != nil { if err != nil {
return logs, err return logs, err

View File

@ -18,8 +18,8 @@ var customPageStmts CustomPageStmts
func init() { func init() {
DbInits.Add(func(acc *qgen.Accumulator) error { DbInits.Add(func(acc *qgen.Accumulator) error {
customPageStmts = CustomPageStmts{ customPageStmts = CustomPageStmts{
update: acc.Update("pages").Set("name = ?, title = ?, body = ?, allowedGroups = ?, menuID = ?").Where("pid = ?").Prepare(), update: acc.Update("pages").Set("name=?,title=?,body=?,allowedGroups=?,menuID=?").Where("pid=?").Prepare(),
create: acc.Insert("pages").Columns("name, title, body, allowedGroups, menuID").Fields("?,?,?,?,?").Prepare(), create: acc.Insert("pages").Columns("name,title,body,allowedGroups,menuID").Fields("?,?,?,?,?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -74,7 +74,7 @@ type PageStore interface {
Count() (count int) Count() (count int)
Get(id int) (*CustomPage, error) Get(id int) (*CustomPage, error)
GetByName(name string) (*CustomPage, error) GetByName(name string) (*CustomPage, error)
GetOffset(offset int, perPage int) (pages []*CustomPage, err error) GetOffset(offset, perPage int) (pages []*CustomPage, err error)
Reload(id int) error Reload(id int) error
Delete(id int) error Delete(id int) error
} }
@ -91,11 +91,11 @@ type DefaultPageStore struct {
func NewDefaultPageStore(acc *qgen.Accumulator) (*DefaultPageStore, error) { func NewDefaultPageStore(acc *qgen.Accumulator) (*DefaultPageStore, error) {
pa := "pages" pa := "pages"
return &DefaultPageStore{ return &DefaultPageStore{
get: acc.Select(pa).Columns("name, title, body, allowedGroups, menuID").Where("pid = ?").Prepare(), get: acc.Select(pa).Columns("name, title, body, allowedGroups, menuID").Where("pid=?").Prepare(),
getByName: acc.Select(pa).Columns("pid, name, title, body, allowedGroups, menuID").Where("name = ?").Prepare(), getByName: acc.Select(pa).Columns("pid, name, title, body, allowedGroups, menuID").Where("name=?").Prepare(),
getOffset: acc.Select(pa).Columns("pid, name, title, body, allowedGroups, menuID").Orderby("pid DESC").Limit("?,?").Prepare(), getOffset: acc.Select(pa).Columns("pid, name, title, body, allowedGroups, menuID").Orderby("pid DESC").Limit("?,?").Prepare(),
count: acc.Count(pa).Prepare(), count: acc.Count(pa).Prepare(),
delete: acc.Delete(pa).Where("pid = ?").Prepare(), delete: acc.Delete(pa).Where("pid=?").Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }

View File

@ -108,7 +108,7 @@ type TagToAction struct {
} }
// TODO: Write a test for this // TODO: Write a test for this
func tryStepForward(i int, step int, runes []rune) (int, bool) { func tryStepForward(i, step int, runes []rune) (int, bool) {
i += step i += step
if i < len(runes) { if i < len(runes) {
return i, true return i, true
@ -117,7 +117,7 @@ func tryStepForward(i int, step int, runes []rune) (int, bool) {
} }
// TODO: Write a test for this // TODO: Write a test for this
func tryStepBackward(i int, step int, runes []rune) (int, bool) { func tryStepBackward(i, step int, runes []rune) (int, bool) {
if i == 0 { if i == 0 {
return i, false return i, false
} }
@ -369,7 +369,7 @@ func PreparseMessage(msg string) string {
// TODO: Test this // TODO: Test this
// TODO: Use this elsewhere in the parser? // TODO: Use this elsewhere in the parser?
func peek(cur int, skip int, runes []rune) rune { func peek(cur, skip int, runes []rune) rune {
if (cur + skip) < len(runes) { if (cur + skip) < len(runes) {
return runes[cur+skip] return runes[cur+skip]
} }
@ -972,7 +972,7 @@ func parseMediaString(data string, settings *ParseSettings) (media MediaEmbed, o
} }
// TODO: Write a test for this // TODO: Write a test for this
func CoerceIntString(data string) (res int, length int) { func CoerceIntString(data string) (res, length int) {
if !(data[0] > 47 && data[0] < 58) { if !(data[0] > 47 && data[0] < 58) {
return 0, 1 return 0, 1
} }

View File

@ -174,7 +174,7 @@ func PresetToLang(preset string) string {
// TODO: Is this racey? // TODO: Is this racey?
// TODO: Test this along with the rest of the perms system // TODO: Test this along with the rest of the perms system
func RebuildGroupPermissions(group *Group) error { func RebuildGroupPermissions(g *Group) error {
var permstr []byte var permstr []byte
log.Print("Reloading a group") log.Print("Reloading a group")
@ -185,7 +185,7 @@ func RebuildGroupPermissions(group *Group) error {
} }
defer getGroupPerms.Close() defer getGroupPerms.Close()
err = getGroupPerms.QueryRow(group.ID).Scan(&permstr) err = getGroupPerms.QueryRow(g.ID).Scan(&permstr)
if err != nil { if err != nil {
return err return err
} }
@ -197,15 +197,15 @@ func RebuildGroupPermissions(group *Group) error {
if err != nil { if err != nil {
return err return err
} }
group.Perms = tmpPerms g.Perms = tmpPerms
return nil return nil
} }
func OverridePerms(perms *Perms, status bool) { func OverridePerms(p *Perms, status bool) {
if status { if status {
*perms = AllPerms *p = AllPerms
} else { } else {
*perms = BlankPerms *p = BlankPerms
} }
} }

View File

@ -66,18 +66,17 @@ func InitPluginLangs() error {
continue continue
} }
e := func(field string, name string) error { e := func(field, name string) error {
return errors.New("The "+field+" field must not be blank on plugin '" + name + "'") return errors.New("The " + field + " field must not be blank on plugin '" + name + "'")
} }
if plugin.UName == "" { if plugin.UName == "" {
return e("UName",pluginItem) return e("UName", pluginItem)
} }
if plugin.Name == "" { if plugin.Name == "" {
return e("Name",pluginItem) return e("Name", pluginItem)
} }
if plugin.Author == "" { if plugin.Author == "" {
return e("Author",pluginItem) return e("Author", pluginItem)
} }
if plugin.Main == "" { if plugin.Main == "" {
return errors.New("Couldn't find a main file for plugin '" + pluginItem + "'") return errors.New("Couldn't find a main file for plugin '" + pluginItem + "'")

View File

@ -22,7 +22,7 @@ type Poll struct {
VoteCount int VoteCount int
} }
func (p *Poll) CastVote(optionIndex int, uid int, ip string) error { func (p *Poll) CastVote(optionIndex, uid int, ip string) error {
return Polls.CastVote(optionIndex, p.ID, uid, ip) // TODO: Move the query into a pollStmts rather than having it in the store return Polls.CastVote(optionIndex, p.ID, uid, ip) // TODO: Move the query into a pollStmts rather than having it in the store
} }

View File

@ -61,9 +61,9 @@ func NewDefaultPollStore(cache PollCache) (*DefaultPollStore, error) {
exists: acc.Select("polls").Columns("pollID").Where("pollID = ?").Prepare(), exists: acc.Select("polls").Columns("pollID").Where("pollID = ?").Prepare(),
createPoll: acc.Insert("polls").Columns("parentID, parentTable, type, options").Fields("?,?,?,?").Prepare(), createPoll: acc.Insert("polls").Columns("parentID, parentTable, type, options").Fields("?,?,?,?").Prepare(),
createPollOption: acc.Insert("polls_options").Columns("pollID, option, votes").Fields("?,?,0").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(), addVote: acc.Insert("polls_votes").Columns("pollID,uid,option,castAt,ip").Fields("?,?,?,UTC_TIMESTAMP(),?").Prepare(),
incVoteCount: acc.Update("polls").Set("votes = votes + 1").Where("pollID = ?").Prepare(), incVoteCount: acc.Update("polls").Set("votes = votes + 1").Where("pollID = ?").Prepare(),
incVoteCountForOption: acc.Update("polls_options").Set("votes = votes + 1").Where("option = ? AND pollID = ?").Prepare(), incVoteCountForOption: acc.Update("polls_options").Set("votes = votes + 1").Where("option=? AND pollID=?").Prepare(),
//count: acc.SimpleCount("polls", "", ""), //count: acc.SimpleCount("polls", "", ""),
}, acc.FirstError() }, acc.FirstError()
} }

View File

@ -3,6 +3,7 @@ package common
import ( import (
"database/sql" "database/sql"
"html" "html"
"strconv"
"time" "time"
qgen "github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
@ -45,9 +46,20 @@ func BlankProfileReply(id int) *ProfileReply {
} }
// TODO: Write tests for this // TODO: Write tests for this
// TODO: Remove alerts.
func (r *ProfileReply) Delete() error { func (r *ProfileReply) Delete() error {
_, err := profileReplyStmts.delete.Exec(r.ID) _, err := profileReplyStmts.delete.Exec(r.ID)
if err != nil {
return err
}
// TODO: Better coupling between the two paramsextra queries
aids, err := Activity.AidsByParamsExtra("reply", r.ParentID, "user", strconv.Itoa(r.ID))
if err != nil {
return err
}
for _, aid := range aids {
DismissAlert(r.ParentID, aid)
}
err = Activity.DeleteByParamsExtra("reply", r.ParentID, "user", strconv.Itoa(r.ID))
return err return err
} }

View File

@ -10,6 +10,7 @@ var Prstore ProfileReplyStore
type ProfileReplyStore interface { type ProfileReplyStore interface {
Get(id int) (*ProfileReply, error) Get(id int) (*ProfileReply, error)
Exists(id int) bool
Create(profileID int, content string, createdBy int, ip string) (id int, err error) Create(profileID int, content string, createdBy int, ip string) (id int, err error)
Count() (count int) Count() (count int)
} }
@ -18,6 +19,7 @@ type ProfileReplyStore interface {
// TODO: Add more methods to this like Create() // TODO: Add more methods to this like Create()
type SQLProfileReplyStore struct { type SQLProfileReplyStore struct {
get *sql.Stmt get *sql.Stmt
exists *sql.Stmt
create *sql.Stmt create *sql.Stmt
count *sql.Stmt count *sql.Stmt
} }
@ -25,8 +27,9 @@ type SQLProfileReplyStore struct {
func NewSQLProfileReplyStore(acc *qgen.Accumulator) (*SQLProfileReplyStore, error) { func NewSQLProfileReplyStore(acc *qgen.Accumulator) (*SQLProfileReplyStore, error) {
ur := "users_replies" ur := "users_replies"
return &SQLProfileReplyStore{ return &SQLProfileReplyStore{
get: acc.Select(ur).Columns("uid, content, createdBy, createdAt, lastEdit, lastEditBy, ipaddress").Where("rid = ?").Prepare(), get: acc.Select(ur).Columns("uid, content, createdBy, createdAt, lastEdit, lastEditBy, ip").Where("rid=?").Prepare(),
create: acc.Insert(ur).Columns("uid, content, parsed_content, createdAt, createdBy, ipaddress").Fields("?,?,?,UTC_TIMESTAMP(),?,?").Prepare(), exists: acc.Exists(ur, "rid").Prepare(),
create: acc.Insert(ur).Columns("uid, content, parsed_content, createdAt, createdBy, ip").Fields("?,?,?,UTC_TIMESTAMP(),?,?").Prepare(),
count: acc.Count(ur).Prepare(), count: acc.Count(ur).Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }
@ -37,6 +40,14 @@ func (s *SQLProfileReplyStore) Get(id int) (*ProfileReply, error) {
return &r, err return &r, err
} }
func (s *SQLProfileReplyStore) Exists(id int) bool {
err := s.exists.QueryRow(id).Scan(&id)
if err != nil && err != ErrNoRows {
LogError(err)
}
return err != ErrNoRows
}
func (s *SQLProfileReplyStore) Create(profileID int, content string, createdBy int, ip string) (id int, err error) { func (s *SQLProfileReplyStore) Create(profileID int, content string, createdBy int, ip string) (id int, err error) {
if Config.DisablePostIP { if Config.DisablePostIP {
ip = "0" ip = "0"

View File

@ -12,7 +12,7 @@ var ErrExceededRateLimit = errors.New("You're exceeding a rate limit. Please wai
// TODO: Persist rate limits to disk // TODO: Persist rate limits to disk
type RateLimiter interface { type RateLimiter interface {
LimitIP(limit string, ip string) error LimitIP(limit, ip string) error
LimitUser(limit string, user int) error LimitUser(limit string, user int) error
} }
@ -83,7 +83,7 @@ func NewDefaultRateLimiter() *DefaultRateLimiter {
}} }}
} }
func (l *DefaultRateLimiter) LimitIP(limit string, ip string) error { func (l *DefaultRateLimiter) LimitIP(limit, ip string) error {
limiter, ok := l.limits[limit] limiter, ok := l.limits[limit]
if !ok { if !ok {
return ErrBadRateLimiter return ErrBadRateLimiter

177
common/recalc.go Normal file
View File

@ -0,0 +1,177 @@
package common
import (
"database/sql"
//"log"
"strconv"
qgen "github.com/Azareal/Gosora/query_gen"
)
var Recalc RecalcInt
type RecalcInt interface {
Replies() (count int, err error)
Subscriptions() (count int, err error)
ActivityStream() (count int, err error)
Users() error
Attachments() (count int, err error)
}
type DefaultRecalc struct {
getActivitySubscriptions *sql.Stmt
getActivityStream *sql.Stmt
getAttachments *sql.Stmt
}
func NewDefaultRecalc(acc *qgen.Accumulator) (*DefaultRecalc, error) {
return &DefaultRecalc{
getActivitySubscriptions: acc.Select("activity_subscriptions").Columns("targetID,targetType").Prepare(),
getActivityStream: acc.Select("activity_stream").Columns("asid,event,elementID,elementType,extra").Prepare(),
getAttachments: acc.Select("attachments").Columns("attachID,originID,originTable").Prepare(),
}, acc.FirstError()
}
func (s *DefaultRecalc) Replies() (count int, err error) {
var ltid int
err = Rstore.Each(func(r *Reply) error {
if ltid == r.ParentID && r.ParentID > 0 {
//return nil
}
if !Topics.Exists(r.ParentID) {
// TODO: Delete in chunks not one at a time?
if err := r.Delete(); err != nil {
return err
}
count++
}
return nil
})
return count, err
}
func (s *DefaultRecalc) Subscriptions() (count int, err error) {
err = eachall(s.getActivitySubscriptions, func(r *sql.Rows) error {
var targetID int
var targetType string
err := r.Scan(&targetID, &targetType)
if err != nil {
return err
}
if targetType == "topic" {
if !Topics.Exists(targetID) {
// TODO: Delete in chunks not one at a time?
err := Subscriptions.DeleteResource(targetID, targetType)
if err != nil {
return err
}
count++
}
}
return nil
})
return count, err
}
type Existable interface {
Exists(id int) bool
}
func (s *DefaultRecalc) ActivityStream() (count int, err error) {
err = eachall(s.getActivityStream, func(r *sql.Rows) error {
var asid, elementID int
var event, elementType, extra string
err := r.Scan(&asid, &event, &elementID, &elementType, &extra)
if err != nil {
return err
}
//log.Print("asid:",asid)
var s Existable
switch elementType {
case "user":
if event == "reply" {
extraI, _ := strconv.Atoi(extra)
if extraI > 0 {
s = Prstore
elementID = extraI
} else {
return nil
}
} else {
return nil
}
case "topic":
s = Topics
// TODO: Delete reply events with an empty extra field
if event == "reply" {
extraI, _ := strconv.Atoi(extra)
if extraI > 0 {
s = Rstore
elementID = extraI
}
}
case "post":
s = Rstore
// TODO: Add a TopicExistsByReplyID for efficiency
/*_, err = TopicByReplyID(elementID)
if err == sql.ErrNoRows {
// TODO: Delete in chunks not one at a time?
err := Activity.Delete(asid)
if err != nil {
return err
}
count++
} else if err != nil {
return err
}*/
default:
return nil
}
if !s.Exists(elementID) {
// TODO: Delete in chunks not one at a time?
err := Activity.Delete(asid)
if err != nil {
return err
}
count++
}
return nil
})
return count, err
}
func (s *DefaultRecalc) Users() error {
return Users.Each(func(u *User) error {
return u.RecalcPostStats()
})
}
func (s *DefaultRecalc) Attachments() (count int, err error) {
err = eachall(s.getAttachments, func(r *sql.Rows) error {
var aid, originID int
var originType string
err := r.Scan(&aid, &originID, &originType)
if err != nil {
return err
}
var s Existable
switch originType {
case "topics":
s = Topics
case "replies":
s = Rstore
default:
return nil
}
if !s.Exists(originID) {
// TODO: Delete in chunks not one at a time?
err := Attachments.Delete(aid)
if err != nil {
return err
}
count++
}
return nil
})
return count, err
}

View File

@ -11,6 +11,7 @@ import (
"errors" "errors"
"html" "html"
"time" "time"
"strconv"
qgen "github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
@ -69,6 +70,8 @@ type ReplyStmts struct {
updateTopicReplies *sql.Stmt updateTopicReplies *sql.Stmt
updateTopicReplies2 *sql.Stmt updateTopicReplies2 *sql.Stmt
getAidsOfReply *sql.Stmt
} }
func init() { func init() {
@ -89,6 +92,8 @@ func init() {
// TODO: Optimise this to avoid firing an update if it's not the last reply in a topic. We will need to set lastReplyID properly in other places and in the patcher first so we can use it here. // TODO: Optimise this to avoid firing an update if it's not the last reply in a topic. We will need to set lastReplyID properly in other places and in the patcher first so we can use it here.
updateTopicReplies: acc.RawPrepare("UPDATE topics t INNER JOIN replies r ON t.tid = r.tid SET t.lastReplyBy = r.createdBy, t.lastReplyAt = r.createdAt, t.lastReplyID = r.rid WHERE t.tid = ?"), updateTopicReplies: acc.RawPrepare("UPDATE topics t INNER JOIN replies r ON t.tid = r.tid SET t.lastReplyBy = r.createdBy, t.lastReplyAt = r.createdAt, t.lastReplyID = r.rid WHERE t.tid = ?"),
updateTopicReplies2: acc.Update("topics").Set("lastReplyAt=createdAt,lastReplyBy=createdBy,lastReplyID=0").Where("postCount=1 AND tid=?").Prepare(), updateTopicReplies2: acc.Update("topics").Set("lastReplyAt=createdAt,lastReplyBy=createdBy,lastReplyID=0").Where("postCount=1 AND tid=?").Prepare(),
getAidsOfReply: acc.Select("attachments").Columns("attachID").Where("originID=? AND originTable='replies'").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -120,7 +125,6 @@ func (r *Reply) Like(uid int) (err error) {
} }
// TODO: Refresh topic list? // TODO: Refresh topic list?
// TODO: Restructure alerts so we can delete the "x replied to topic" ones too.
func (r *Reply) Delete() error { func (r *Reply) Delete() error {
creator, err := Users.Get(r.CreatedBy) creator, err := Users.Get(r.CreatedBy)
if err == nil { if err == nil {
@ -158,6 +162,14 @@ func (r *Reply) Delete() error {
if err != nil { if err != nil {
return err return err
} }
err = handleReplyAttachments(r.ID)
if err != nil {
return err
}
err = Activity.DeleteByParamsExtra("reply",r.ParentID,"topic",strconv.Itoa(r.ID))
if err != nil {
return err
}
_, err = replyStmts.deleteActivitySubs.Exec(r.ID) _, err = replyStmts.deleteActivitySubs.Exec(r.ID)
if err != nil { if err != nil {
return err return err

View File

@ -11,8 +11,13 @@ var Rstore ReplyStore
type ReplyStore interface { type ReplyStore interface {
Get(id int) (*Reply, error) Get(id int) (*Reply, error)
Create(topic *Topic, content string, ip string, uid int) (id int, err error) Each(f func(*Reply) error) error
Exists(id int) bool
Create(t *Topic, content string, ip string, uid int) (id int, err error)
Count() (count int) Count() (count int)
CountUser(uid int) (count int)
CountMegaUser(uid int) (count int)
CountBigUser(uid int) (count int)
SetCache(cache ReplyCache) SetCache(cache ReplyCache)
GetCache() ReplyCache GetCache() ReplyCache
@ -21,9 +26,13 @@ type ReplyStore interface {
type SQLReplyStore struct { type SQLReplyStore struct {
cache ReplyCache cache ReplyCache
get *sql.Stmt get *sql.Stmt
create *sql.Stmt getAll *sql.Stmt
count *sql.Stmt exists *sql.Stmt
create *sql.Stmt
count *sql.Stmt
countUser *sql.Stmt
countWordUser *sql.Stmt
} }
func NewSQLReplyStore(acc *qgen.Accumulator, cache ReplyCache) (*SQLReplyStore, error) { func NewSQLReplyStore(acc *qgen.Accumulator, cache ReplyCache) (*SQLReplyStore, error) {
@ -32,10 +41,14 @@ func NewSQLReplyStore(acc *qgen.Accumulator, cache ReplyCache) (*SQLReplyStore,
} }
re := "replies" re := "replies"
return &SQLReplyStore{ return &SQLReplyStore{
cache: cache, cache: cache,
get: acc.Select(re).Columns("tid, content, createdBy, createdAt, lastEdit, lastEditBy, ipaddress, likeCount, attachCount, actionType").Where("rid = ?").Prepare(), get: acc.Select(re).Columns("tid, content, createdBy, createdAt, lastEdit, lastEditBy, ip, likeCount, attachCount, actionType").Where("rid=?").Prepare(),
create: acc.Insert(re).Columns("tid, content, parsed_content, createdAt, lastUpdated, ipaddress, words, createdBy").Fields("?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?").Prepare(), getAll: acc.Select(re).Columns("rid,tid, content, createdBy, createdAt, lastEdit, lastEditBy, ip, likeCount, attachCount, actionType").Prepare(),
count: acc.Count(re).Prepare(), exists: acc.Exists(re, "rid").Prepare(),
create: acc.Insert(re).Columns("tid, content, parsed_content, createdAt, lastUpdated, ip, words, createdBy").Fields("?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?").Prepare(),
count: acc.Count(re).Prepare(),
countUser: acc.Count(re).Where("createdBy=?").Prepare(),
countWordUser: acc.Count(re).Where("createdBy=? AND words>=?").Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }
@ -53,8 +66,38 @@ func (s *SQLReplyStore) Get(id int) (*Reply, error) {
return r, err return r, err
} }
/*func (s *SQLReplyStore) eachr(f func(*sql.Rows) error) error {
return eachall(s.getAll, f)
}*/
func (s *SQLReplyStore) Each(f func(*Reply) error) error {
rows, err := s.getAll.Query()
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
r := new(Reply)
if err := rows.Scan(&r.ID, &r.ParentID, &r.Content, &r.CreatedBy, &r.CreatedAt, &r.LastEdit, &r.LastEditBy, &r.IP, &r.LikeCount, &r.AttachCount, &r.ActionType); err != nil {
return err
}
if err := f(r); err != nil {
return err
}
}
return rows.Err()
}
func (s *SQLReplyStore) Exists(id int) bool {
err := s.exists.QueryRow(id).Scan(&id)
if err != nil && err != ErrNoRows {
LogError(err)
}
return err != ErrNoRows
}
// TODO: Write a test for this // TODO: Write a test for this
func (s *SQLReplyStore) Create(t *Topic, content string, ip string, uid int) (rid int, err error) { func (s *SQLReplyStore) Create(t *Topic, content, ip string, uid int) (rid int, err error) {
if Config.DisablePostIP { if Config.DisablePostIP {
ip = "0" ip = "0"
} }
@ -74,11 +117,16 @@ func (s *SQLReplyStore) Create(t *Topic, content string, ip string, uid int) (ri
// TODO: Write a test for this // TODO: Write a test for this
// Count returns the total number of topic replies on these forums // Count returns the total number of topic replies on these forums
func (s *SQLReplyStore) Count() (count int) { func (s *SQLReplyStore) Count() (count int) {
err := s.count.QueryRow().Scan(&count) return Countf(s.count)
if err != nil { }
LogError(err) func (s *SQLReplyStore) CountUser(uid int) (count int) {
} return Countf(s.countUser, uid)
return count }
func (s *SQLReplyStore) CountMegaUser(uid int) (count int) {
return Countf(s.countWordUser, uid, SettingBox.Load().(SettingMap)["megapost_min_words"].(int))
}
func (s *SQLReplyStore) CountBigUser(uid int) (count int) {
return Countf(s.countWordUser, uid, SettingBox.Load().(SettingMap)["bigpost_min_words"].(int))
} }
func (s *SQLReplyStore) SetCache(cache ReplyCache) { func (s *SQLReplyStore) SetCache(cache ReplyCache) {

View File

@ -17,7 +17,7 @@ var ErrAlreadyReported = errors.New("This item has already been reported")
// The report system mostly wraps around the topic system for simplicty // The report system mostly wraps around the topic system for simplicty
type ReportStore interface { type ReportStore interface {
Create(title string, content string, user *User, itemType string, itemID int) (int, error) Create(title, content string, user *User, itemType string, itemID int) (int, error)
} }
type DefaultReportStore struct { type DefaultReportStore struct {
@ -28,13 +28,13 @@ type DefaultReportStore struct {
func NewDefaultReportStore(acc *qgen.Accumulator) (*DefaultReportStore, error) { func NewDefaultReportStore(acc *qgen.Accumulator) (*DefaultReportStore, error) {
t := "topics" t := "topics"
return &DefaultReportStore{ return &DefaultReportStore{
create: acc.Insert(t).Columns("title, content, parsed_content, ipaddress, createdAt, lastReplyAt, createdBy, lastReplyBy, data, parentID, css_class").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?,'report'").Prepare(), create: acc.Insert(t).Columns("title, content, parsed_content, ip, createdAt, lastReplyAt, createdBy, lastReplyBy, data, parentID, css_class").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?,'report'").Prepare(),
exists: acc.Count(t).Where("data = ? AND data != '' AND parentID = ?").Prepare(), exists: acc.Count(t).Where("data=? AND data!='' AND parentID=?").Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }
// ! There's a data race in this. If two users report one item at the exact same time, then both reports will go through // ! There's a data race in this. If two users report one item at the exact same time, then both reports will go through
func (s *DefaultReportStore) Create(title string, content string, u *User, itemType string, itemID int) (tid int, err error) { func (s *DefaultReportStore) Create(title, content string, u *User, itemType string, itemID int) (tid int, err error) {
var count int var count int
err = s.exists.QueryRow(itemType+"_"+strconv.Itoa(itemID), ReportForumID).Scan(&count) err = s.exists.QueryRow(itemType+"_"+strconv.Itoa(itemID), ReportForumID).Scan(&count)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {

View File

@ -10,21 +10,39 @@ var Subscriptions SubscriptionStore
// ? Should we have a subscription store for each zone? topic, forum, etc? // ? Should we have a subscription store for each zone? topic, forum, etc?
type SubscriptionStore interface { type SubscriptionStore interface {
Add(uid int, elementID int, elementType string) error Add(uid, elementID int, elementType string) error
Delete(uid, targetID int, targetType string) error
DeleteResource(targetID int, targetType string) error
} }
type DefaultSubscriptionStore struct { type DefaultSubscriptionStore struct {
add *sql.Stmt add *sql.Stmt
delete *sql.Stmt
deleteResource *sql.Stmt
} }
func NewDefaultSubscriptionStore() (*DefaultSubscriptionStore, error) { func NewDefaultSubscriptionStore() (*DefaultSubscriptionStore, error) {
acc := qgen.NewAcc() acc := qgen.NewAcc()
ast := "activity_subscriptions"
return &DefaultSubscriptionStore{ return &DefaultSubscriptionStore{
add: acc.Insert("activity_subscriptions").Columns("user, targetID, targetType, level").Fields("?,?,?,2").Prepare(), add: acc.Insert(ast).Columns("user, targetID, targetType, level").Fields("?,?,?,2").Prepare(),
delete: acc.Delete(ast).Where("user=? AND targetID=? AND targetType=?").Prepare(),
deleteResource: acc.Delete(ast).Where("targetID=? AND targetType=?").Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }
func (s *DefaultSubscriptionStore) Add(uid int, elementID int, elementType string) error { func (s *DefaultSubscriptionStore) Add(uid, elementID int, elementType string) error {
_, err := s.add.Exec(uid, elementID, elementType) _, err := s.add.Exec(uid, elementID, elementType)
return err return err
} }
// TODO: Add a primary key to the activity subscriptions table
func (s *DefaultSubscriptionStore) Delete(uid, targetID int, targetType string) error {
_, err := s.delete.Exec(uid, targetID, targetType)
return err
}
func (s *DefaultSubscriptionStore) DeleteResource(targetID int, targetType string) error {
_, err := s.deleteResource.Exec(targetID, targetType)
return err
}

View File

@ -15,7 +15,7 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
// TODO: Something more thread-safe // TODO: Something more thread-safe
@ -44,8 +44,8 @@ func init() {
t := "themes" t := "themes"
themeStmts = ThemeStmts{ themeStmts = ThemeStmts{
getAll: acc.Select(t).Columns("uname,default").Prepare(), getAll: acc.Select(t).Columns("uname,default").Prepare(),
isDefault: acc.Select(t).Columns("default").Where("uname = ?").Prepare(), isDefault: acc.Select(t).Columns("default").Where("uname=?").Prepare(),
update: acc.Update(t).Set("default = ?").Where("uname = ?").Prepare(), update: acc.Update(t).Set("default=?").Where("uname=?").Prepare(),
add: acc.Insert(t).Columns("uname,default").Fields("?,?").Prepare(), add: acc.Insert(t).Columns("uname,default").Fields("?,?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()

View File

@ -31,12 +31,12 @@ func ThumbTask(thumbChan chan bool) {
// Has the avatar been removed or already been processed by the thumbnailer? // Has the avatar been removed or already been processed by the thumbnailer?
if len(u.RawAvatar) < 2 || u.RawAvatar[1] == '.' { if len(u.RawAvatar) < 2 || u.RawAvatar[1] == '.' {
_, _ = acc.Delete("users_avatar_queue").Where("uid = ?").Run(uid) _, _ = acc.Delete("users_avatar_queue").Where("uid=?").Run(uid)
return nil return nil
} }
_, err = os.Stat("./uploads/avatar_" + strconv.Itoa(u.ID) + u.RawAvatar) _, err = os.Stat("./uploads/avatar_" + strconv.Itoa(u.ID) + u.RawAvatar)
if os.IsNotExist(err) { if os.IsNotExist(err) {
_, _ = acc.Delete("users_avatar_queue").Where("uid = ?").Run(uid) _, _ = acc.Delete("users_avatar_queue").Where("uid=?").Run(uid)
return nil return nil
} else if err != nil { } else if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
@ -63,7 +63,7 @@ func ThumbTask(thumbChan chan bool) {
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
_, err = acc.Delete("users_avatar_queue").Where("uid = ?").Run(uid) _, err = acc.Delete("users_avatar_queue").Where("uid=?").Run(uid)
return errors.WithStack(err) return errors.WithStack(err)
}) })
if err != nil { if err != nil {
@ -86,18 +86,18 @@ func ThumbTask(thumbChan chan bool) {
var Thumbnailer ThumbnailerInt var Thumbnailer ThumbnailerInt
type ThumbnailerInt interface { type ThumbnailerInt interface {
Resize(format string, inPath string, tmpPath string, outPath string, width int) error Resize(format, inPath, tmpPath, outPath string, width int) error
} }
type RezThumbnailer struct { type RezThumbnailer struct {
} }
func (thumb *RezThumbnailer) Resize(format string, inPath string, tmpPath string, outPath string, width int) error { func (thumb *RezThumbnailer) Resize(format, inPath, tmpPath, outPath string, width int) error {
// TODO: Sniff the aspect ratio of the image and calculate the dest height accordingly, bug make sure it isn't excessively high // TODO: Sniff the aspect ratio of the image and calculate the dest height accordingly, bug make sure it isn't excessively high
return nil return nil
} }
func (thumb *RezThumbnailer) resize(format string, inPath string, outPath string, width int, height int) error { func (thumb *RezThumbnailer) resize(format, inPath, outPath string, width, height int) error {
return nil return nil
} }
@ -109,7 +109,7 @@ func NewCaireThumbnailer() *CaireThumbnailer {
return &CaireThumbnailer{} return &CaireThumbnailer{}
} }
func precodeImage(format string, inPath string, tmpPath string) error { func precodeImage(format, inPath, tmpPath string) error {
imageFile, err := os.Open(inPath) imageFile, err := os.Open(inPath)
if err != nil { if err != nil {
return err return err
@ -139,7 +139,7 @@ func precodeImage(format string, inPath string, tmpPath string) error {
return jpeg.Encode(outFile, img, nil) return jpeg.Encode(outFile, img, nil)
} }
func (thumb *CaireThumbnailer) Resize(format string, inPath string, tmpPath string, outPath string, width int) error { func (thumb *CaireThumbnailer) Resize(format, inPath, tmpPath, outPath string, width int) error {
err := precodeImage(format, inPath, tmpPath) err := precodeImage(format, inPath, tmpPath)
if err != nil { if err != nil {
return err return err

View File

@ -15,6 +15,8 @@ import (
"strings" "strings"
"time" "time"
//"log"
p "github.com/Azareal/Gosora/common/phrases" p "github.com/Azareal/Gosora/common/phrases"
qgen "github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
@ -197,9 +199,9 @@ type TopicStmts struct {
createLike *sql.Stmt createLike *sql.Stmt
addLikesToTopic *sql.Stmt addLikesToTopic *sql.Stmt
delete *sql.Stmt delete *sql.Stmt
deleteReplies *sql.Stmt
deleteLikesForTopic *sql.Stmt deleteLikesForTopic *sql.Stmt
deleteActivity *sql.Stmt deleteActivity *sql.Stmt
deleteActivitySubs *sql.Stmt
edit *sql.Stmt edit *sql.Stmt
setPoll *sql.Stmt setPoll *sql.Stmt
createAction *sql.Stmt createAction *sql.Stmt
@ -215,8 +217,8 @@ func init() {
t := "topics" t := "topics"
topicStmts = TopicStmts{ topicStmts = TopicStmts{
getRids: acc.Select("replies").Columns("rid").Where("tid = ?").Orderby("rid ASC").Limit("?,?").Prepare(), getRids: acc.Select("replies").Columns("rid").Where("tid = ?").Orderby("rid ASC").Limit("?,?").Prepare(),
getReplies: acc.SimpleLeftJoin("replies AS r", "users AS u", "r.rid, r.content, r.createdBy, r.createdAt, r.lastEdit, r.lastEditBy, u.avatar, u.name, u.group, u.level, r.ipaddress, r.likeCount, r.attachCount, r.actionType", "r.createdBy = u.uid", "r.tid = ?", "r.rid ASC", "?,?"), getReplies: acc.SimpleLeftJoin("replies AS r", "users AS u", "r.rid, r.content, r.createdBy, r.createdAt, r.lastEdit, r.lastEditBy, u.avatar, u.name, u.group, u.level, r.ip, r.likeCount, r.attachCount, r.actionType", "r.createdBy = u.uid", "r.tid = ?", "r.rid ASC", "?,?"),
addReplies: acc.Update(t).Set("postCount = postCount + ?, lastReplyBy = ?, lastReplyAt = UTC_TIMESTAMP()").Where("tid = ?").Prepare(), addReplies: acc.Update(t).Set("postCount=postCount+?, lastReplyBy=?, lastReplyAt=UTC_TIMESTAMP()").Where("tid=?").Prepare(),
updateLastReply: acc.Update(t).Set("lastReplyID=?").Where("lastReplyID > ? AND tid = ?").Prepare(), updateLastReply: acc.Update(t).Set("lastReplyID=?").Where("lastReplyID > ? AND tid = ?").Prepare(),
lock: acc.Update(t).Set("is_closed=1").Where("tid=?").Prepare(), lock: acc.Update(t).Set("is_closed=1").Where("tid=?").Prepare(),
unlock: acc.Update(t).Set("is_closed=0").Where("tid=?").Prepare(), unlock: acc.Update(t).Set("is_closed=0").Where("tid=?").Prepare(),
@ -225,17 +227,17 @@ func init() {
unstick: acc.Update(t).Set("sticky=0").Where("tid=?").Prepare(), unstick: acc.Update(t).Set("sticky=0").Where("tid=?").Prepare(),
hasLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy=? and targetItem=? and targetType='topics'").Prepare(), hasLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy=? and targetItem=? and targetType='topics'").Prepare(),
createLike: acc.Insert("likes").Columns("weight, targetItem, targetType, sentBy, createdAt").Fields("?,?,?,?,UTC_TIMESTAMP()").Prepare(), createLike: acc.Insert("likes").Columns("weight, targetItem, targetType, sentBy, createdAt").Fields("?,?,?,?,UTC_TIMESTAMP()").Prepare(),
addLikesToTopic: acc.Update(t).Set("likeCount=likeCount+?").Where("tid = ?").Prepare(), addLikesToTopic: acc.Update(t).Set("likeCount=likeCount+?").Where("tid=?").Prepare(),
delete: acc.Delete(t).Where("tid=?").Prepare(), delete: acc.Delete(t).Where("tid=?").Prepare(),
deleteReplies: acc.Delete("replies").Where("tid=?").Prepare(),
deleteLikesForTopic: acc.Delete("likes").Where("targetItem=? AND targetType='topics'").Prepare(), deleteLikesForTopic: acc.Delete("likes").Where("targetItem=? AND targetType='topics'").Prepare(),
deleteActivity: acc.Delete("activity_stream").Where("elementID=? AND elementType='topic'").Prepare(), deleteActivity: acc.Delete("activity_stream").Where("elementID=? AND elementType='topic'").Prepare(),
deleteActivitySubs: acc.Delete("activity_subscriptions").Where("targetID=? AND targetType='topic'").Prepare(),
edit: acc.Update(t).Set("title=?,content=?,parsed_content=?").Where("tid=?").Prepare(), // TODO: Only run the content update bits on non-polls, does this matter? edit: acc.Update(t).Set("title=?,content=?,parsed_content=?").Where("tid=?").Prepare(), // TODO: Only run the content update bits on non-polls, does this matter?
setPoll: acc.Update(t).Set("poll=?").Where("tid=? AND poll=0").Prepare(), setPoll: acc.Update(t).Set("poll=?").Where("tid=? AND poll=0").Prepare(),
createAction: acc.Insert("replies").Columns("tid, actionType, ipaddress, createdBy, createdAt, lastUpdated, content, parsed_content").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''").Prepare(), createAction: acc.Insert("replies").Columns("tid, actionType, ip, createdBy, createdAt, lastUpdated, content, parsed_content").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''").Prepare(),
getTopicUser: acc.SimpleLeftJoin("topics AS t", "users AS u", "t.title, t.content, t.createdBy, t.createdAt, t.lastReplyAt, t.lastReplyBy, t.lastReplyID, t.is_closed, t.sticky, t.parentID, t.ipaddress, t.views, t.postCount, t.likeCount, t.attachCount,t.poll, u.name, u.avatar, u.group, u.level", "t.createdBy = u.uid", "tid = ?", "", ""), getTopicUser: acc.SimpleLeftJoin("topics AS t", "users AS u", "t.title, t.content, t.createdBy, t.createdAt, t.lastReplyAt, t.lastReplyBy, t.lastReplyID, t.is_closed, t.sticky, t.parentID, t.ip, t.views, t.postCount, t.likeCount, t.attachCount,t.poll, u.name, u.avatar, u.group, u.level", "t.createdBy=u.uid", "tid=?", "", ""),
getByReplyID: acc.SimpleLeftJoin("replies AS r", "topics AS t", "t.tid, t.title, t.content, t.createdBy, t.createdAt, t.is_closed, t.sticky, t.parentID, t.ipaddress, t.views, t.postCount, t.likeCount, t.poll, t.data", "r.tid = t.tid", "rid = ?", "", ""), getByReplyID: acc.SimpleLeftJoin("replies AS r", "topics AS t", "t.tid, t.title, t.content, t.createdBy, t.createdAt, t.is_closed, t.sticky, t.parentID, t.ip, t.views, t.postCount, t.likeCount, t.poll, t.data", "r.tid=t.tid", "rid=?", "", ""),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -322,8 +324,17 @@ func (t *Topic) Like(score, uid int) (err error) {
return err return err
} }
// TODO: Implement this // TODO: Use a transaction
func (t *Topic) Unlike(uid int) error { func (t *Topic) Unlike(uid int) error {
err := Likes.Delete(t.ID,"topics")
if err != nil {
return err
}
_, err = topicStmts.addLikesToTopic.Exec(-1, t.ID)
if err != nil {
return err
}
_, err = userStmts.decLiked.Exec(1, uid)
t.cacheRemove() t.cacheRemove()
return nil return nil
} }
@ -345,43 +356,72 @@ func handleLikedTopicReplies(tid int) error {
if err != nil { if err != nil {
return err return err
} }
err = Activity.DeleteByParams("like", rid, "post")
if err != nil {
return err
}
} }
return rows.Err() return rows.Err()
} }
func handleTopicAttachments(tid int) error { func handleTopicAttachments(tid int) error {
f := func(stmt *sql.Stmt) error { err := handleAttachments(userStmts.getAttachmentsOfTopic, tid)
rows, err := stmt.Query(tid)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var aid int
err := rows.Scan(&aid)
if err != nil {
return err
}
err = DeleteAttachment(aid)
if err != nil && err != sql.ErrNoRows {
return err
}
}
return rows.Err()
}
err := f(userStmts.getAttachmentsOfTopic)
if err != nil { if err != nil {
return err return err
} }
return f(userStmts.getAttachmentsOfTopic2) return handleAttachments(userStmts.getAttachmentsOfTopic2, tid)
}
func handleReplyAttachments(rid int) error {
return handleAttachments(replyStmts.getAidsOfReply, rid)
}
func handleAttachments(stmt *sql.Stmt, id int) error {
rows, err := stmt.Query(id)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var aid int
err := rows.Scan(&aid)
if err != nil {
return err
}
err = DeleteAttachment(aid)
if err != nil && err != sql.ErrNoRows {
return err
}
}
return rows.Err()
}
// TODO: Only load a row per createdBy, maybe with group by?
func handleTopicReplies(umap map[int]struct{}, uid int, tid int) error {
rows, err := userStmts.getRepliesOfTopic.Query(uid, tid)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var createdBy int
err := rows.Scan(&createdBy)
if err != nil {
return err
}
umap[createdBy] = struct{}{}
}
return rows.Err()
} }
// TODO: Use a transaction here // TODO: Use a transaction here
func (t *Topic) Delete() error { func (t *Topic) Delete() error {
creator, err := Users.Get(t.CreatedBy) /*creator, err := Users.Get(t.CreatedBy)
if err == nil { if err == nil {
err = creator.DecreasePostStats(WordCount(t.Content), true) err = creator.DecreasePostStats(WordCount(t.Content), true)
if err != nil { if err != nil {
@ -389,10 +429,10 @@ func (t *Topic) Delete() error {
} }
} else if err != ErrNoRows { } else if err != ErrNoRows {
return err return err
} }*/
// TODO: Clear reply cache too // TODO: Clear reply cache too
_, err = topicStmts.delete.Exec(t.ID) _, err := topicStmts.delete.Exec(t.ID)
t.cacheRemove() t.cacheRemove()
if err != nil { if err != nil {
return err return err
@ -405,7 +445,30 @@ func (t *Topic) Delete() error {
if err != nil { if err != nil {
return err return err
} }
err = handleLikedTopicReplies(t.ID)
if t.PostCount > 1 {
err = handleLikedTopicReplies(t.ID)
if err != nil {
return err
}
umap := make(map[int]struct{})
err = handleTopicReplies(umap, t.CreatedBy, t.ID)
if err != nil {
return err
}
_, err = topicStmts.deleteReplies.Exec(t.ID)
if err != nil {
return err
}
for uid := range umap {
err = (&User{ID: uid}).RecalcPostStats()
if err != nil {
//log.Printf("err: %+v\n", err)
return err
}
}
}
err = (&User{ID: t.CreatedBy}).RecalcPostStats()
if err != nil { if err != nil {
return err return err
} }
@ -413,7 +476,7 @@ func (t *Topic) Delete() error {
if err != nil { if err != nil {
return err return err
} }
_, err = topicStmts.deleteActivitySubs.Exec(t.ID) err = Subscriptions.DeleteResource(t.ID, "topic")
if err != nil { if err != nil {
return err return err
} }

View File

@ -29,13 +29,16 @@ type TopicStore interface {
BypassGet(id int) (*Topic, error) BypassGet(id int) (*Topic, error)
BulkGetMap(ids []int) (list map[int]*Topic, err error) BulkGetMap(ids []int) (list map[int]*Topic, err error)
Exists(id int) bool Exists(id int) bool
Create(fid int, name string, content string, uid int, ip string) (tid int, err error) Create(fid int, name, content string, uid int, ip string) (tid int, err error)
AddLastTopic(item *Topic, fid int) error // unimplemented AddLastTopic(item *Topic, fid int) error // unimplemented
Reload(id int) error // Too much SQL logic to move into TopicCache Reload(id int) error // Too much SQL logic to move into TopicCache
// TODO: Implement these two methods // TODO: Implement these two methods
//Replies(tid int) ([]*Reply, error) //Replies(tid int) ([]*Reply, error)
//RepliesRange(tid int, lower int, higher int) ([]*Reply, error) //RepliesRange(tid int, lower int, higher int) ([]*Reply, error)
Count() int Count() int
CountUser(uid int) int
CountMegaUser(uid int) int
CountBigUser(uid int) int
SetCache(cache TopicCache) SetCache(cache TopicCache)
GetCache() TopicCache GetCache() TopicCache
@ -44,10 +47,12 @@ type TopicStore interface {
type DefaultTopicStore struct { type DefaultTopicStore struct {
cache TopicCache cache TopicCache
get *sql.Stmt get *sql.Stmt
exists *sql.Stmt exists *sql.Stmt
count *sql.Stmt count *sql.Stmt
create *sql.Stmt countUser *sql.Stmt
countWordUser *sql.Stmt
create *sql.Stmt
} }
// NewDefaultTopicStore gives you a new instance of DefaultTopicStore // NewDefaultTopicStore gives you a new instance of DefaultTopicStore
@ -58,38 +63,40 @@ func NewDefaultTopicStore(cache TopicCache) (*DefaultTopicStore, error) {
} }
t := "topics" t := "topics"
return &DefaultTopicStore{ return &DefaultTopicStore{
cache: cache, cache: cache,
get: acc.Select(t).Columns("title, content, createdBy, createdAt, lastReplyBy, lastReplyAt, lastReplyID, is_closed, sticky, parentID, ipaddress, views, postCount, likeCount, attachCount, poll, data").Where("tid = ?").Prepare(), get: acc.Select(t).Columns("title, content, createdBy, createdAt, lastReplyBy, lastReplyAt, lastReplyID, is_closed, sticky, parentID, ip, views, postCount, likeCount, attachCount, poll, data").Where("tid=?").Prepare(),
exists: acc.Exists(t, "tid").Prepare(), exists: acc.Exists(t, "tid").Prepare(),
count: acc.Count(t).Prepare(), count: acc.Count(t).Prepare(),
create: acc.Insert(t).Columns("parentID, title, content, parsed_content, createdAt, lastReplyAt, lastReplyBy, ipaddress, words, createdBy").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?").Prepare(), countUser: acc.Count(t).Where("createdBy=?").Prepare(),
countWordUser: acc.Count(t).Where("createdBy=? AND words>=?").Prepare(),
create: acc.Insert(t).Columns("parentID, title, content, parsed_content, createdAt, lastReplyAt, lastReplyBy, ip, words, createdBy").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?").Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }
func (s *DefaultTopicStore) DirtyGet(id int) *Topic { func (s *DefaultTopicStore) DirtyGet(id int) *Topic {
topic, err := s.cache.Get(id) t, err := s.cache.Get(id)
if err == nil { if err == nil {
return topic return t
} }
topic, err = s.BypassGet(id) t, err = s.BypassGet(id)
if err == nil { if err == nil {
_ = s.cache.Set(topic) _ = s.cache.Set(t)
return topic return t
} }
return BlankTopic() return BlankTopic()
} }
// TODO: Log weird cache errors? // TODO: Log weird cache errors?
func (s *DefaultTopicStore) Get(id int) (topic *Topic, err error) { func (s *DefaultTopicStore) Get(id int) (t *Topic, err error) {
topic, err = s.cache.Get(id) t, err = s.cache.Get(id)
if err == nil { if err == nil {
return topic, nil return t, nil
} }
topic, err = s.BypassGet(id) t, err = s.BypassGet(id)
if err == nil { if err == nil {
_ = s.cache.Set(topic) _ = s.cache.Set(t)
} }
return topic, err return t, err
} }
// BypassGet will always bypass the cache and pull the topic directly from the database // BypassGet will always bypass the cache and pull the topic directly from the database
@ -102,6 +109,15 @@ func (s *DefaultTopicStore) BypassGet(id int) (*Topic, error) {
return t, err return t, err
} }
/*func (s *DefaultTopicStore) GetByUser(uid int) (list map[int]*Topic, err error) {
t := &Topic{ID: id}
err := s.get.QueryRow(id).Scan(&t.Title, &t.Content, &t.CreatedBy, &t.CreatedAt, &t.LastReplyBy, &t.LastReplyAt, &t.LastReplyID, &t.IsClosed, &t.Sticky, &t.ParentID, &t.IP, &t.ViewCount, &t.PostCount, &t.LikeCount, &t.AttachCount, &t.Poll, &t.Data)
if err == nil {
t.Link = BuildTopicURL(NameToSlug(t.Title), id)
}
return t, err
}*/
// TODO: Avoid duplicating much of this logic from user_store.go // TODO: Avoid duplicating much of this logic from user_store.go
func (s *DefaultTopicStore) BulkGetMap(ids []int) (list map[int]*Topic, err error) { func (s *DefaultTopicStore) BulkGetMap(ids []int) (list map[int]*Topic, err error) {
idCount := len(ids) idCount := len(ids)
@ -144,7 +160,7 @@ func (s *DefaultTopicStore) BulkGetMap(ids []int) (list map[int]*Topic, err erro
} }
q = q[0 : len(q)-1] q = q[0 : len(q)-1]
rows, err := qgen.NewAcc().Select("topics").Columns("tid,title,content,createdBy,createdAt,lastReplyBy,lastReplyAt,lastReplyID,is_closed,sticky,parentID,ipaddress,views,postCount,likeCount,attachCount,poll,data").Where("tid IN(" + q + ")").Query(idList...) rows, err := qgen.NewAcc().Select("topics").Columns("tid,title,content,createdBy,createdAt,lastReplyBy,lastReplyAt,lastReplyID,is_closed,sticky,parentID,ip,views,postCount,likeCount,attachCount,poll,data").Where("tid IN(" + q + ")").Query(idList...)
if err != nil { if err != nil {
return list, err return list, err
} }
@ -197,7 +213,7 @@ func (s *DefaultTopicStore) Exists(id int) bool {
return s.exists.QueryRow(id).Scan(&id) == nil return s.exists.QueryRow(id).Scan(&id) == nil
} }
func (s *DefaultTopicStore) Create(fid int, name string, content string, uid int, ip string) (tid int, err error) { func (s *DefaultTopicStore) Create(fid int, name, content string, uid int, ip string) (tid int, err error) {
if name == "" { if name == "" {
return 0, ErrNoTitle return 0, ErrNoTitle
} }
@ -236,11 +252,16 @@ func (s *DefaultTopicStore) AddLastTopic(t *Topic, fid int) error {
// Count returns the total number of topics on these forums // Count returns the total number of topics on these forums
func (s *DefaultTopicStore) Count() (count int) { func (s *DefaultTopicStore) Count() (count int) {
err := s.count.QueryRow().Scan(&count) return Countf(s.count)
if err != nil { }
LogError(err) func (s *DefaultTopicStore) CountUser(uid int) (count int) {
} return Countf(s.countUser, uid)
return count }
func (s *DefaultTopicStore) CountMegaUser(uid int) (count int) {
return Countf(s.countWordUser, uid, SettingBox.Load().(SettingMap)["megapost_min_words"].(int))
}
func (s *DefaultTopicStore) CountBigUser(uid int) (count int) {
return Countf(s.countWordUser, uid, SettingBox.Load().(SettingMap)["bigpost_min_words"].(int))
} }
func (s *DefaultTopicStore) SetCache(cache TopicCache) { func (s *DefaultTopicStore) SetCache(cache TopicCache) {

View File

@ -12,6 +12,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
//"log"
qgen "github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
"github.com/go-sql-driver/mysql" "github.com/go-sql-driver/mysql"
@ -137,6 +138,7 @@ type UserStmts struct {
incTopics *sql.Stmt incTopics *sql.Stmt
updateLevel *sql.Stmt updateLevel *sql.Stmt
resetStats *sql.Stmt resetStats *sql.Stmt
setStats *sql.Stmt
decLiked *sql.Stmt decLiked *sql.Stmt
updateLastIP *sql.Stmt updateLastIP *sql.Stmt
@ -152,6 +154,7 @@ type UserStmts struct {
getLikedRepliesOfTopic *sql.Stmt getLikedRepliesOfTopic *sql.Stmt
getAttachmentsOfTopic *sql.Stmt getAttachmentsOfTopic *sql.Stmt
getAttachmentsOfTopic2 *sql.Stmt getAttachmentsOfTopic2 *sql.Stmt
getRepliesOfTopic *sql.Stmt
} }
var userStmts UserStmts var userStmts UserStmts
@ -168,6 +171,8 @@ func init() {
setName: acc.Update(u).Set("name=?").Where(w).Prepare(), setName: acc.Update(u).Set("name=?").Where(w).Prepare(),
update: acc.Update(u).Set("name=?,email=?,group=?").Where(w).Prepare(), // TODO: Implement user_count for users_groups on things which use this update: acc.Update(u).Set("name=?,email=?,group=?").Where(w).Prepare(), // TODO: Implement user_count for users_groups on things which use this
// Stat Statements
// TODO: Do +0 to avoid having as many statements?
incScore: acc.Update(u).Set("score=score+?").Where(w).Prepare(), incScore: acc.Update(u).Set("score=score+?").Where(w).Prepare(),
incPosts: acc.Update(u).Set("posts=posts+?").Where(w).Prepare(), incPosts: acc.Update(u).Set("posts=posts+?").Where(w).Prepare(),
incBigposts: acc.Update(u).Set("posts=posts+?,bigposts=bigposts+?").Where(w).Prepare(), incBigposts: acc.Update(u).Set("posts=posts+?,bigposts=bigposts+?").Where(w).Prepare(),
@ -178,6 +183,7 @@ func init() {
incTopics: acc.SimpleUpdate(u, "topics=topics+?", w), incTopics: acc.SimpleUpdate(u, "topics=topics+?", w),
updateLevel: acc.SimpleUpdate(u, "level=?", w), updateLevel: acc.SimpleUpdate(u, "level=?", w),
resetStats: acc.Update(u).Set("score=0,posts=0,bigposts=0,megaposts=0,topics=0,level=0").Where(w).Prepare(), resetStats: acc.Update(u).Set("score=0,posts=0,bigposts=0,megaposts=0,topics=0,level=0").Where(w).Prepare(),
setStats: acc.Update(u).Set("score=?,posts=?,bigposts=?,megaposts=?,topics=?,level=?").Where(w).Prepare(),
incLiked: acc.Update(u).Set("liked=liked+?,lastLiked=UTC_TIMESTAMP()").Where(w).Prepare(), incLiked: acc.Update(u).Set("liked=liked+?,lastLiked=UTC_TIMESTAMP()").Where(w).Prepare(),
decLiked: acc.Update(u).Set("liked=liked-?").Where(w).Prepare(), decLiked: acc.Update(u).Set("liked=liked-?").Where(w).Prepare(),
@ -189,12 +195,14 @@ func init() {
scheduleAvatarResize: acc.Insert("users_avatar_queue").Columns("uid").Fields("?").Prepare(), scheduleAvatarResize: acc.Insert("users_avatar_queue").Columns("uid").Fields("?").Prepare(),
deletePosts: acc.Select("topics").Columns("tid,parentID,poll").Where("createdBy=?").Prepare(), // Delete All Posts Statements
deleteProfilePosts: acc.Select("users_replies").Columns("rid").Where("createdBy=?").Prepare(), deletePosts: acc.Select("topics").Columns("tid,parentID,postCount,poll").Where("createdBy=?").Prepare(),
deleteProfilePosts: acc.Select("users_replies").Columns("rid,uid").Where("createdBy=?").Prepare(),
deleteReplyPosts: acc.Select("replies").Columns("rid,tid").Where("createdBy=?").Prepare(), deleteReplyPosts: acc.Select("replies").Columns("rid,tid").Where("createdBy=?").Prepare(),
getLikedRepliesOfTopic: acc.Select("replies").Columns("rid").Where("tid=? AND likeCount>0").Prepare(), getLikedRepliesOfTopic: acc.Select("replies").Columns("rid").Where("tid=? AND likeCount>0").Prepare(),
getAttachmentsOfTopic: acc.Select("attachments").Columns("attachID").Where("originID=? AND originTable='topics'").Prepare(), getAttachmentsOfTopic: acc.Select("attachments").Columns("attachID").Where("originID=? AND originTable='topics'").Prepare(),
getAttachmentsOfTopic2: acc.Select("attachments").Columns("attachID").Where("extra=? AND originTable='replies'").Prepare(), getAttachmentsOfTopic2: acc.Select("attachments").Columns("attachID").Where("extra=? AND originTable='replies'").Prepare(),
getRepliesOfTopic: acc.Select("replies").Columns("words").Where("createdBy!=? AND tid=?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -325,6 +333,7 @@ func (u *User) Delete() error {
return nil return nil
} }
// TODO: dismiss-event
func (u *User) DeletePosts() error { func (u *User) DeletePosts() error {
rows, err := userStmts.deletePosts.Query(u.ID) rows, err := userStmts.deletePosts.Query(u.ID)
if err != nil { if err != nil {
@ -336,9 +345,10 @@ func (u *User) DeletePosts() error {
updatedForums := make(map[int]int) // forum[count] updatedForums := make(map[int]int) // forum[count]
tc := Topics.GetCache() tc := Topics.GetCache()
umap := make(map[int]struct{})
for rows.Next() { for rows.Next() {
var tid, parentID, poll int var tid, parentID, postCount,poll int
err := rows.Scan(&tid, &parentID, &poll) err := rows.Scan(&tid, &parentID, &postCount, &poll)
if err != nil { if err != nil {
return err return err
} }
@ -356,15 +366,25 @@ func (u *User) DeletePosts() error {
if err != nil { if err != nil {
return err return err
} }
err = handleLikedTopicReplies(tid)
if err != nil {
return err
}
err = handleTopicAttachments(tid) err = handleTopicAttachments(tid)
if err != nil { if err != nil {
return err return err
} }
_, err = topicStmts.deleteActivitySubs.Exec(tid) if postCount > 1 {
err = handleLikedTopicReplies(tid)
if err != nil {
return err
}
err = handleTopicReplies(umap, u.ID, tid)
if err != nil {
return err
}
_, err = topicStmts.deleteReplies.Exec(tid)
if err != nil {
return err
}
}
err = Subscriptions.DeleteResource(tid,"topic")
if err != nil { if err != nil {
return err return err
} }
@ -386,6 +406,12 @@ func (u *User) DeletePosts() error {
if err != nil { if err != nil {
return err return err
} }
for uid, _ := range umap {
err = (&User{ID: uid}).RecalcPostStats()
if err != nil {
return err
}
}
for fid, count := range updatedForums { for fid, count := range updatedForums {
err := Forums.RemoveTopics(fid, count) err := Forums.RemoveTopics(fid, count)
if err != nil && err != ErrNoRows { if err != nil && err != ErrNoRows {
@ -400,8 +426,8 @@ func (u *User) DeletePosts() error {
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
var rid int var rid, uid int
err := rows.Scan(&rid) err := rows.Scan(&rid,&uid)
if err != nil { if err != nil {
return err return err
} }
@ -409,7 +435,12 @@ func (u *User) DeletePosts() error {
if err != nil { if err != nil {
return err return err
} }
// TODO: Remove alerts. // TODO: Optimise this
// TODO: dismiss-event
err = Activity.DeleteByParamsExtra("reply",uid,"user",strconv.Itoa(rid))
if err != nil {
return err
}
} }
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return err return err
@ -454,6 +485,10 @@ func (u *User) DeletePosts() error {
if err != nil { if err != nil {
return err return err
} }
err = Activity.DeleteByParamsExtra("reply",tid,"topic",strconv.Itoa(rid))
if err != nil {
return err
}
_, err = replyStmts.deleteActivitySubs.Exec(rid) _, err = replyStmts.deleteActivitySubs.Exec(rid)
if err != nil { if err != nil {
return err return err
@ -561,6 +596,42 @@ func (u *User) IncreasePostStats(wcount int, topic bool) (err error) {
return err return err
} }
func (u *User) countf(stmt *sql.Stmt) (count int) {
err := stmt.QueryRow().Scan(&count)
if err != nil {
LogError(err)
}
return count
}
func (u *User) RecalcPostStats() error {
var score int
tcount := Topics.CountUser(u.ID)
rcount := Rstore.CountUser(u.ID)
//log.Print("tcount:", tcount)
//log.Print("rcount:", rcount)
score += tcount * 2
score += rcount
var tmega, tbig, rmega, rbig int
if tcount > 0 {
tmega = Topics.CountMegaUser(u.ID)
score += tmega * 3
tbig := Topics.CountBigUser(u.ID)
score += tbig
}
if rcount > 0 {
rmega = Rstore.CountMegaUser(u.ID)
score += rmega * 3
rbig = Rstore.CountBigUser(u.ID)
score += rbig
}
_, err := userStmts.setStats.Exec(score, tcount+rcount, tbig+rbig, tmega+rmega, tcount, GetLevel(score), u.ID)
u.CacheRemove()
return err
}
func (u *User) DecreasePostStats(wcount int, topic bool) (err error) { func (u *User) DecreasePostStats(wcount int, topic bool) (err error) {
baseScore := -1 baseScore := -1
if topic { if topic {

View File

@ -21,6 +21,7 @@ type UserStore interface {
GetByName(name string) (*User, error) GetByName(name string) (*User, error)
Exists(id int) bool Exists(id int) bool
GetOffset(offset, perPage int) ([]*User, error) GetOffset(offset, perPage int) ([]*User, error)
Each(f func(*User) error) error
//BulkGet(ids []int) ([]*User, error) //BulkGet(ids []int) ([]*User, error)
BulkGetMap(ids []int) (map[int]*User, error) BulkGetMap(ids []int) (map[int]*User, error)
BypassGet(id int) (*User, error) BypassGet(id int) (*User, error)
@ -38,6 +39,7 @@ type DefaultUserStore struct {
get *sql.Stmt get *sql.Stmt
getByName *sql.Stmt getByName *sql.Stmt
getOffset *sql.Stmt getOffset *sql.Stmt
getAll *sql.Stmt
exists *sql.Stmt exists *sql.Stmt
register *sql.Stmt register *sql.Stmt
nameExists *sql.Stmt nameExists *sql.Stmt
@ -57,6 +59,7 @@ func NewDefaultUserStore(cache UserCache) (*DefaultUserStore, error) {
get: acc.Select(u).Columns("name, group, active, is_super_admin, session, email, avatar, message, level, score, posts, liked, last_ip, temp_group, enable_embeds").Where("uid = ?").Prepare(), get: acc.Select(u).Columns("name, group, active, is_super_admin, session, email, avatar, message, level, score, posts, liked, last_ip, temp_group, enable_embeds").Where("uid = ?").Prepare(),
getByName: acc.Select(u).Columns("uid, name, group, active, is_super_admin, session, email, avatar, message, level, score, posts, liked, last_ip, temp_group, enable_embeds").Where("name = ?").Prepare(), getByName: acc.Select(u).Columns("uid, name, group, active, is_super_admin, session, email, avatar, message, level, score, posts, liked, last_ip, temp_group, enable_embeds").Where("name = ?").Prepare(),
getOffset: acc.Select(u).Columns("uid, name, group, active, is_super_admin, session, email, avatar, message, level, score, posts, liked, last_ip, temp_group, enable_embeds").Orderby("uid ASC").Limit("?,?").Prepare(), getOffset: acc.Select(u).Columns("uid, name, group, active, is_super_admin, session, email, avatar, message, level, score, posts, liked, last_ip, temp_group, enable_embeds").Orderby("uid ASC").Limit("?,?").Prepare(),
getAll: acc.Select(u).Columns("uid, name, group, active, is_super_admin, session, email, avatar, message, level, score, posts, liked, last_ip, temp_group, enable_embeds").Prepare(),
exists: acc.Exists(u, "uid").Prepare(), exists: acc.Exists(u, "uid").Prepare(),
register: acc.Insert(u).Columns("name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt, lastLiked, oldestItemLikedCreatedAt").Fields("?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP()").Prepare(), // TODO: Implement user_count on users_groups here register: acc.Insert(u).Columns("name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt, lastLiked, oldestItemLikedCreatedAt").Fields("?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP()").Prepare(), // TODO: Implement user_count on users_groups here
nameExists: acc.Exists(u, "name").Prepare(), nameExists: acc.Exists(u, "name").Prepare(),
@ -143,6 +146,29 @@ func (s *DefaultUserStore) GetOffset(offset, perPage int) (users []*User, err er
} }
return users, rows.Err() return users, rows.Err()
} }
func (s *DefaultUserStore) Each(f func(*User) error) error {
rows, err := s.getAll.Query()
if err != nil {
return err
}
defer rows.Close()
var embeds int
for rows.Next() {
u := new(User)
if err := rows.Scan(&u.ID, &u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &embeds); err != nil {
return err
}
if embeds != -1 {
u.ParseSettings = DefaultParseSettings.CopyPtr()
u.ParseSettings.NoEmbed = embeds == 0
}
u.Init()
if err := f(u); err != nil {
return err
}
}
return rows.Err()
}
// TODO: Optimise the query to avoid preparing it on the spot? Maybe, use knowledge of the most common IN() parameter counts? // 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? // TODO: ID of 0 should always error?
@ -265,7 +291,7 @@ func (s *DefaultUserStore) Exists(id int) bool {
// TODO: Change active to a bool? // TODO: Change active to a bool?
// TODO: Use unique keys for the usernames // TODO: Use unique keys for the usernames
func (s *DefaultUserStore) Create(name string, password string, email string, group int, active bool) (int, error) { func (s *DefaultUserStore) Create(name, password, email string, group int, active bool) (int, error) {
// TODO: Strip spaces? // TODO: Strip spaces?
// ? This number might be a little screwy with Unicode, but it's the only consistent thing we have, as Unicode characters can be any number of bytes in theory? // ? This number might be a little screwy with Unicode, but it's the only consistent thing we have, as Unicode characters can be any number of bytes in theory?
@ -297,11 +323,7 @@ func (s *DefaultUserStore) Create(name string, password string, email string, gr
// Count returns the total number of users registered on the forums // Count returns the total number of users registered on the forums
func (s *DefaultUserStore) Count() (count int) { func (s *DefaultUserStore) Count() (count int) {
err := s.count.QueryRow().Scan(&count) return Countf(s.count)
if err != nil {
LogError(err)
}
return count
} }
func (s *DefaultUserStore) SetCache(cache UserCache) { func (s *DefaultUserStore) SetCache(cache UserCache) {

View File

@ -3,11 +3,11 @@ package common
import ( import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"strings"
"strconv" "strconv"
"strings"
"sync/atomic" "sync/atomic"
"github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
type WidgetStmts struct { type WidgetStmts struct {
@ -16,7 +16,7 @@ type WidgetStmts struct {
delete *sql.Stmt delete *sql.Stmt
create *sql.Stmt create *sql.Stmt
update *sql.Stmt update *sql.Stmt
//qgen.SimpleModel //qgen.SimpleModel
} }
@ -29,9 +29,9 @@ func init() {
//getList: acc.Select(w).Columns("wid, position, side, type, active, location, data").Orderby("position ASC").Prepare(), //getList: acc.Select(w).Columns("wid, position, side, type, active, location, data").Orderby("position ASC").Prepare(),
getDockList: acc.Select(w).Columns("wid, position, type, active, location, data").Where("side = ?").Orderby("position ASC").Prepare(), getDockList: acc.Select(w).Columns("wid, position, type, active, location, data").Where("side = ?").Orderby("position ASC").Prepare(),
//model: acc.SimpleModel(w,"position,type,active,location,data","wid"), //model: acc.SimpleModel(w,"position,type,active,location,data","wid"),
delete: acc.Delete(w).Where("wid = ?").Prepare(), delete: acc.Delete(w).Where("wid=?").Prepare(),
create: acc.Insert(w).Columns("position, side, type, active, location, data").Fields("?,?,?,?,?,?").Prepare(), create: acc.Insert(w).Columns("position, side, type, active, location, data").Fields("?,?,?,?,?,?").Prepare(),
update: acc.Update(w).Set("position = ?, side = ?, type = ?, active = ?, location = ?, data = ?").Where("wid = ?").Prepare(), update: acc.Update(w).Set("position=?,side=?,type=?,active=?,location=?,data=?").Where("wid=?").Prepare(),
} }
return acc.FirstError() return acc.FirstError()
}) })
@ -87,7 +87,7 @@ func (w *Widget) Allowed(zone string, zoneid int) bool {
if len(loc) == 0 { if len(loc) == 0 {
continue continue
} }
sloc := strings.Split(":",loc) sloc := strings.Split(":", loc)
if len(sloc) > 1 { if len(sloc) > 1 {
iloc, _ := strconv.Atoi(sloc[1]) iloc, _ := strconv.Atoi(sloc[1])
if zoneid != 0 && iloc != zoneid { if zoneid != 0 && iloc != zoneid {

View File

@ -26,9 +26,9 @@ type WordFilterStore interface {
ReloadAll() error ReloadAll() error
GetAll() (filters map[int]*WordFilter, err error) GetAll() (filters map[int]*WordFilter, err error)
Get(id int) (*WordFilter, error) Get(id int) (*WordFilter, error)
Create(find string, replace string) (int, error) Create(find, replace string) (int, error)
Delete(id int) error Delete(id int) error
Update(id int, find string, replace string) error Update(id int, find, replace string) error
Length() int Length() int
EstCount() int EstCount() int
Count() (count int) Count() (count int)
@ -49,10 +49,10 @@ func NewDefaultWordFilterStore(acc *qgen.Accumulator) (*DefaultWordFilterStore,
wf := "word_filters" wf := "word_filters"
store := &DefaultWordFilterStore{ store := &DefaultWordFilterStore{
getAll: acc.Select(wf).Columns("wfid,find,replacement").Prepare(), getAll: acc.Select(wf).Columns("wfid,find,replacement").Prepare(),
get: acc.Select(wf).Columns("find,replacement").Where("wfid = ?").Prepare(), get: acc.Select(wf).Columns("find,replacement").Where("wfid=?").Prepare(),
create: acc.Insert(wf).Columns("find,replacement").Fields("?,?").Prepare(), create: acc.Insert(wf).Columns("find,replacement").Fields("?,?").Prepare(),
delete: acc.Delete(wf).Where("wfid = ?").Prepare(), delete: acc.Delete(wf).Where("wfid=?").Prepare(),
update: acc.Update(wf).Set("find = ?, replacement = ?").Where("wfid = ?").Prepare(), update: acc.Update(wf).Set("find=?,replacement=?").Where("wfid=?").Prepare(),
count: acc.Count(wf).Prepare(), count: acc.Count(wf).Prepare(),
} }
// TODO: Should we initialise this elsewhere? // TODO: Should we initialise this elsewhere?
@ -109,7 +109,7 @@ func (s *DefaultWordFilterStore) Get(id int) (*WordFilter, error) {
} }
// Create adds a new word filter to the database and refreshes the memory cache // Create adds a new word filter to the database and refreshes the memory cache
func (s *DefaultWordFilterStore) Create(find string, replace string) (int, error) { func (s *DefaultWordFilterStore) Create(find, replace string) (int, error) {
res, err := s.create.Exec(find, replace) res, err := s.create.Exec(find, replace)
if err != nil { if err != nil {
return 0, err return 0, err
@ -130,7 +130,7 @@ func (s *DefaultWordFilterStore) Delete(id int) error {
return s.ReloadAll() return s.ReloadAll()
} }
func (s *DefaultWordFilterStore) Update(id int, find string, replace string) error { func (s *DefaultWordFilterStore) Update(id int, find, replace string) error {
_, err := s.update.Exec(find, replace, id) _, err := s.update.Exec(find, replace, id)
if err != nil { if err != nil {
return err return err

View File

@ -158,6 +158,7 @@ var RouteMap = map[string]interface{}{
"routes.UnlockTopicSubmit": routes.UnlockTopicSubmit, "routes.UnlockTopicSubmit": routes.UnlockTopicSubmit,
"routes.MoveTopicSubmit": routes.MoveTopicSubmit, "routes.MoveTopicSubmit": routes.MoveTopicSubmit,
"routes.LikeTopicSubmit": routes.LikeTopicSubmit, "routes.LikeTopicSubmit": routes.LikeTopicSubmit,
"routes.UnlikeTopicSubmit": routes.UnlikeTopicSubmit,
"routes.AddAttachToTopicSubmit": routes.AddAttachToTopicSubmit, "routes.AddAttachToTopicSubmit": routes.AddAttachToTopicSubmit,
"routes.RemoveAttachFromTopicSubmit": routes.RemoveAttachFromTopicSubmit, "routes.RemoveAttachFromTopicSubmit": routes.RemoveAttachFromTopicSubmit,
"routes.ViewTopic": routes.ViewTopic, "routes.ViewTopic": routes.ViewTopic,
@ -330,39 +331,40 @@ var routeMapEnum = map[string]int{
"routes.UnlockTopicSubmit": 132, "routes.UnlockTopicSubmit": 132,
"routes.MoveTopicSubmit": 133, "routes.MoveTopicSubmit": 133,
"routes.LikeTopicSubmit": 134, "routes.LikeTopicSubmit": 134,
"routes.AddAttachToTopicSubmit": 135, "routes.UnlikeTopicSubmit": 135,
"routes.RemoveAttachFromTopicSubmit": 136, "routes.AddAttachToTopicSubmit": 136,
"routes.ViewTopic": 137, "routes.RemoveAttachFromTopicSubmit": 137,
"routes.CreateReplySubmit": 138, "routes.ViewTopic": 138,
"routes.ReplyEditSubmit": 139, "routes.CreateReplySubmit": 139,
"routes.ReplyDeleteSubmit": 140, "routes.ReplyEditSubmit": 140,
"routes.ReplyLikeSubmit": 141, "routes.ReplyDeleteSubmit": 141,
"routes.AddAttachToReplySubmit": 142, "routes.ReplyLikeSubmit": 142,
"routes.RemoveAttachFromReplySubmit": 143, "routes.AddAttachToReplySubmit": 143,
"routes.ProfileReplyCreateSubmit": 144, "routes.RemoveAttachFromReplySubmit": 144,
"routes.ProfileReplyEditSubmit": 145, "routes.ProfileReplyCreateSubmit": 145,
"routes.ProfileReplyDeleteSubmit": 146, "routes.ProfileReplyEditSubmit": 146,
"routes.PollVote": 147, "routes.ProfileReplyDeleteSubmit": 147,
"routes.PollResults": 148, "routes.PollVote": 148,
"routes.AccountLogin": 149, "routes.PollResults": 149,
"routes.AccountRegister": 150, "routes.AccountLogin": 150,
"routes.AccountLogout": 151, "routes.AccountRegister": 151,
"routes.AccountLoginSubmit": 152, "routes.AccountLogout": 152,
"routes.AccountLoginMFAVerify": 153, "routes.AccountLoginSubmit": 153,
"routes.AccountLoginMFAVerifySubmit": 154, "routes.AccountLoginMFAVerify": 154,
"routes.AccountRegisterSubmit": 155, "routes.AccountLoginMFAVerifySubmit": 155,
"routes.AccountPasswordReset": 156, "routes.AccountRegisterSubmit": 156,
"routes.AccountPasswordResetSubmit": 157, "routes.AccountPasswordReset": 157,
"routes.AccountPasswordResetToken": 158, "routes.AccountPasswordResetSubmit": 158,
"routes.AccountPasswordResetTokenSubmit": 159, "routes.AccountPasswordResetToken": 159,
"routes.DynamicRoute": 160, "routes.AccountPasswordResetTokenSubmit": 160,
"routes.UploadedFile": 161, "routes.DynamicRoute": 161,
"routes.StaticFile": 162, "routes.UploadedFile": 162,
"routes.RobotsTxt": 163, "routes.StaticFile": 163,
"routes.SitemapXml": 164, "routes.RobotsTxt": 164,
"routes.OpenSearchXml": 165, "routes.SitemapXml": 165,
"routes.BadRoute": 166, "routes.OpenSearchXml": 166,
"routes.HTTPSRedirect": 167, "routes.BadRoute": 167,
"routes.HTTPSRedirect": 168,
} }
var reverseRouteMapEnum = map[int]string{ var reverseRouteMapEnum = map[int]string{
0: "routes.Overview", 0: "routes.Overview",
@ -500,39 +502,40 @@ var reverseRouteMapEnum = map[int]string{
132: "routes.UnlockTopicSubmit", 132: "routes.UnlockTopicSubmit",
133: "routes.MoveTopicSubmit", 133: "routes.MoveTopicSubmit",
134: "routes.LikeTopicSubmit", 134: "routes.LikeTopicSubmit",
135: "routes.AddAttachToTopicSubmit", 135: "routes.UnlikeTopicSubmit",
136: "routes.RemoveAttachFromTopicSubmit", 136: "routes.AddAttachToTopicSubmit",
137: "routes.ViewTopic", 137: "routes.RemoveAttachFromTopicSubmit",
138: "routes.CreateReplySubmit", 138: "routes.ViewTopic",
139: "routes.ReplyEditSubmit", 139: "routes.CreateReplySubmit",
140: "routes.ReplyDeleteSubmit", 140: "routes.ReplyEditSubmit",
141: "routes.ReplyLikeSubmit", 141: "routes.ReplyDeleteSubmit",
142: "routes.AddAttachToReplySubmit", 142: "routes.ReplyLikeSubmit",
143: "routes.RemoveAttachFromReplySubmit", 143: "routes.AddAttachToReplySubmit",
144: "routes.ProfileReplyCreateSubmit", 144: "routes.RemoveAttachFromReplySubmit",
145: "routes.ProfileReplyEditSubmit", 145: "routes.ProfileReplyCreateSubmit",
146: "routes.ProfileReplyDeleteSubmit", 146: "routes.ProfileReplyEditSubmit",
147: "routes.PollVote", 147: "routes.ProfileReplyDeleteSubmit",
148: "routes.PollResults", 148: "routes.PollVote",
149: "routes.AccountLogin", 149: "routes.PollResults",
150: "routes.AccountRegister", 150: "routes.AccountLogin",
151: "routes.AccountLogout", 151: "routes.AccountRegister",
152: "routes.AccountLoginSubmit", 152: "routes.AccountLogout",
153: "routes.AccountLoginMFAVerify", 153: "routes.AccountLoginSubmit",
154: "routes.AccountLoginMFAVerifySubmit", 154: "routes.AccountLoginMFAVerify",
155: "routes.AccountRegisterSubmit", 155: "routes.AccountLoginMFAVerifySubmit",
156: "routes.AccountPasswordReset", 156: "routes.AccountRegisterSubmit",
157: "routes.AccountPasswordResetSubmit", 157: "routes.AccountPasswordReset",
158: "routes.AccountPasswordResetToken", 158: "routes.AccountPasswordResetSubmit",
159: "routes.AccountPasswordResetTokenSubmit", 159: "routes.AccountPasswordResetToken",
160: "routes.DynamicRoute", 160: "routes.AccountPasswordResetTokenSubmit",
161: "routes.UploadedFile", 161: "routes.DynamicRoute",
162: "routes.StaticFile", 162: "routes.UploadedFile",
163: "routes.RobotsTxt", 163: "routes.StaticFile",
164: "routes.SitemapXml", 164: "routes.RobotsTxt",
165: "routes.OpenSearchXml", 165: "routes.SitemapXml",
166: "routes.BadRoute", 166: "routes.OpenSearchXml",
167: "routes.HTTPSRedirect", 167: "routes.BadRoute",
168: "routes.HTTPSRedirect",
} }
var osMapEnum = map[string]int{ var osMapEnum = map[string]int{
"unknown": 0, "unknown": 0,
@ -690,7 +693,7 @@ type HTTPSRedirect struct {}
func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Connection", "close") w.Header().Set("Connection", "close")
co.RouteViewCounter.Bump(167) co.RouteViewCounter.Bump(168)
dest := "https://" + req.Host + req.URL.String() dest := "https://" + req.Host + req.URL.String()
http.Redirect(w, req, dest, http.StatusTemporaryRedirect) http.Redirect(w, req, dest, http.StatusTemporaryRedirect)
} }
@ -898,7 +901,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
co.GlobalViewCounter.Bump() co.GlobalViewCounter.Bump()
if prefix == "/s" { //old prefix: /static if prefix == "/s" { //old prefix: /static
co.RouteViewCounter.Bump(162) co.RouteViewCounter.Bump(163)
req.URL.Path += extraData req.URL.Path += extraData
routes.StaticFile(w, req) routes.StaticFile(w, req)
return return
@ -2274,6 +2277,19 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
co.RouteViewCounter.Bump(134) co.RouteViewCounter.Bump(134)
err = routes.LikeTopicSubmit(w,req,user,extraData) err = routes.LikeTopicSubmit(w,req,user,extraData)
case "/topic/unlike/submit/":
err = c.NoSessionMismatch(w,req,user)
if err != nil {
return err
}
err = c.MemberOnly(w,req,user)
if err != nil {
return err
}
co.RouteViewCounter.Bump(135)
err = routes.UnlikeTopicSubmit(w,req,user,extraData)
case "/topic/attach/add/submit/": case "/topic/attach/add/submit/":
err = c.MemberOnly(w,req,user) err = c.MemberOnly(w,req,user)
if err != nil { if err != nil {
@ -2289,7 +2305,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(135) co.RouteViewCounter.Bump(136)
err = routes.AddAttachToTopicSubmit(w,req,user,extraData) err = routes.AddAttachToTopicSubmit(w,req,user,extraData)
case "/topic/attach/remove/submit/": case "/topic/attach/remove/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2302,10 +2318,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(136) co.RouteViewCounter.Bump(137)
err = routes.RemoveAttachFromTopicSubmit(w,req,user,extraData) err = routes.RemoveAttachFromTopicSubmit(w,req,user,extraData)
default: default:
co.RouteViewCounter.Bump(137) co.RouteViewCounter.Bump(138)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -2329,7 +2345,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(138) co.RouteViewCounter.Bump(139)
err = routes.CreateReplySubmit(w,req,user) err = routes.CreateReplySubmit(w,req,user)
case "/reply/edit/submit/": case "/reply/edit/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2342,7 +2358,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(139) co.RouteViewCounter.Bump(140)
err = routes.ReplyEditSubmit(w,req,user,extraData) err = routes.ReplyEditSubmit(w,req,user,extraData)
case "/reply/delete/submit/": case "/reply/delete/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2355,7 +2371,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(140) co.RouteViewCounter.Bump(141)
err = routes.ReplyDeleteSubmit(w,req,user,extraData) err = routes.ReplyDeleteSubmit(w,req,user,extraData)
case "/reply/like/submit/": case "/reply/like/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2368,7 +2384,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(141) co.RouteViewCounter.Bump(142)
err = routes.ReplyLikeSubmit(w,req,user,extraData) err = routes.ReplyLikeSubmit(w,req,user,extraData)
case "/reply/attach/add/submit/": case "/reply/attach/add/submit/":
err = c.MemberOnly(w,req,user) err = c.MemberOnly(w,req,user)
@ -2385,7 +2401,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(142) co.RouteViewCounter.Bump(143)
err = routes.AddAttachToReplySubmit(w,req,user,extraData) err = routes.AddAttachToReplySubmit(w,req,user,extraData)
case "/reply/attach/remove/submit/": case "/reply/attach/remove/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2398,7 +2414,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(143) co.RouteViewCounter.Bump(144)
err = routes.RemoveAttachFromReplySubmit(w,req,user,extraData) err = routes.RemoveAttachFromReplySubmit(w,req,user,extraData)
} }
case "/profile": case "/profile":
@ -2414,7 +2430,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(144) co.RouteViewCounter.Bump(145)
err = routes.ProfileReplyCreateSubmit(w,req,user) err = routes.ProfileReplyCreateSubmit(w,req,user)
case "/profile/reply/edit/submit/": case "/profile/reply/edit/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2427,7 +2443,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(145) co.RouteViewCounter.Bump(146)
err = routes.ProfileReplyEditSubmit(w,req,user,extraData) err = routes.ProfileReplyEditSubmit(w,req,user,extraData)
case "/profile/reply/delete/submit/": case "/profile/reply/delete/submit/":
err = c.NoSessionMismatch(w,req,user) err = c.NoSessionMismatch(w,req,user)
@ -2440,7 +2456,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(146) co.RouteViewCounter.Bump(147)
err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData) err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData)
} }
case "/poll": case "/poll":
@ -2456,23 +2472,23 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(147) co.RouteViewCounter.Bump(148)
err = routes.PollVote(w,req,user,extraData) err = routes.PollVote(w,req,user,extraData)
case "/poll/results/": case "/poll/results/":
co.RouteViewCounter.Bump(148) co.RouteViewCounter.Bump(149)
err = routes.PollResults(w,req,user,extraData) err = routes.PollResults(w,req,user,extraData)
} }
case "/accounts": case "/accounts":
switch(req.URL.Path) { switch(req.URL.Path) {
case "/accounts/login/": case "/accounts/login/":
co.RouteViewCounter.Bump(149) co.RouteViewCounter.Bump(150)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
} }
err = routes.AccountLogin(w,req,user,head) err = routes.AccountLogin(w,req,user,head)
case "/accounts/create/": case "/accounts/create/":
co.RouteViewCounter.Bump(150) co.RouteViewCounter.Bump(151)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -2489,7 +2505,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(151) co.RouteViewCounter.Bump(152)
err = routes.AccountLogout(w,req,user) err = routes.AccountLogout(w,req,user)
case "/accounts/login/submit/": case "/accounts/login/submit/":
err = c.ParseForm(w,req,user) err = c.ParseForm(w,req,user)
@ -2497,10 +2513,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(152) co.RouteViewCounter.Bump(153)
err = routes.AccountLoginSubmit(w,req,user) err = routes.AccountLoginSubmit(w,req,user)
case "/accounts/mfa_verify/": case "/accounts/mfa_verify/":
co.RouteViewCounter.Bump(153) co.RouteViewCounter.Bump(154)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -2512,7 +2528,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(154) co.RouteViewCounter.Bump(155)
err = routes.AccountLoginMFAVerifySubmit(w,req,user) err = routes.AccountLoginMFAVerifySubmit(w,req,user)
case "/accounts/create/submit/": case "/accounts/create/submit/":
err = c.ParseForm(w,req,user) err = c.ParseForm(w,req,user)
@ -2520,10 +2536,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(155) co.RouteViewCounter.Bump(156)
err = routes.AccountRegisterSubmit(w,req,user) err = routes.AccountRegisterSubmit(w,req,user)
case "/accounts/password-reset/": case "/accounts/password-reset/":
co.RouteViewCounter.Bump(156) co.RouteViewCounter.Bump(157)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -2535,10 +2551,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(157) co.RouteViewCounter.Bump(158)
err = routes.AccountPasswordResetSubmit(w,req,user) err = routes.AccountPasswordResetSubmit(w,req,user)
case "/accounts/password-reset/token/": case "/accounts/password-reset/token/":
co.RouteViewCounter.Bump(158) co.RouteViewCounter.Bump(159)
head, err := c.UserCheck(w,req,&user) head, err := c.UserCheck(w,req,&user)
if err != nil { if err != nil {
return err return err
@ -2550,7 +2566,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
return err return err
} }
co.RouteViewCounter.Bump(159) co.RouteViewCounter.Bump(160)
err = routes.AccountPasswordResetTokenSubmit(w,req,user) err = routes.AccountPasswordResetTokenSubmit(w,req,user)
} }
/*case "/sitemaps": // TODO: Count these views /*case "/sitemaps": // TODO: Count these views
@ -2567,7 +2583,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
h.Del("Content-Type") h.Del("Content-Type")
h.Del("Content-Encoding") h.Del("Content-Encoding")
} }
co.RouteViewCounter.Bump(161) co.RouteViewCounter.Bump(162)
req.URL.Path += extraData req.URL.Path += extraData
// TODO: Find a way to propagate errors up from this? // TODO: Find a way to propagate errors up from this?
r.UploadHandler(w,req) // TODO: Count these views r.UploadHandler(w,req) // TODO: Count these views
@ -2577,7 +2593,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
// TODO: Add support for favicons and robots.txt files // TODO: Add support for favicons and robots.txt files
switch(extraData) { switch(extraData) {
case "robots.txt": case "robots.txt":
co.RouteViewCounter.Bump(163) co.RouteViewCounter.Bump(164)
return routes.RobotsTxt(w,req) return routes.RobotsTxt(w,req)
case "favicon.ico": case "favicon.ico":
gzw, ok := w.(c.GzipResponseWriter) gzw, ok := w.(c.GzipResponseWriter)
@ -2591,10 +2607,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
routes.StaticFile(w,req) routes.StaticFile(w,req)
return nil return nil
case "opensearch.xml": case "opensearch.xml":
co.RouteViewCounter.Bump(165) co.RouteViewCounter.Bump(166)
return routes.OpenSearchXml(w,req) return routes.OpenSearchXml(w,req)
/*case "sitemap.xml": /*case "sitemap.xml":
co.RouteViewCounter.Bump(164) co.RouteViewCounter.Bump(165)
return routes.SitemapXml(w,req)*/ return routes.SitemapXml(w,req)*/
} }
return c.NotFound(w,req,nil) return c.NotFound(w,req,nil)
@ -2605,7 +2621,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
r.RUnlock() r.RUnlock()
if ok { if ok {
co.RouteViewCounter.Bump(160) // TODO: Be more specific about *which* dynamic route it is co.RouteViewCounter.Bump(161) // TODO: Be more specific about *which* dynamic route it is
req.URL.Path += extraData req.URL.Path += extraData
return handle(w,req,user) return handle(w,req,user)
} }
@ -2616,7 +2632,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c
} else { } else {
r.DumpRequest(req,"Bad Route") r.DumpRequest(req,"Bad Route")
} }
co.RouteViewCounter.Bump(166) co.RouteViewCounter.Bump(167)
return c.NotFound(w,req,nil) return c.NotFound(w,req,nil)
} }
return err return err

View File

@ -791,7 +791,7 @@ func BenchmarkQueryTopicParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
var tu c.TopicUser var tu c.TopicUser
for pb.Next() { for pb.Next() {
err := db.QueryRow("select topics.title, topics.content, topics.createdBy, topics.createdAt, topics.is_closed, topics.sticky, topics.parentID, topics.ipaddress, topics.views, topics.postCount, topics.likeCount, users.name, users.avatar, users.group, users.level from topics left join users ON topics.createdBy = users.uid where tid = ?", 1).Scan(&tu.Title, &tu.Content, &tu.CreatedBy, &tu.CreatedAt, &tu.IsClosed, &tu.Sticky, &tu.ParentID, &tu.IP, &tu.ViewCount, &tu.PostCount, &tu.LikeCount, &tu.CreatedByName, &tu.Avatar, &tu.Group, &tu.Level) err := db.QueryRow("select topics.title, topics.content, topics.createdBy, topics.createdAt, topics.is_closed, topics.sticky, topics.parentID, topics.ip, topics.views, topics.postCount, topics.likeCount, users.name, users.avatar, users.group, users.level from topics left join users ON topics.createdBy = users.uid where tid = ?", 1).Scan(&tu.Title, &tu.Content, &tu.CreatedBy, &tu.CreatedAt, &tu.IsClosed, &tu.Sticky, &tu.ParentID, &tu.IP, &tu.ViewCount, &tu.PostCount, &tu.LikeCount, &tu.CreatedByName, &tu.Avatar, &tu.Group, &tu.Level)
if err == ErrNoRows { if err == ErrNoRows {
log.Fatal("No rows found!") log.Fatal("No rows found!")
return return
@ -812,7 +812,7 @@ func BenchmarkQueryPreparedTopicParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
var tu c.TopicUser var tu c.TopicUser
getTopicUser, err := qgen.Builder.SimpleLeftJoin("topics", "users", "topics.title, topics.content, topics.createdBy, topics.createdAt, topics.is_closed, topics.sticky, topics.parentID, topics.ipaddress, topics.postCount, topics.likeCount, users.name, users.avatar, users.group, users.level", "topics.createdBy = users.uid", "tid = ?", "", "") getTopicUser, err := qgen.Builder.SimpleLeftJoin("topics", "users", "topics.title, topics.content, topics.createdBy, topics.createdAt, topics.is_closed, topics.sticky, topics.parentID, topics.ip, topics.postCount, topics.likeCount, users.name, users.avatar, users.group, users.level", "topics.createdBy = users.uid", "tid = ?", "", "")
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
@ -873,7 +873,7 @@ func BenchmarkQueriesSerial(b *testing.B) {
var tu c.TopicUser var tu c.TopicUser
b.Run("topic", func(b *testing.B) { b.Run("topic", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
err := db.QueryRow("select topics.title, topics.content, topics.createdBy, topics.createdAt, topics.is_closed, topics.sticky, topics.parentID, topics.ipaddress, topics.postCount, topics.likeCount, users.name, users.avatar, users.group, users.level from topics left join users ON topics.createdBy = users.uid where tid = ?", 1).Scan(&tu.Title, &tu.Content, &tu.CreatedBy, &tu.CreatedAt, &tu.IsClosed, &tu.Sticky, &tu.ParentID, &tu.IP, &tu.PostCount, &tu.LikeCount, &tu.CreatedByName, &tu.Avatar, &tu.Group, &tu.Level) err := db.QueryRow("select topics.title, topics.content, topics.createdBy, topics.createdAt, topics.is_closed, topics.sticky, topics.parentID, topics.ip, topics.postCount, topics.likeCount, users.name, users.avatar, users.group, users.level from topics left join users ON topics.createdBy = users.uid where tid = ?", 1).Scan(&tu.Title, &tu.Content, &tu.CreatedBy, &tu.CreatedAt, &tu.IsClosed, &tu.Sticky, &tu.ParentID, &tu.IP, &tu.PostCount, &tu.LikeCount, &tu.CreatedByName, &tu.Avatar, &tu.Group, &tu.Level)
if err == ErrNoRows { if err == ErrNoRows {
b.Fatal("No rows found!") b.Fatal("No rows found!")
return return
@ -885,7 +885,7 @@ func BenchmarkQueriesSerial(b *testing.B) {
}) })
b.Run("topic_replies", func(b *testing.B) { b.Run("topic_replies", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
rows, err := db.Query("select replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.avatar, users.name, users.is_super_admin, users.group, users.level, replies.ipaddress from replies left join users ON replies.createdBy = users.uid where tid = ?", 1) rows, err := db.Query("select replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.avatar, users.name, users.is_super_admin, users.group, users.level, replies.ip from replies left join users ON replies.createdBy = users.uid where tid = ?", 1)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
return return
@ -907,7 +907,7 @@ func BenchmarkQueriesSerial(b *testing.B) {
var group int var group int
b.Run("topic_replies_scan", func(b *testing.B) { b.Run("topic_replies_scan", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
rows, err := db.Query("select replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.avatar, users.name, users.is_super_admin, users.group, users.level, replies.ipaddress from replies left join users ON replies.createdBy = users.uid where tid = ?", 1) rows, err := db.Query("select replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.avatar, users.name, users.is_super_admin, users.group, users.level, replies.ip from replies left join users ON replies.createdBy = users.uid where tid = ?", 1)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
return return

13
main.go
View File

@ -28,6 +28,7 @@ import (
_ "github.com/Azareal/Gosora/extend" _ "github.com/Azareal/Gosora/extend"
co "github.com/Azareal/Gosora/common/counters" co "github.com/Azareal/Gosora/common/counters"
p "github.com/Azareal/Gosora/common/phrases" p "github.com/Azareal/Gosora/common/phrases"
meta "github.com/Azareal/Gosora/common/meta"
"github.com/Azareal/Gosora/query_gen" "github.com/Azareal/Gosora/query_gen"
"github.com/Azareal/Gosora/routes" "github.com/Azareal/Gosora/routes"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
@ -264,6 +265,10 @@ func storeInit() (err error) {
} }
// TODO: Let the admin choose other thumbnailers, maybe ones defined in plugins // TODO: Let the admin choose other thumbnailers, maybe ones defined in plugins
c.Thumbnailer = c.NewCaireThumbnailer() c.Thumbnailer = c.NewCaireThumbnailer()
c.Recalc, err = c.NewDefaultRecalc(acc)
if err != nil {
return errors.WithStack(err)
}
log.Print("Initialising the view counters") log.Print("Initialising the view counters")
co.GlobalViewCounter, err = co.NewGlobalViewCounter(acc) co.GlobalViewCounter, err = co.NewGlobalViewCounter(acc)
@ -312,7 +317,7 @@ func storeInit() (err error) {
} }
log.Print("Initialising the meta store") log.Print("Initialising the meta store")
c.Meta, err = c.NewDefaultMetaStore(acc) c.Meta, err = meta.NewDefaultMetaStore(acc)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -482,6 +487,12 @@ func main() {
} }
} }
log.Print("Checking for init tasks")
err = sched()
if err != nil {
c.LogError(err)
}
log.Print("Initialising the task system") log.Print("Initialising the task system")
// Thumbnailer goroutine, we only want one image being thumbnailed at a time, otherwise they might wind up consuming all the CPU time and leave no resources left to service the actual requests // Thumbnailer goroutine, we only want one image being thumbnailed at a time, otherwise they might wind up consuming all the CPU time and leave no resources left to service the actual requests

View File

@ -917,7 +917,7 @@ func TestReplyStore(t *testing.T) {
testReplyStore(t, 5, 3, "0") testReplyStore(t, 5, 3, "0")
} }
func testReplyStore(t *testing.T, newID int, newPostCount int, ip string) { func testReplyStore(t *testing.T, newID, newPostCount int, ip string) {
replyTest2 := func(reply *c.Reply, err error, rid int, parentID int, createdBy int, content string, ip string) { replyTest2 := func(reply *c.Reply, err error, rid int, parentID int, createdBy int, content string, ip string) {
expectNilErr(t, err) expectNilErr(t, err)
expect(t, reply.ID == rid, fmt.Sprintf("RID #%d has the wrong ID. It should be %d not %d", rid, rid, reply.ID)) expect(t, reply.ID == rid, fmt.Sprintf("RID #%d has the wrong ID. It should be %d not %d", rid, rid, reply.ID))
@ -1028,20 +1028,20 @@ func testProfileReplyStore(t *testing.T, newID int, ip string) {
expectNilErr(t, err) expectNilErr(t, err)
expect(t, prid == newID, fmt.Sprintf("The first profile reply should have an ID of %d", newID)) expect(t, prid == newID, fmt.Sprintf("The first profile reply should have an ID of %d", newID))
profileReply, err := c.Prstore.Get(newID) pr, err := c.Prstore.Get(newID)
expectNilErr(t, err) expectNilErr(t, err)
expect(t, profileReply.ID == newID, fmt.Sprintf("The profile reply should have an ID of %d not %d", newID, profileReply.ID)) expect(t, pr.ID == newID, fmt.Sprintf("The profile reply should have an ID of %d not %d", newID, pr.ID))
expect(t, profileReply.ParentID == 1, fmt.Sprintf("The parent ID of the profile reply should be 1 not %d", profileReply.ParentID)) expect(t, pr.ParentID == 1, fmt.Sprintf("The parent ID of the profile reply should be 1 not %d", pr.ParentID))
expect(t, profileReply.Content == "Haha", fmt.Sprintf("The profile reply's contents should be 'Haha' not '%s'", profileReply.Content)) expect(t, pr.Content == "Haha", fmt.Sprintf("The profile reply's contents should be 'Haha' not '%s'", pr.Content))
expect(t, profileReply.CreatedBy == 1, fmt.Sprintf("The profile reply's creator should be 1 not %d", profileReply.CreatedBy)) expect(t, pr.CreatedBy == 1, fmt.Sprintf("The profile reply's creator should be 1 not %d", pr.CreatedBy))
expect(t, profileReply.IP == ip, fmt.Sprintf("The profile reply's IP should be '%s' not '%s'", ip, profileReply.IP)) expect(t, pr.IP == ip, fmt.Sprintf("The profile reply's IP should be '%s' not '%s'", ip, pr.IP))
err = profileReply.Delete() err = pr.Delete()
expectNilErr(t, err) expectNilErr(t, err)
_, err = c.Prstore.Get(newID) _, err = c.Prstore.Get(newID)
expect(t, err != nil, fmt.Sprintf("PRID #%d shouldn't exist after being deleted", newID)) expect(t, err != nil, fmt.Sprintf("PRID #%d shouldn't exist after being deleted", newID))
// TODO: Test profileReply.SetBody() and profileReply.Creator() // TODO: Test pr.SetBody() and pr.Creator()
} }
func TestActivityStream(t *testing.T) { func TestActivityStream(t *testing.T) {

View File

@ -4,7 +4,10 @@ import (
"bufio" "bufio"
"database/sql" "database/sql"
"strconv" "strconv"
"strings"
"unicode"
meta "github.com/Azareal/Gosora/common/meta"
qgen "github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
) )
@ -43,6 +46,7 @@ func init() {
addPatch(26, patch26) addPatch(26, patch26)
addPatch(27, patch27) addPatch(27, patch27)
addPatch(28, patch28) addPatch(28, patch28)
addPatch(29, patch29)
} }
func patch0(scanner *bufio.Scanner) (err error) { func patch0(scanner *bufio.Scanner) (err error) {
@ -749,4 +753,108 @@ func patch27(scanner *bufio.Scanner) error {
func patch28(scanner *bufio.Scanner) error { func patch28(scanner *bufio.Scanner) error {
return execStmt(qgen.Builder.AddColumn("users", tC{"enable_embeds", "int", 0, false, false, "-1"}, nil)) return execStmt(qgen.Builder.AddColumn("users", tC{"enable_embeds", "int", 0, false, false, "-1"}, nil))
} }
// The word counter might run into problems with some languages where words aren't as obviously demarcated, I would advise turning it off in those cases, or if it becomes annoying in general, really.
func WordCount(input string) (count int) {
input = strings.TrimSpace(input)
if input == "" {
return 0
}
var inSpace bool
for _, value := range input {
if unicode.IsSpace(value) || unicode.IsPunct(value) {
if !inSpace {
inSpace = true
}
} else if inSpace {
count++
inSpace = false
}
}
return count + 1
}
func patch29(scanner *bufio.Scanner) error {
f := func(tbl, idCol string) error {
return acc().Select(tbl).Cols(idCol + ",content").Each(func(rows *sql.Rows) error {
var id int
var content string
err := rows.Scan(&id, &content)
if err != nil {
return err
}
_, err = acc().Update(tbl).Set("words=?").Where(idCol+"=?").Exec(WordCount(content), id)
return err
})
}
err := f("topics", "tid")
if err != nil {
return err
}
err = f("replies", "rid")
if err != nil {
return err
}
meta, err := meta.NewDefaultMetaStore(acc())
if err != nil {
return err
}
err = meta.Set("sched", "recalc")
if err != nil {
return err
}
fixCol := func(tbl string) error {
//err := execStmt(qgen.Builder.RenameColumn(tbl, "ipaddress","ip"))
err := execStmt(qgen.Builder.ChangeColumn(tbl, "ipaddress", tC{"ip", "varchar", 200, false, false, "''"}))
if err != nil {
return err
}
return execStmt(qgen.Builder.SetDefaultColumn(tbl, "ip", "varchar", ""))
}
err = fixCol("topics")
if err != nil {
return err
}
err = fixCol("replies")
if err != nil {
return err
}
err = fixCol("polls_votes")
if err != nil {
return err
}
err = fixCol("users_replies")
if err != nil {
return err
}
err = execStmt(qgen.Builder.SetDefaultColumn("users", "last_ip", "varchar", ""))
if err != nil {
return err
}
err = execStmt(qgen.Builder.SetDefaultColumn("replies", "lastEdit", "int", "0"))
if err != nil {
return err
}
err = execStmt(qgen.Builder.SetDefaultColumn("replies", "lastEditBy", "int", "0"))
if err != nil {
return err
}
err = execStmt(qgen.Builder.SetDefaultColumn("users_replies", "lastEdit", "int", "0"))
if err != nil {
return err
}
err = execStmt(qgen.Builder.SetDefaultColumn("users_replies", "lastEditBy", "int", "0"))
if err != nil {
return err
}
return execStmt(qgen.Builder.AddColumn("activity_stream", tC{"extra", "varchar", 200, false, false, "''"}, nil))
}

View File

@ -18,58 +18,58 @@ type builder struct {
adapter Adapter adapter Adapter
} }
func (build *builder) Accumulator() *Accumulator { func (b *builder) Accumulator() *Accumulator {
return &Accumulator{build.conn, build.adapter, nil} return &Accumulator{b.conn, b.adapter, nil}
} }
// TODO: Move this method out of builder? // TODO: Move this method out of builder?
func (build *builder) Init(adapter string, config map[string]string) error { func (b *builder) Init(adapter string, config map[string]string) error {
err := build.SetAdapter(adapter) err := b.SetAdapter(adapter)
if err != nil { if err != nil {
return err return err
} }
conn, err := build.adapter.BuildConn(config) conn, err := b.adapter.BuildConn(config)
build.conn = conn b.conn = conn
log.Print("err: ", err) // Is the problem here somehow? log.Print("err:", err) // Is the problem here somehow?
return err return err
} }
func (build *builder) SetConn(conn *sql.DB) { func (b *builder) SetConn(conn *sql.DB) {
build.conn = conn b.conn = conn
} }
func (build *builder) GetConn() *sql.DB { func (b *builder) GetConn() *sql.DB {
return build.conn return b.conn
} }
func (build *builder) SetAdapter(name string) error { func (b *builder) SetAdapter(name string) error {
adap, err := GetAdapter(name) adap, err := GetAdapter(name)
if err != nil { if err != nil {
return err return err
} }
build.adapter = adap b.adapter = adap
return nil return nil
} }
func (build *builder) GetAdapter() Adapter { func (b *builder) GetAdapter() Adapter {
return build.adapter return b.adapter
} }
func (build *builder) DbVersion() (dbVersion string) { func (b *builder) DbVersion() (dbVersion string) {
build.conn.QueryRow(build.adapter.DbVersion()).Scan(&dbVersion) b.conn.QueryRow(b.adapter.DbVersion()).Scan(&dbVersion)
return dbVersion return dbVersion
} }
func (build *builder) Begin() (*sql.Tx, error) { func (b *builder) Begin() (*sql.Tx, error) {
return build.conn.Begin() return b.conn.Begin()
} }
func (build *builder) Tx(handler func(*TransactionBuilder) error) error { func (b *builder) Tx(h func(*TransactionBuilder) error) error {
tx, err := build.conn.Begin() tx, err := b.conn.Begin()
if err != nil { if err != nil {
return err return err
} }
err = handler(&TransactionBuilder{tx, build.adapter, nil}) err = h(&TransactionBuilder{tx, b.adapter, nil})
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
return err return err
@ -77,83 +77,99 @@ func (build *builder) Tx(handler func(*TransactionBuilder) error) error {
return tx.Commit() return tx.Commit()
} }
func (build *builder) prepare(res string, err error) (*sql.Stmt, error) { func (b *builder) prepare(res string, err error) (*sql.Stmt, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return build.conn.Prepare(res) return b.conn.Prepare(res)
} }
func (build *builder) SimpleSelect(table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { func (b *builder) SimpleSelect(table, columns, where, orderby, limit string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleSelect("", table, columns, where, orderby, limit)) return b.prepare(b.adapter.SimpleSelect("", table, columns, where, orderby, limit))
} }
func (build *builder) SimpleCount(table string, where string, limit string) (stmt *sql.Stmt, err error) { func (b *builder) SimpleCount(table, where, limit string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleCount("", table, where, limit)) return b.prepare(b.adapter.SimpleCount("", table, where, limit))
} }
func (build *builder) SimpleLeftJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { func (b *builder) SimpleLeftJoin(table1, table2, columns, joiners, where, orderby, limit string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit)) return b.prepare(b.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit))
} }
func (build *builder) SimpleInnerJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { func (b *builder) SimpleInnerJoin(table1, table2, columns, joiners, where, orderby, limit string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit)) return b.prepare(b.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit))
} }
func (build *builder) DropTable(table string) (stmt *sql.Stmt, err error) { func (b *builder) DropTable(table string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.DropTable("", table)) return b.prepare(b.adapter.DropTable("", table))
} }
func (build *builder) CreateTable(table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt, err error) { func (build *builder) CreateTable(table, charset, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.CreateTable("", table, charset, collation, columns, keys)) return build.prepare(build.adapter.CreateTable("", table, charset, collation, columns, keys))
} }
func (build *builder) AddColumn(table string, column DBTableColumn, key *DBTableKey) (stmt *sql.Stmt, err error) { func (b *builder) AddColumn(table string, column DBTableColumn, key *DBTableKey) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.AddColumn("", table, column, key)) return b.prepare(b.adapter.AddColumn("", table, column, key))
} }
func (build *builder) AddIndex(table string, iname string, colname string) (stmt *sql.Stmt, err error) { func (b *builder) DropColumn(table, colName string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.AddIndex("", table, iname, colname)) return b.prepare(b.adapter.DropColumn("", table, colName))
} }
func (build *builder) AddKey(table string, column string, key DBTableKey) (stmt *sql.Stmt, err error) { func (b *builder) RenameColumn(table, oldName, newName string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.AddKey("", table, column, key)) return b.prepare(b.adapter.RenameColumn("", table, oldName, newName))
} }
func (build *builder) AddForeignKey(table string, column string, ftable string, fcolumn string, cascade bool) (stmt *sql.Stmt, err error) { func (b *builder) ChangeColumn(table, colName string, col DBTableColumn) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.AddForeignKey("", table, column, ftable, fcolumn, cascade)) return b.prepare(b.adapter.ChangeColumn("", table, colName, col))
} }
func (build *builder) SimpleInsert(table string, columns string, fields string) (stmt *sql.Stmt, err error) { func (b *builder) SetDefaultColumn(table, colName, colType, defaultStr string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInsert("", table, columns, fields)) return b.prepare(b.adapter.SetDefaultColumn("", table, colName, colType, defaultStr))
} }
func (build *builder) SimpleInsertSelect(ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) { func (b *builder) AddIndex(table, iname, colname string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInsertSelect("", ins, sel)) return b.prepare(b.adapter.AddIndex("", table, iname, colname))
} }
func (build *builder) SimpleInsertLeftJoin(ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { func (b *builder) AddKey(table, column string, key DBTableKey) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInsertLeftJoin("", ins, sel)) return b.prepare(b.adapter.AddKey("", table, column, key))
} }
func (build *builder) SimpleInsertInnerJoin(ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { func (b *builder) AddForeignKey(table, column, ftable, fcolumn string, cascade bool) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleInsertInnerJoin("", ins, sel)) return b.prepare(b.adapter.AddForeignKey("", table, column, ftable, fcolumn, cascade))
} }
func (build *builder) SimpleUpdate(table string, set string, where string) (stmt *sql.Stmt, err error) { func (b *builder) SimpleInsert(table, columns, fields string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleUpdate(qUpdate(table, set, where))) return b.prepare(b.adapter.SimpleInsert("", table, columns, fields))
} }
func (build *builder) SimpleDelete(table string, where string) (stmt *sql.Stmt, err error) { func (b *builder) SimpleInsertSelect(ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.SimpleDelete("", table, where)) return b.prepare(b.adapter.SimpleInsertSelect("", ins, sel))
}
func (b *builder) SimpleInsertLeftJoin(ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) {
return b.prepare(b.adapter.SimpleInsertLeftJoin("", ins, sel))
}
func (b *builder) SimpleInsertInnerJoin(ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) {
return b.prepare(b.adapter.SimpleInsertInnerJoin("", ins, sel))
}
func (b *builder) SimpleUpdate(table, set, where string) (stmt *sql.Stmt, err error) {
return b.prepare(b.adapter.SimpleUpdate(qUpdate(table, set, where)))
}
func (b *builder) SimpleDelete(table, where string) (stmt *sql.Stmt, err error) {
return b.prepare(b.adapter.SimpleDelete("", table, where))
} }
// I don't know why you need this, but here it is x.x // I don't know why you need this, but here it is x.x
func (build *builder) Purge(table string) (stmt *sql.Stmt, err error) { func (b *builder) Purge(table string) (stmt *sql.Stmt, err error) {
return build.prepare(build.adapter.Purge("", table)) return b.prepare(b.adapter.Purge("", table))
} }
func (build *builder) prepareTx(tx *sql.Tx, res string, err error) (*sql.Stmt, error) { func (b *builder) prepareTx(tx *sql.Tx, res string, err error) (*sql.Stmt, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -161,63 +177,63 @@ func (build *builder) prepareTx(tx *sql.Tx, res string, err error) (*sql.Stmt, e
} }
// These ones support transactions // These ones support transactions
func (build *builder) SimpleSelectTx(tx *sql.Tx, table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { func (b *builder) SimpleSelectTx(tx *sql.Tx, table, columns, where, orderby, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleSelect("", table, columns, where, orderby, limit) res, err := b.adapter.SimpleSelect("", table, columns, where, orderby, limit)
return build.prepareTx(tx, res, err) return b.prepareTx(tx, res, err)
} }
func (build *builder) SimpleCountTx(tx *sql.Tx, table string, where string, limit string) (stmt *sql.Stmt, err error) { func (b *builder) SimpleCountTx(tx *sql.Tx, table, where, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleCount("", table, where, limit) res, err := b.adapter.SimpleCount("", table, where, limit)
return build.prepareTx(tx, res, err) return b.prepareTx(tx, res, err)
} }
func (build *builder) SimpleLeftJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { func (b *builder) SimpleLeftJoinTx(tx *sql.Tx, table1, table2, columns, joiners, where, orderby, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit) res, err := b.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit)
return build.prepareTx(tx, res, err) return b.prepareTx(tx, res, err)
} }
func (build *builder) SimpleInnerJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { func (b *builder) SimpleInnerJoinTx(tx *sql.Tx, table1, table2, columns, joiners, where, orderby, limit string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit) res, err := b.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit)
return build.prepareTx(tx, res, err) return b.prepareTx(tx, res, err)
} }
func (build *builder) CreateTableTx(tx *sql.Tx, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt, err error) { func (b *builder) CreateTableTx(tx *sql.Tx, table, charset, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt, err error) {
res, err := build.adapter.CreateTable("", table, charset, collation, columns, keys) res, err := b.adapter.CreateTable("", table, charset, collation, columns, keys)
return build.prepareTx(tx, res, err) return b.prepareTx(tx, res, err)
} }
func (build *builder) SimpleInsertTx(tx *sql.Tx, table string, columns string, fields string) (stmt *sql.Stmt, err error) { func (b *builder) SimpleInsertTx(tx *sql.Tx, table, columns, fields string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsert("", table, columns, fields) res, err := b.adapter.SimpleInsert("", table, columns, fields)
return build.prepareTx(tx, res, err) return b.prepareTx(tx, res, err)
} }
func (build *builder) SimpleInsertSelectTx(tx *sql.Tx, ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) { func (b *builder) SimpleInsertSelectTx(tx *sql.Tx, ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsertSelect("", ins, sel) res, err := b.adapter.SimpleInsertSelect("", ins, sel)
return build.prepareTx(tx, res, err) return b.prepareTx(tx, res, err)
} }
func (build *builder) SimpleInsertLeftJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { func (b *builder) SimpleInsertLeftJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsertLeftJoin("", ins, sel) res, err := b.adapter.SimpleInsertLeftJoin("", ins, sel)
return build.prepareTx(tx, res, err) return b.prepareTx(tx, res, err)
} }
func (build *builder) SimpleInsertInnerJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { func (b *builder) SimpleInsertInnerJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleInsertInnerJoin("", ins, sel) res, err := b.adapter.SimpleInsertInnerJoin("", ins, sel)
return build.prepareTx(tx, res, err) return b.prepareTx(tx, res, err)
} }
func (build *builder) SimpleUpdateTx(tx *sql.Tx, table string, set string, where string) (stmt *sql.Stmt, err error) { func (b *builder) SimpleUpdateTx(tx *sql.Tx, table, set, where string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleUpdate(qUpdate(table, set, where)) res, err := b.adapter.SimpleUpdate(qUpdate(table, set, where))
return build.prepareTx(tx, res, err) return b.prepareTx(tx, res, err)
} }
func (build *builder) SimpleDeleteTx(tx *sql.Tx, table string, where string) (stmt *sql.Stmt, err error) { func (b *builder) SimpleDeleteTx(tx *sql.Tx, table, where string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.SimpleDelete("", table, where) res, err := b.adapter.SimpleDelete("", table, where)
return build.prepareTx(tx, res, err) return b.prepareTx(tx, res, err)
} }
// I don't know why you need this, but here it is x.x // I don't know why you need this, but here it is x.x
func (build *builder) PurgeTx(tx *sql.Tx, table string) (stmt *sql.Stmt, err error) { func (b *builder) PurgeTx(tx *sql.Tx, table string) (stmt *sql.Stmt, err error) {
res, err := build.adapter.Purge("", table) res, err := b.adapter.Purge("", table)
return build.prepareTx(tx, res, err) return b.prepareTx(tx, res, err)
} }

View File

@ -56,7 +56,7 @@ func (a *MssqlAdapter) DropTable(name, table string) (string, error) {
// TODO: Add support for foreign keys? // TODO: Add support for foreign keys?
// TODO: Convert any remaining stringy types to nvarchar // TODO: Convert any remaining stringy types to nvarchar
// We may need to change the CreateTable API to better suit Mssql and the other database drivers which are coming up // We may need to change the CreateTable API to better suit Mssql and the other database drivers which are coming up
func (a *MssqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) { func (a *MssqlAdapter) CreateTable(name, table, charset, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -137,7 +137,7 @@ func (a *MssqlAdapter) parseColumn(column DBTableColumn) (col DBTableColumn, siz
// TODO: Test this, not sure if some things work // TODO: Test this, not sure if some things work
// TODO: Add support for keys // TODO: Add support for keys
func (a *MssqlAdapter) AddColumn(name string, table string, column DBTableColumn, key *DBTableKey) (string, error) { func (a *MssqlAdapter) AddColumn(name, table string, column DBTableColumn, key *DBTableKey) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -148,6 +148,29 @@ func (a *MssqlAdapter) AddColumn(name string, table string, column DBTableColumn
return q, nil return q, nil
} }
// TODO: Implement this
func (a *MssqlAdapter) DropColumn(name, table, colName string) (string, error) {
return "", errors.New("not implemented")
}
// TODO: Implement this
func (a *MssqlAdapter) RenameColumn(name, table, oldName, newName string) (string, error) {
return "", errors.New("not implemented")
}
// TODO: Implement this
func (a *MssqlAdapter) ChangeColumn(name, table, colName string, col DBTableColumn) (string, error) {
return "", errors.New("not implemented")
}
// TODO: Implement this
func (a *MssqlAdapter) SetDefaultColumn(name, table, colName, colType, defaultStr string) (string, error) {
if colType == "text" {
return "", errors.New("text fields cannot have default values")
}
return "", errors.New("not implemented")
}
// TODO: Implement this // TODO: Implement this
// TODO: Test to make sure everything works here // TODO: Test to make sure everything works here
func (a *MssqlAdapter) AddIndex(name, table, iname, colname string) (string, error) { func (a *MssqlAdapter) AddIndex(name, table, iname, colname string) (string, error) {

View File

@ -92,7 +92,7 @@ func (a *MysqlAdapter) DropTable(name, table string) (string, error) {
return q, nil return q, nil
} }
func (a *MysqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) { func (a *MysqlAdapter) CreateTable(name, table, charset, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -143,53 +143,95 @@ func (a *MysqlAdapter) CreateTable(name string, table string, charset string, co
return q, nil return q, nil
} }
func (a *MysqlAdapter) parseColumn(column DBTableColumn) (col DBTableColumn, size string, end string) { func (a *MysqlAdapter) DropColumn(name, table, colName string) (string, error) {
// Make it easier to support Cassandra in the future q := "ALTER TABLE `" + table + "` DROP COLUMN `" + colName + "`;"
if column.Type == "createdAt" { a.pushStatement(name, "drop-column", q)
column.Type = "datetime" return q, nil
// MySQL doesn't support this x.x }
/*if column.Default == "" {
column.Default = "UTC_TIMESTAMP()" // ! Currently broken in MariaDB. Planned.
}*/ func (a *MysqlAdapter) RenameColumn(name, table, oldName, newName string) (string, error) {
} else if column.Type == "json" { q := "ALTER TABLE `" + table + "` RENAME COLUMN `" + oldName + "` TO `" + newName + "`;"
column.Type = "text" a.pushStatement(name, "rename-column", q)
return q, nil
}
func (a *MysqlAdapter) ChangeColumn(name, table, colName string, col DBTableColumn) (string, error) {
col.Default = ""
col, size, end := a.parseColumn(col)
q := "ALTER TABLE `" + table + "` CHANGE COLUMN `" + colName + "` `" + col.Name + "` " + col.Type + size + end
a.pushStatement(name, "change-column", q)
return q, nil
}
func (a *MysqlAdapter) SetDefaultColumn(name, table, colName, colType, defaultStr string) (string, error) {
if colType == "text" {
return "", errors.New("text fields cannot have default values")
} }
if column.Size > 0 { if defaultStr == "" {
size = "(" + strconv.Itoa(column.Size) + ")" defaultStr = "''"
}
// TODO: Exclude the other variants of text like mediumtext and longtext too
expr := ""
/*if colType == "datetime" && defaultStr[len(defaultStr)-1] == ')' {
end += defaultStr
} else */if a.stringyType(colType) && defaultStr != "''" {
expr += "'" + defaultStr + "'"
} else {
expr += defaultStr
}
q := "ALTER TABLE `" + table + "` ALTER COLUMN `" + colName + "` SET DEFAULT " + expr + ";"
a.pushStatement(name, "set-default-column", q)
return q, nil
}
func (a *MysqlAdapter) parseColumn(col DBTableColumn) (ocol DBTableColumn, size, end string) {
// Make it easier to support Cassandra in the future
if col.Type == "createdAt" {
col.Type = "datetime"
// MySQL doesn't support this x.x
/*if col.Default == "" {
col.Default = "UTC_TIMESTAMP()"
}*/
} else if col.Type == "json" {
col.Type = "text"
}
if col.Size > 0 {
size = "(" + strconv.Itoa(col.Size) + ")"
} }
// TODO: Exclude the other variants of text like mediumtext and longtext too // TODO: Exclude the other variants of text like mediumtext and longtext too
if column.Default != "" && column.Type != "text" { if col.Default != "" && col.Type != "text" {
end = " DEFAULT " end = " DEFAULT "
/*if column.Type == "datetime" && column.Default[len(column.Default)-1] == ')' { /*if col.Type == "datetime" && col.Default[len(col.Default)-1] == ')' {
end += column.Default end += column.Default
} else */if a.stringyType(column.Type) && column.Default != "''" { } else */if a.stringyType(col.Type) && col.Default != "''" {
end += "'" + column.Default + "'" end += "'" + col.Default + "'"
} else { } else {
end += column.Default end += col.Default
} }
} }
if column.Null { if col.Null {
end += " null" end += " null"
} else { } else {
end += " not null" end += " not null"
} }
if column.AutoIncrement { if col.AutoIncrement {
end += " AUTO_INCREMENT" end += " AUTO_INCREMENT"
} }
return column, size, end return col, size, end
} }
// TODO: Support AFTER column // TODO: Support AFTER column
// TODO: Test to make sure everything works here // TODO: Test to make sure everything works here
func (a *MysqlAdapter) AddColumn(name string, table string, column DBTableColumn, key *DBTableKey) (string, error) { func (a *MysqlAdapter) AddColumn(name, table string, col DBTableColumn, key *DBTableKey) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
column, size, end := a.parseColumn(column) col, size, end := a.parseColumn(col)
q := "ALTER TABLE `" + table + "` ADD COLUMN " + "`" + column.Name + "` " + column.Type + size + end q := "ALTER TABLE `" + table + "` ADD COLUMN " + "`" + col.Name + "` " + col.Type + size + end
if key != nil { if key != nil {
q += " " + key.Type q += " " + key.Type
@ -225,7 +267,7 @@ func (a *MysqlAdapter) AddIndex(name, table, iname, colname string) (string, err
// TODO: Test to make sure everything works here // TODO: Test to make sure everything works here
// Only supports FULLTEXT right now // Only supports FULLTEXT right now
func (a *MysqlAdapter) AddKey(name string, table string, column string, key DBTableKey) (string, error) { func (a *MysqlAdapter) AddKey(name, table, column string, key DBTableKey) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -241,7 +283,7 @@ func (a *MysqlAdapter) AddKey(name string, table string, column string, key DBTa
return q, nil return q, nil
} }
func (a *MysqlAdapter) AddForeignKey(name string, table string, column string, ftable string, fcolumn string, cascade bool) (out string, e error) { func (a *MysqlAdapter) AddForeignKey(name, table, column, ftable, fcolumn string, cascade bool) (out string, e error) {
c := func(str string, val bool) { c := func(str string, val bool) {
if e != nil || !val { if e != nil || !val {
return return
@ -338,7 +380,7 @@ func (a *MysqlAdapter) SimpleReplace(name, table, columns, fields string) (strin
for _, field := range processFields(fields) { for _, field := range processFields(fields) {
q += field.Name + "," q += field.Name + ","
} }
q = q[0 : len(q)-1] + ")" q = q[0:len(q)-1] + ")"
// TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator // TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator
a.pushStatement(name, "replace", q) a.pushStatement(name, "replace", q)
@ -362,8 +404,7 @@ func (a *MysqlAdapter) SimpleUpsert(name, table, columns, fields, where string)
q := "INSERT INTO `" + table + "`(" q := "INSERT INTO `" + table + "`("
parsedFields := processFields(fields) parsedFields := processFields(fields)
var insertColumns string var insertColumns, insertValues string
var insertValues string
setBit := ") ON DUPLICATE KEY UPDATE " setBit := ") ON DUPLICATE KEY UPDATE "
for columnID, col := range processColumns(columns) { for columnID, col := range processColumns(columns) {
@ -667,7 +708,7 @@ func (a *MysqlAdapter) complexSelect(preBuilder *selectPrebuilder, sb *strings.B
return nil return nil
} }
func (a *MysqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { func (a *MysqlAdapter) SimpleLeftJoin(name, table1, table2, columns, joiners, where, orderby, limit string) (string, error) {
if table1 == "" { if table1 == "" {
return "", errors.New("You need a name for the left table") return "", errors.New("You need a name for the left table")
} }
@ -704,7 +745,7 @@ func (a *MysqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string,
return q, nil return q, nil
} }
func (a *MysqlAdapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { func (a *MysqlAdapter) SimpleInnerJoin(name, table1, table2, columns, joiners, where, orderby, limit string) (string, error) {
if table1 == "" { if table1 == "" {
return "", errors.New("You need a name for the left table") return "", errors.New("You need a name for the left table")
} }

View File

@ -42,7 +42,7 @@ func (a *PgsqlAdapter) DbVersion() string {
return "SELECT version()" return "SELECT version()"
} }
func (a *PgsqlAdapter) DropTable(name string, table string) (string, error) { func (a *PgsqlAdapter) DropTable(name, table string) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -53,7 +53,7 @@ func (a *PgsqlAdapter) DropTable(name string, table string) (string, error) {
// TODO: Implement this // TODO: Implement this
// We may need to change the CreateTable API to better suit PGSQL and the other database drivers which are coming up // We may need to change the CreateTable API to better suit PGSQL and the other database drivers which are coming up
func (a *PgsqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) { func (a *PgsqlAdapter) CreateTable(name, table, charset, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -112,16 +112,39 @@ func (a *PgsqlAdapter) CreateTable(name string, table string, charset string, co
} }
// TODO: Implement this // TODO: Implement this
func (a *PgsqlAdapter) AddColumn(name string, table string, column DBTableColumn, key *DBTableKey) (string, error) { func (a *PgsqlAdapter) AddColumn(name, table string, column DBTableColumn, key *DBTableKey) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
return "", nil return "", nil
} }
// TODO: Implement this
func (a *PgsqlAdapter) DropColumn(name, table, colName string) (string, error) {
return "", errors.New("not implemented")
}
// TODO: Implement this
func (a *PgsqlAdapter) RenameColumn(name, table, oldName, newName string) (string, error) {
return "", errors.New("not implemented")
}
// TODO: Implement this
func (a *PgsqlAdapter) ChangeColumn(name, table, colName string, col DBTableColumn) (string, error) {
return "", errors.New("not implemented")
}
// TODO: Implement this
func (a *PgsqlAdapter) SetDefaultColumn(name, table, colName, colType, defaultStr string) (string, error) {
if colType == "text" {
return "", errors.New("text fields cannot have default values")
}
return "", errors.New("not implemented")
}
// TODO: Implement this // TODO: Implement this
// TODO: Test to make sure everything works here // TODO: Test to make sure everything works here
func (a *PgsqlAdapter) AddIndex(name string, table string, iname string, colname string) (string, error) { func (a *PgsqlAdapter) AddIndex(name, table, iname, colname string) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -136,7 +159,7 @@ func (a *PgsqlAdapter) AddIndex(name string, table string, iname string, colname
// TODO: Implement this // TODO: Implement this
// TODO: Test to make sure everything works here // TODO: Test to make sure everything works here
func (a *PgsqlAdapter) AddKey(name string, table string, column string, key DBTableKey) (string, error) { func (a *PgsqlAdapter) AddKey(name, table, column string, key DBTableKey) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -148,7 +171,7 @@ func (a *PgsqlAdapter) AddKey(name string, table string, column string, key DBTa
// TODO: Implement this // TODO: Implement this
// TODO: Test to make sure everything works here // TODO: Test to make sure everything works here
func (a *PgsqlAdapter) AddForeignKey(name string, table string, column string, ftable string, fcolumn string, cascade bool) (out string, e error) { func (a *PgsqlAdapter) AddForeignKey(name, table, column, ftable, fcolumn string, cascade bool) (out string, e error) {
var c = func(str string, val bool) { var c = func(str string, val bool) {
if e != nil || !val { if e != nil || !val {
return return
@ -167,7 +190,7 @@ func (a *PgsqlAdapter) AddForeignKey(name string, table string, column string, f
// TODO: Test this // TODO: Test this
// ! We need to get the last ID out of this somehow, maybe add returning to every query? Might require some sort of wrapper over the sql statements // ! We need to get the last ID out of this somehow, maybe add returning to every query? Might require some sort of wrapper over the sql statements
func (a *PgsqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) { func (a *PgsqlAdapter) SimpleInsert(name, table, columns, fields string) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -225,7 +248,7 @@ func (a *PgsqlAdapter) SimpleReplace(name, table, columns, fields string) (strin
} }
// TODO: Implement this // TODO: Implement this
func (a *PgsqlAdapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) { func (a *PgsqlAdapter) SimpleUpsert(name, table, columns, fields, where string) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -307,7 +330,7 @@ func (a *PgsqlAdapter) SimpleUpdateSelect(up *updatePrebuilder) (string, error)
} }
// TODO: Implement this // TODO: Implement this
func (a *PgsqlAdapter) SimpleDelete(name string, table string, where string) (string, error) { func (a *PgsqlAdapter) SimpleDelete(name, table, where string) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -330,7 +353,7 @@ func (a *PgsqlAdapter) ComplexDelete(b *deletePrebuilder) (string, error) {
// TODO: Implement this // TODO: Implement this
// We don't want to accidentally wipe tables, so we'll have a separate method for purging tables instead // We don't want to accidentally wipe tables, so we'll have a separate method for purging tables instead
func (a *PgsqlAdapter) Purge(name string, table string) (string, error) { func (a *PgsqlAdapter) Purge(name, table string) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -338,7 +361,7 @@ func (a *PgsqlAdapter) Purge(name string, table string) (string, error) {
} }
// TODO: Implement this // TODO: Implement this
func (a *PgsqlAdapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) { func (a *PgsqlAdapter) SimpleSelect(name, table, columns, where, orderby, limit string) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -360,7 +383,7 @@ func (a *PgsqlAdapter) ComplexSelect(prebuilder *selectPrebuilder) (string, erro
} }
// TODO: Implement this // TODO: Implement this
func (a *PgsqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { func (a *PgsqlAdapter) SimpleLeftJoin(name, table1, table2, columns, joiners, where, orderby, limit string) (string, error) {
if table1 == "" { if table1 == "" {
return "", errors.New("You need a name for the left table") return "", errors.New("You need a name for the left table")
} }
@ -377,7 +400,7 @@ func (a *PgsqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string,
} }
// TODO: Implement this // TODO: Implement this
func (a *PgsqlAdapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { func (a *PgsqlAdapter) SimpleInnerJoin(name, table1, table2, columns, joiners, where, orderby, limit string) (string, error) {
if table1 == "" { if table1 == "" {
return "", errors.New("You need a name for the left table") return "", errors.New("You need a name for the left table")
} }
@ -409,7 +432,7 @@ func (a *PgsqlAdapter) SimpleInsertInnerJoin(name string, ins DBInsert, sel DBJo
} }
// TODO: Implement this // TODO: Implement this
func (a *PgsqlAdapter) SimpleCount(name string, table string, where string, limit string) (string, error) { func (a *PgsqlAdapter) SimpleCount(name, table, where, limit string) (string, error) {
if table == "" { if table == "" {
return "", errors.New("You need a name for this table") return "", errors.New("You need a name for this table")
} }
@ -471,7 +494,7 @@ func _gen_pgsql() (err error) {
} }
// Internal methods, not exposed in the interface // Internal methods, not exposed in the interface
func (a *PgsqlAdapter) pushStatement(name string, stype string, q string) { func (a *PgsqlAdapter) pushStatement(name, stype, q string) {
if name == "" { if name == "" {
return return
} }

View File

@ -130,22 +130,26 @@ type Adapter interface {
BuildConn(config map[string]string) (*sql.DB, error) BuildConn(config map[string]string) (*sql.DB, error)
DbVersion() string DbVersion() string
DropTable(name string, table string) (string, error) DropTable(name, table string) (string, error)
CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) CreateTable(name, table, charset, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error)
// TODO: Some way to add indices and keys // TODO: Some way to add indices and keys
// TODO: Test this // TODO: Test this
AddColumn(name string, table string, column DBTableColumn, key *DBTableKey) (string, error) AddColumn(name, table string, column DBTableColumn, key *DBTableKey) (string, error)
AddIndex(name string, table string, iname string, colname string) (string, error) DropColumn(name, table, colname string) (string, error)
AddKey(name string, table string, column string, key DBTableKey) (string, error) RenameColumn(name, table, oldName, newName string) (string, error)
AddForeignKey(name string, table string, column string, ftable string, fcolumn string, cascade bool) (out string, e error) ChangeColumn(name, table, colName string, col DBTableColumn) (string, error)
SimpleInsert(name string, table string, columns string, fields string) (string, error) SetDefaultColumn(name, table, colName, colType, defaultStr string) (string, error)
AddIndex(name, table, iname, colname string) (string, error)
AddKey(name, table, column string, key DBTableKey) (string, error)
AddForeignKey(name, table, column, ftable, fcolumn string, cascade bool) (out string, e error)
SimpleInsert(name, table, columns, fields string) (string, error)
SimpleUpdate(b *updatePrebuilder) (string, error) SimpleUpdate(b *updatePrebuilder) (string, error)
SimpleUpdateSelect(b *updatePrebuilder) (string, error) // ! Experimental SimpleUpdateSelect(b *updatePrebuilder) (string, error) // ! Experimental
SimpleDelete(name string, table string, where string) (string, error) SimpleDelete(name, table, where string) (string, error)
Purge(name string, table string) (string, error) Purge(name, table string) (string, error)
SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) SimpleSelect(name, table, columns, where, orderby, limit string) (string, error)
ComplexDelete(b *deletePrebuilder) (string, error) ComplexDelete(b *deletePrebuilder) (string, error)
SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) SimpleLeftJoin(name, table1, table2, columns, joiners, where, orderby, limit string) (string, error)
SimpleInnerJoin(string, string, string, string, string, string, string, string) (string, error) SimpleInnerJoin(string, string, string, string, string, string, string, string) (string, error)
SimpleInsertSelect(string, DBInsert, DBSelect) (string, error) SimpleInsertSelect(string, DBInsert, DBSelect) (string, error)
SimpleInsertLeftJoin(string, DBInsert, DBJoin) (string, error) SimpleInsertLeftJoin(string, DBInsert, DBJoin) (string, error)
@ -192,9 +196,9 @@ func PrepareMySQLUpsertCallback(db *sql.DB, query string) (*MySQLUpsertCallback,
type LitStr string type LitStr string
// TODO: Test this // TODO: Test this
func InterfaceMapToInsertStrings(data map[string]interface{}, order string) (cols string, values string) { func InterfaceMapToInsertStrings(data map[string]interface{}, order string) (cols, values string) {
var done = make(map[string]bool) done := make(map[string]bool)
var addValue = func(value interface{}) { addValue := func(value interface{}) {
switch value := value.(type) { switch value := value.(type) {
case string: case string:
values += "'" + strings.Replace(value, "'", "\\'", -1) + "'," values += "'" + strings.Replace(value, "'", "\\'", -1) + "',"

View File

@ -127,6 +127,7 @@ func topicRoutes() *RouteGroup {
Action("routes.UnlockTopicSubmit", "/topic/unlock/submit/", "extraData"), Action("routes.UnlockTopicSubmit", "/topic/unlock/submit/", "extraData"),
Action("routes.MoveTopicSubmit", "/topic/move/submit/", "extraData"), Action("routes.MoveTopicSubmit", "/topic/move/submit/", "extraData"),
Action("routes.LikeTopicSubmit", "/topic/like/submit/", "extraData"), Action("routes.LikeTopicSubmit", "/topic/like/submit/", "extraData"),
Action("routes.UnlikeTopicSubmit", "/topic/unlike/submit/", "extraData"),
UploadAction("routes.AddAttachToTopicSubmit", "/topic/attach/add/submit/", "extraData").MaxSizeVar("int(c.Config.MaxRequestSize)"), UploadAction("routes.AddAttachToTopicSubmit", "/topic/attach/add/submit/", "extraData").MaxSizeVar("int(c.Config.MaxRequestSize)"),
Action("routes.RemoveAttachFromTopicSubmit", "/topic/attach/remove/submit/", "extraData"), Action("routes.RemoveAttachFromTopicSubmit", "/topic/attach/remove/submit/", "extraData"),
) )

View File

@ -66,7 +66,7 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user c.User) c.RouteError
} }
// Don't want to throw an internal error due to a socket closing // Don't want to throw an internal error due to a socket closing
if c.EnableWebsockets && count > 0 { if c.EnableWebsockets && count > 0 {
_ = c.WsHub.PushMessage(user.ID, `{"event":"dismiss-alert","id":`+strconv.Itoa(id)+`}`) c.DismissAlert(user.ID, id)
} }
w.Write(successJSONBytes) w.Write(successJSONBytes)
// TODO: Split this into it's own function // TODO: Split this into it's own function

View File

@ -39,13 +39,13 @@ func ProfileReplyCreateSubmit(w http.ResponseWriter, r *http.Request, user c.Use
return c.LocalError("You can't make a blank post", w, r, user) return c.LocalError("You can't make a blank post", w, r, user)
} }
// TODO: Fully parse the post and store it in the parsed column // TODO: Fully parse the post and store it in the parsed column
_, err = c.Prstore.Create(profileOwner.ID, content, user.ID, user.GetIP()) prid, err := c.Prstore.Create(profileOwner.ID, content, user.ID, user.GetIP())
if err != nil { if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)
} }
// ! Be careful about leaking per-route permission state with &user // ! Be careful about leaking per-route permission state with &user
alert := c.Alert{ActorID: user.ID, TargetUserID: profileOwner.ID, Event: "reply", ElementType: "user", ElementID: profileOwner.ID, Actor: &user} alert := c.Alert{ActorID: user.ID, TargetUserID: profileOwner.ID, Event: "reply", ElementType: "user", ElementID: profileOwner.ID, Actor: &user, Extra: strconv.Itoa(prid)}
err = c.AddActivityAndNotifyTarget(alert) err = c.AddActivityAndNotifyTarget(alert)
if err != nil { if err != nil {
return c.InternalError(err, w, r) return c.InternalError(err, w, r)

View File

@ -136,7 +136,7 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user c.User) c.Ro
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }
c.AddActivityAndNotifyAll(user.ID, topic.CreatedBy, "reply", "topic", tid) c.AddActivityAndNotifyAll(c.Alert{ActorID: user.ID, TargetUserID: topic.CreatedBy, Event: "reply", ElementType: "topic", ElementID: tid, Extra: strconv.Itoa(rid)})
if err != nil { if err != nil {
return c.InternalErrorJSQ(err, w, r, js) return c.InternalErrorJSQ(err, w, r, js)
} }

View File

@ -346,10 +346,10 @@ func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, user c.User) c.Ro
return c.NoPermissions(w, r, user) return c.NoPermissions(w, r, user)
} }
tname := c.SanitiseSingleLine(r.PostFormValue("name")) name := c.SanitiseSingleLine(r.PostFormValue("name"))
content := c.PreparseMessage(r.PostFormValue("content")) content := c.PreparseMessage(r.PostFormValue("content"))
// TODO: Fully parse the post and store it in the parsed column // TODO: Fully parse the post and store it in the parsed column
tid, err := c.Topics.Create(fid, tname, content, user.ID, user.GetIP()) tid, err := c.Topics.Create(fid, name, content, user.ID, user.GetIP())
if err != nil { if err != nil {
switch err { switch err {
case c.ErrNoRows: case c.ErrNoRows:
@ -959,3 +959,55 @@ func LikeTopicSubmit(w http.ResponseWriter, r *http.Request, user c.User, stid s
} }
return nil return nil
} }
func UnlikeTopicSubmit(w http.ResponseWriter, r *http.Request, user c.User, stid string) c.RouteError {
js := r.PostFormValue("js") == "1"
tid, err := strconv.Atoi(stid)
if err != nil {
return c.PreErrorJSQ(phrases.GetErrorPhrase("id_must_be_integer"), w, r, js)
}
topic, err := c.Topics.Get(tid)
if err == sql.ErrNoRows {
return c.PreErrorJSQ("The requested topic doesn't exist.", w, r, js)
} else if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
// TODO: Add hooks to make use of headerLite
lite, ferr := c.SimpleForumUserCheck(w, r, &user, topic.ParentID)
if ferr != nil {
return ferr
}
if !user.Perms.ViewTopic || !user.Perms.LikeItem {
return c.NoPermissionsJSQ(w, r, user, js)
}
_, err = c.Users.Get(topic.CreatedBy)
if err != nil && err == sql.ErrNoRows {
return c.LocalErrorJSQ("The target user doesn't exist", w, r, user, js)
} else if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
err = topic.Unlike(user.ID)
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
// TODO: Push dismiss-event alerts to the users.
err = c.Activity.DeleteByParams("like", topic.ID, "topic")
if err != nil {
return c.InternalErrorJSQ(err, w, r, js)
}
skip, rerr := lite.Hooks.VhookSkippable("action_end_unlike_topic", topic.ID, &user)
if skip || rerr != nil {
return rerr
}
if !js {
http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther)
} else {
_, _ = w.Write(successJSONBytes)
}
return nil
}

View File

@ -27,8 +27,8 @@ INSERT INTO [forums_permissions] ([gid],[fid],[permissions]) VALUES (3,2,'{"View
INSERT INTO [forums_permissions] ([gid],[fid],[permissions]) VALUES (4,2,'{"ViewTopic":true}'); INSERT INTO [forums_permissions] ([gid],[fid],[permissions]) VALUES (4,2,'{"ViewTopic":true}');
INSERT INTO [forums_permissions] ([gid],[fid],[permissions]) VALUES (5,2,'{"ViewTopic":true}'); INSERT INTO [forums_permissions] ([gid],[fid],[permissions]) VALUES (5,2,'{"ViewTopic":true}');
INSERT INTO [forums_permissions] ([gid],[fid],[permissions]) VALUES (6,2,'{"ViewTopic":true}'); INSERT INTO [forums_permissions] ([gid],[fid],[permissions]) VALUES (6,2,'{"ViewTopic":true}');
INSERT INTO [topics] ([title],[content],[parsed_content],[createdAt],[lastReplyAt],[lastReplyBy],[createdBy],[parentID],[ipaddress]) VALUES ('Test Topic','A topic automatically generated by the software.','A topic automatically generated by the software.',GETUTCDATE(),GETUTCDATE(),1,1,2,'::1'); INSERT INTO [topics] ([title],[content],[parsed_content],[createdAt],[lastReplyAt],[lastReplyBy],[createdBy],[parentID],[ip]) VALUES ('Test Topic','A topic automatically generated by the software.','A topic automatically generated by the software.',GETUTCDATE(),GETUTCDATE(),1,1,2,'::1');
INSERT INTO [replies] ([tid],[content],[parsed_content],[createdAt],[createdBy],[lastUpdated],[lastEdit],[lastEditBy],[ipaddress]) VALUES (1,'A reply!','A reply!',GETUTCDATE(),1,GETUTCDATE(),0,0,'::1'); INSERT INTO [replies] ([tid],[content],[parsed_content],[createdAt],[createdBy],[lastUpdated],[lastEdit],[lastEditBy],[ip]) VALUES (1,'A reply!','A reply!',GETUTCDATE(),1,GETUTCDATE(),0,0,'::1');
INSERT INTO [menus] () VALUES (); INSERT INTO [menus] () VALUES ();
INSERT INTO [menu_items] ([mid],[name],[htmlID],[position],[path],[aria],[tooltip],[order]) VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0); INSERT INTO [menu_items] ([mid],[name],[htmlID],[position],[path],[aria],[tooltip],[order]) VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0);
INSERT INTO [menu_items] ([mid],[name],[htmlID],[cssClass],[position],[path],[aria],[tooltip],[order]) VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1); INSERT INTO [menu_items] ([mid],[name],[htmlID],[cssClass],[position],[path],[aria],[tooltip],[order]) VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1);

View File

@ -6,5 +6,6 @@ CREATE TABLE [activity_stream] (
[elementType] nvarchar (50) not null, [elementType] nvarchar (50) not null,
[elementID] int not null, [elementID] int not null,
[createdAt] datetime not null, [createdAt] datetime not null,
[extra] nvarchar (200) DEFAULT '' not null,
primary key([asid]) primary key([asid])
); );

View File

@ -3,5 +3,5 @@ CREATE TABLE [polls_votes] (
[uid] int not null, [uid] int not null,
[option] int DEFAULT 0 not null, [option] int DEFAULT 0 not null,
[castAt] datetime not null, [castAt] datetime not null,
[ipaddress] nvarchar (200) DEFAULT '0.0.0.0.0' not null [ip] nvarchar (200) DEFAULT '' not null
); );

View File

@ -8,7 +8,7 @@ CREATE TABLE [replies] (
[lastEdit] int DEFAULT 0 not null, [lastEdit] int DEFAULT 0 not null,
[lastEditBy] int DEFAULT 0 not null, [lastEditBy] int DEFAULT 0 not null,
[lastUpdated] datetime not null, [lastUpdated] datetime not null,
[ipaddress] nvarchar (200) DEFAULT '0.0.0.0.0' not null, [ip] nvarchar (200) DEFAULT '' not null,
[likeCount] int DEFAULT 0 not null, [likeCount] int DEFAULT 0 not null,
[attachCount] int DEFAULT 0 not null, [attachCount] int DEFAULT 0 not null,
[words] int DEFAULT 1 not null, [words] int DEFAULT 1 not null,

View File

@ -11,7 +11,7 @@ CREATE TABLE [topics] (
[is_closed] bit DEFAULT 0 not null, [is_closed] bit DEFAULT 0 not null,
[sticky] bit DEFAULT 0 not null, [sticky] bit DEFAULT 0 not null,
[parentID] int DEFAULT 2 not null, [parentID] int DEFAULT 2 not null,
[ipaddress] nvarchar (200) DEFAULT '0.0.0.0.0' not null, [ip] nvarchar (200) DEFAULT '' not null,
[postCount] int DEFAULT 1 not null, [postCount] int DEFAULT 1 not null,
[likeCount] int DEFAULT 0 not null, [likeCount] int DEFAULT 0 not null,
[attachCount] int DEFAULT 0 not null, [attachCount] int DEFAULT 0 not null,

View File

@ -7,6 +7,6 @@ CREATE TABLE [users_replies] (
[createdBy] int not null, [createdBy] int not null,
[lastEdit] int DEFAULT 0 not null, [lastEdit] int DEFAULT 0 not null,
[lastEditBy] int DEFAULT 0 not null, [lastEditBy] int DEFAULT 0 not null,
[ipaddress] nvarchar (200) DEFAULT '0.0.0.0.0' not null, [ip] nvarchar (200) DEFAULT '' not null,
primary key([rid]) primary key([rid])
); );

View File

@ -35,8 +35,8 @@ INSERT INTO `forums_permissions`(`gid`,`fid`,`permissions`) VALUES (3,2,'{"ViewT
INSERT INTO `forums_permissions`(`gid`,`fid`,`permissions`) VALUES (4,2,'{"ViewTopic":true}'); INSERT INTO `forums_permissions`(`gid`,`fid`,`permissions`) VALUES (4,2,'{"ViewTopic":true}');
INSERT INTO `forums_permissions`(`gid`,`fid`,`permissions`) VALUES (5,2,'{"ViewTopic":true}'); INSERT INTO `forums_permissions`(`gid`,`fid`,`permissions`) VALUES (5,2,'{"ViewTopic":true}');
INSERT INTO `forums_permissions`(`gid`,`fid`,`permissions`) VALUES (6,2,'{"ViewTopic":true}'); INSERT INTO `forums_permissions`(`gid`,`fid`,`permissions`) VALUES (6,2,'{"ViewTopic":true}');
INSERT INTO `topics`(`title`,`content`,`parsed_content`,`createdAt`,`lastReplyAt`,`lastReplyBy`,`createdBy`,`parentID`,`ipaddress`) VALUES ('Test Topic','A topic automatically generated by the software.','A topic automatically generated by the software.',UTC_TIMESTAMP(),UTC_TIMESTAMP(),1,1,2,'::1'); INSERT INTO `topics`(`title`,`content`,`parsed_content`,`createdAt`,`lastReplyAt`,`lastReplyBy`,`createdBy`,`parentID`,`ip`) VALUES ('Test Topic','A topic automatically generated by the software.','A topic automatically generated by the software.',UTC_TIMESTAMP(),UTC_TIMESTAMP(),1,1,2,'::1');
INSERT INTO `replies`(`tid`,`content`,`parsed_content`,`createdAt`,`createdBy`,`lastUpdated`,`lastEdit`,`lastEditBy`,`ipaddress`) VALUES (1,'A reply!','A reply!',UTC_TIMESTAMP(),1,UTC_TIMESTAMP(),0,0,'::1'); INSERT INTO `replies`(`tid`,`content`,`parsed_content`,`createdAt`,`createdBy`,`lastUpdated`,`lastEdit`,`lastEditBy`,`ip`) VALUES (1,'A reply!','A reply!',UTC_TIMESTAMP(),1,UTC_TIMESTAMP(),0,0,'::1');
INSERT INTO `menus`() VALUES (); INSERT INTO `menus`() VALUES ();
INSERT INTO `menu_items`(`mid`,`name`,`htmlID`,`position`,`path`,`aria`,`tooltip`,`order`) VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0); INSERT INTO `menu_items`(`mid`,`name`,`htmlID`,`position`,`path`,`aria`,`tooltip`,`order`) VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0);
INSERT INTO `menu_items`(`mid`,`name`,`htmlID`,`cssClass`,`position`,`path`,`aria`,`tooltip`,`order`) VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1); INSERT INTO `menu_items`(`mid`,`name`,`htmlID`,`cssClass`,`position`,`path`,`aria`,`tooltip`,`order`) VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1);

View File

@ -6,5 +6,6 @@ CREATE TABLE `activity_stream` (
`elementType` varchar(50) not null, `elementType` varchar(50) not null,
`elementID` int not null, `elementID` int not null,
`createdAt` datetime not null, `createdAt` datetime not null,
`extra` varchar(200) DEFAULT '' not null,
primary key(`asid`) primary key(`asid`)
); );

View File

@ -3,5 +3,5 @@ CREATE TABLE `polls_votes` (
`uid` int not null, `uid` int not null,
`option` int DEFAULT 0 not null, `option` int DEFAULT 0 not null,
`castAt` datetime not null, `castAt` datetime not null,
`ipaddress` varchar(200) DEFAULT '0.0.0.0.0' not null `ip` varchar(200) DEFAULT '' not null
) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci; ) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci;

View File

@ -8,7 +8,7 @@ CREATE TABLE `replies` (
`lastEdit` int DEFAULT 0 not null, `lastEdit` int DEFAULT 0 not null,
`lastEditBy` int DEFAULT 0 not null, `lastEditBy` int DEFAULT 0 not null,
`lastUpdated` datetime not null, `lastUpdated` datetime not null,
`ipaddress` varchar(200) DEFAULT '0.0.0.0.0' not null, `ip` varchar(200) DEFAULT '' not null,
`likeCount` int DEFAULT 0 not null, `likeCount` int DEFAULT 0 not null,
`attachCount` int DEFAULT 0 not null, `attachCount` int DEFAULT 0 not null,
`words` int DEFAULT 1 not null, `words` int DEFAULT 1 not null,

View File

@ -11,7 +11,7 @@ CREATE TABLE `topics` (
`is_closed` boolean DEFAULT 0 not null, `is_closed` boolean DEFAULT 0 not null,
`sticky` boolean DEFAULT 0 not null, `sticky` boolean DEFAULT 0 not null,
`parentID` int DEFAULT 2 not null, `parentID` int DEFAULT 2 not null,
`ipaddress` varchar(200) DEFAULT '0.0.0.0.0' not null, `ip` varchar(200) DEFAULT '' not null,
`postCount` int DEFAULT 1 not null, `postCount` int DEFAULT 1 not null,
`likeCount` int DEFAULT 0 not null, `likeCount` int DEFAULT 0 not null,
`attachCount` int DEFAULT 0 not null, `attachCount` int DEFAULT 0 not null,

View File

@ -7,6 +7,6 @@ CREATE TABLE `users_replies` (
`createdBy` int not null, `createdBy` int not null,
`lastEdit` int DEFAULT 0 not null, `lastEdit` int DEFAULT 0 not null,
`lastEditBy` int DEFAULT 0 not null, `lastEditBy` int DEFAULT 0 not null,
`ipaddress` varchar(200) DEFAULT '0.0.0.0.0' not null, `ip` varchar(200) DEFAULT '' not null,
primary key(`rid`) primary key(`rid`)
) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci; ) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci;

View File

@ -27,8 +27,8 @@ INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (3,2,'{"ViewT
INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (4,2,'{"ViewTopic":true}'); INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (4,2,'{"ViewTopic":true}');
INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (5,2,'{"ViewTopic":true}'); INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (5,2,'{"ViewTopic":true}');
INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (6,2,'{"ViewTopic":true}'); INSERT INTO "forums_permissions"("gid","fid","permissions") VALUES (6,2,'{"ViewTopic":true}');
INSERT INTO "topics"("title","content","parsed_content","createdAt","lastReplyAt","lastReplyBy","createdBy","parentID","ipaddress") VALUES ('Test Topic','A topic automatically generated by the software.','A topic automatically generated by the software.',UTC_TIMESTAMP(),UTC_TIMESTAMP(),1,1,2,'::1'); INSERT INTO "topics"("title","content","parsed_content","createdAt","lastReplyAt","lastReplyBy","createdBy","parentID","ip") VALUES ('Test Topic','A topic automatically generated by the software.','A topic automatically generated by the software.',UTC_TIMESTAMP(),UTC_TIMESTAMP(),1,1,2,'::1');
INSERT INTO "replies"("tid","content","parsed_content","createdAt","createdBy","lastUpdated","lastEdit","lastEditBy","ipaddress") VALUES (1,'A reply!','A reply!',UTC_TIMESTAMP(),1,UTC_TIMESTAMP(),0,0,'::1'); INSERT INTO "replies"("tid","content","parsed_content","createdAt","createdBy","lastUpdated","lastEdit","lastEditBy","ip") VALUES (1,'A reply!','A reply!',UTC_TIMESTAMP(),1,UTC_TIMESTAMP(),0,0,'::1');
INSERT INTO "menus"() VALUES (); INSERT INTO "menus"() VALUES ();
INSERT INTO "menu_items"("mid","name","htmlID","position","path","aria","tooltip","order") VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0); INSERT INTO "menu_items"("mid","name","htmlID","position","path","aria","tooltip","order") VALUES (1,'{lang.menu_forums}','menu_forums','left','/forums/','{lang.menu_forums_aria}','{lang.menu_forums_tooltip}',0);
INSERT INTO "menu_items"("mid","name","htmlID","cssClass","position","path","aria","tooltip","order") VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1); INSERT INTO "menu_items"("mid","name","htmlID","cssClass","position","path","aria","tooltip","order") VALUES (1,'{lang.menu_topics}','menu_topics','menu_topics','left','/topics/','{lang.menu_topics_aria}','{lang.menu_topics_tooltip}',1);

View File

@ -6,5 +6,6 @@ CREATE TABLE "activity_stream" (
`elementType` varchar (50) not null, `elementType` varchar (50) not null,
`elementID` int not null, `elementID` int not null,
`createdAt` timestamp not null, `createdAt` timestamp not null,
`extra` varchar (200) DEFAULT '' not null,
primary key(`asid`) primary key(`asid`)
); );

View File

@ -3,5 +3,5 @@ CREATE TABLE "polls_votes" (
`uid` int not null, `uid` int not null,
`option` int DEFAULT 0 not null, `option` int DEFAULT 0 not null,
`castAt` timestamp not null, `castAt` timestamp not null,
`ipaddress` varchar (200) DEFAULT '0.0.0.0.0' not null `ip` varchar (200) DEFAULT '' not null
); );

View File

@ -8,7 +8,7 @@ CREATE TABLE "replies" (
`lastEdit` int DEFAULT 0 not null, `lastEdit` int DEFAULT 0 not null,
`lastEditBy` int DEFAULT 0 not null, `lastEditBy` int DEFAULT 0 not null,
`lastUpdated` timestamp not null, `lastUpdated` timestamp not null,
`ipaddress` varchar (200) DEFAULT '0.0.0.0.0' not null, `ip` varchar (200) DEFAULT '' not null,
`likeCount` int DEFAULT 0 not null, `likeCount` int DEFAULT 0 not null,
`attachCount` int DEFAULT 0 not null, `attachCount` int DEFAULT 0 not null,
`words` int DEFAULT 1 not null, `words` int DEFAULT 1 not null,

View File

@ -11,7 +11,7 @@ CREATE TABLE "topics" (
`is_closed` boolean DEFAULT 0 not null, `is_closed` boolean DEFAULT 0 not null,
`sticky` boolean DEFAULT 0 not null, `sticky` boolean DEFAULT 0 not null,
`parentID` int DEFAULT 2 not null, `parentID` int DEFAULT 2 not null,
`ipaddress` varchar (200) DEFAULT '0.0.0.0.0' not null, `ip` varchar (200) DEFAULT '' not null,
`postCount` int DEFAULT 1 not null, `postCount` int DEFAULT 1 not null,
`likeCount` int DEFAULT 0 not null, `likeCount` int DEFAULT 0 not null,
`attachCount` int DEFAULT 0 not null, `attachCount` int DEFAULT 0 not null,

View File

@ -7,6 +7,6 @@ CREATE TABLE "users_replies" (
`createdBy` int not null, `createdBy` int not null,
`lastEdit` int DEFAULT 0 not null, `lastEdit` int DEFAULT 0 not null,
`lastEditBy` int DEFAULT 0 not null, `lastEditBy` int DEFAULT 0 not null,
`ipaddress` varchar (200) DEFAULT '0.0.0.0.0' not null, `ip` varchar (200) DEFAULT '' not null,
primary key(`rid`) primary key(`rid`)
); );

View File

@ -83,7 +83,9 @@
<div class="controls button_container{{if .Topic.LikeCount}} has_likes{{end}}"> <div class="controls button_container{{if .Topic.LikeCount}} has_likes{{end}}">
<div class="action_button_left"> <div class="action_button_left">
{{if .CurrentUser.Loggedin}} {{if .CurrentUser.Loggedin}}
{{if .CurrentUser.Perms.LikeItem}}{{if ne .CurrentUser.ID .Topic.CreatedBy}}<a href="/topic/like/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}" class="action_button like_item {{if .Topic.Liked}}remove{{else}}add{{end}}_like" aria-label="{{lang "topic.like_aria"}}" data-action="like"></a>{{end}}{{end}} {{if .CurrentUser.Perms.LikeItem}}{{if ne .CurrentUser.ID .Topic.CreatedBy}}
{{if .Topic.Liked}}<a href="/topic/unlike/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}" class="action_button like_item remove_like" aria-label="{{lang "topic.unlike_aria"}}" data-action="unlike"></a>{{else}}<a href="/topic/like/submit/{{.Topic.ID}}?s={{.CurrentUser.Session}}" class="action_button like_item add_like" aria-label="{{lang "topic.like_aria"}}" data-action="like"></a>{{end}}
{{end}}{{end}}
<a href="" class="action_button quote_item" aria-label="{{lang "topic.quote_aria"}}" data-action="quote"></a> <a href="" class="action_button quote_item" aria-label="{{lang "topic.quote_aria"}}" data-action="quote"></a>
{{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}} {{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}}
{{if .CurrentUser.Perms.EditTopic}}<a href="/topic/edit/{{.Topic.ID}}" class="action_button open_edit" aria-label="{{lang "topic.edit_aria"}}" data-action="edit"></a>{{end}} {{if .CurrentUser.Perms.EditTopic}}<a href="/topic/edit/{{.Topic.ID}}" class="action_button open_edit" aria-label="{{lang "topic.edit_aria"}}" data-action="edit"></a>{{end}}

View File

@ -2,7 +2,6 @@ package main
import ( import (
"database/sql" "database/sql"
"errors"
"log" "log"
"strconv" "strconv"
"sync/atomic" "sync/atomic"
@ -10,6 +9,7 @@ import (
c "github.com/Azareal/Gosora/common" c "github.com/Azareal/Gosora/common"
qgen "github.com/Azareal/Gosora/query_gen" qgen "github.com/Azareal/Gosora/query_gen"
"github.com/pkg/errors"
) )
// TODO: Name the tasks so we can figure out which one it was when something goes wrong? Or maybe toss it up WithStack down there? // TODO: Name the tasks so we can figure out which one it was when something goes wrong? Or maybe toss it up WithStack down there?
@ -189,7 +189,7 @@ func dailies() {
if c.Config.DisablePostIP { if c.Config.DisablePostIP {
f := func(tbl string) { f := func(tbl string) {
_, err := qgen.NewAcc().Update(tbl).Set("ipaddress='0'").Where("ipaddress!='0'").Exec() _, err := qgen.NewAcc().Update(tbl).Set("ip='0'").Where("ip!='0'").Exec()
if err != nil { if err != nil {
c.LogError(err) c.LogError(err)
} }
@ -200,7 +200,7 @@ func dailies() {
} else if c.Config.PostIPCutoff > -1 { } else if c.Config.PostIPCutoff > -1 {
// TODO: Use unixtime to remove this MySQLesque logic? // TODO: Use unixtime to remove this MySQLesque logic?
f := func(tbl string) { f := func(tbl string) {
_, err := qgen.NewAcc().Update(tbl).Set("ipaddress='0'").DateOlderThan("createdAt", c.Config.PostIPCutoff, "day").Where("ipaddress!='0'").Exec() _, err := qgen.NewAcc().Update(tbl).Set("ip='0'").DateOlderThan("createdAt", c.Config.PostIPCutoff, "day").Where("ip!='0'").Exec()
if err != nil { if err != nil {
c.LogError(err) c.LogError(err)
} }
@ -211,13 +211,13 @@ func dailies() {
} }
if c.Config.DisablePollIP { if c.Config.DisablePollIP {
_, err := qgen.NewAcc().Update("polls_votes").Set("ipaddress='0'").Where("ipaddress!='0'").Exec() _, err := qgen.NewAcc().Update("polls_votes").Set("ip='0'").Where("ip!='0'").Exec()
if err != nil { if err != nil {
c.LogError(err) c.LogError(err)
} }
} else if c.Config.PollIPCutoff > -1 { } else if c.Config.PollIPCutoff > -1 {
// TODO: Use unixtime to remove this MySQLesque logic? // TODO: Use unixtime to remove this MySQLesque logic?
_, err := qgen.NewAcc().Update("polls_votes").Set("ipaddress='0'").DateOlderThan("castAt", c.Config.PollIPCutoff, "day").Where("ipaddress!='0'").Exec() _, err := qgen.NewAcc().Update("polls_votes").Set("ip='0'").DateOlderThan("castAt", c.Config.PollIPCutoff, "day").Where("ip!='0'").Exec()
if err != nil { if err != nil {
c.LogError(err) c.LogError(err)
} }
@ -237,7 +237,7 @@ func dailies() {
c.LogError(err) c.LogError(err)
}*/ }*/
mon := time.Now().Month() mon := time.Now().Month()
_, err := qgen.NewAcc().Update("users").Set("last_ip=0").Where("last_ip!=0 AND last_ip NOT LIKE '" + strconv.Itoa(int(mon)) + "-%'").Exec() _, err := qgen.NewAcc().Update("users").Set("last_ip=0").Where("last_ip!='0' AND last_ip NOT LIKE '" + strconv.Itoa(int(mon)) + "-%'").Exec()
if err != nil { if err != nil {
c.LogError(err) c.LogError(err)
} }
@ -250,3 +250,47 @@ func dailies() {
} }
} }
} }
func sched() error {
schedStr, err := c.Meta.Get("sched")
// TODO: Report this error back correctly...
if err != nil && err != sql.ErrNoRows {
return errors.WithStack(err)
}
if schedStr == "recalc" {
log.Print("Cleaning up orphaned data.")
count, err := c.Recalc.Replies()
if err != nil {
return errors.WithStack(err)
}
log.Printf("Deleted %d orphaned replies.", count)
count, err = c.Recalc.Subscriptions()
if err != nil {
return errors.WithStack(err)
}
log.Printf("Deleted %d orphaned subscriptions.", count)
count, err = c.Recalc.ActivityStream()
if err != nil {
return errors.WithStack(err)
}
log.Printf("Deleted %d orphaned activity stream items.", count)
err = c.Recalc.Users()
if err != nil {
return errors.WithStack(err)
}
log.Print("Recalculated user post stats.")
count, err = c.Recalc.Attachments()
if err != nil {
return errors.WithStack(err)
}
log.Printf("Deleted %d orphaned attachments.", count)
}
return nil
}