From 69356378674a04c563c7b70527f0486f9230437f Mon Sep 17 00:00:00 2001 From: Azareal Date: Fri, 31 Jan 2020 17:22:08 +1000 Subject: [PATCH] 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. --- cmd/elasticsearch/setup.go | 4 +- cmd/query_gen/main.go | 4 +- cmd/query_gen/tables.go | 12 +- common/activity_stream.go | 69 ++++++-- common/alerts.go | 57 ++++--- common/attachments.go | 22 +-- common/audit_logs.go | 12 +- common/common.go | 31 +++- common/conversations.go | 37 ++--- common/email.go | 20 +-- common/email_store.go | 22 +-- common/files.go | 8 +- common/forum.go | 15 +- common/ip_search.go | 6 +- common/likes.go | 14 +- common/menu_store.go | 4 +- common/{ => meta}/meta_store.go | 11 +- common/misc_logs.go | 16 +- common/page_store.go | 12 +- common/parser.go | 8 +- common/permissions.go | 12 +- common/pluginlangs.go | 11 +- common/poll.go | 2 +- common/poll_store.go | 4 +- common/profile_reply.go | 14 +- common/profile_reply_store.go | 15 +- common/ratelimit.go | 4 +- common/recalc.go | 177 ++++++++++++++++++++ common/reply.go | 14 +- common/reply_store.go | 76 +++++++-- common/report_store.go | 8 +- common/subscription.go | 26 ++- common/theme_list.go | 6 +- common/thumbnailer.go | 16 +- common/topic.go | 137 +++++++++++----- common/topic_store.go | 77 +++++---- common/user.go | 95 +++++++++-- common/user_store.go | 34 +++- common/widget.go | 14 +- common/word_filters.go | 14 +- gen_router.go | 214 +++++++++++++------------ general_test.go | 10 +- main.go | 13 +- misc_test.go | 18 +-- patcher/patches.go | 110 ++++++++++++- query_gen/builder.go | 204 ++++++++++++----------- query_gen/mssql.go | 27 +++- query_gen/mysql.go | 103 ++++++++---- query_gen/pgsql.go | 53 ++++-- query_gen/querygen.go | 32 ++-- router_gen/routes.go | 1 + routes.go | 2 +- routes/profile_reply.go | 4 +- routes/reply.go | 2 +- routes/topic.go | 56 ++++++- schema/mssql/inserts.sql | 4 +- schema/mssql/query_activity_stream.sql | 1 + schema/mssql/query_polls_votes.sql | 2 +- schema/mssql/query_replies.sql | 2 +- schema/mssql/query_topics.sql | 2 +- schema/mssql/query_users_replies.sql | 2 +- schema/mysql/inserts.sql | 4 +- schema/mysql/query_activity_stream.sql | 1 + schema/mysql/query_polls_votes.sql | 2 +- schema/mysql/query_replies.sql | 2 +- schema/mysql/query_topics.sql | 2 +- schema/mysql/query_users_replies.sql | 2 +- schema/pgsql/inserts.sql | 4 +- schema/pgsql/query_activity_stream.sql | 1 + schema/pgsql/query_polls_votes.sql | 2 +- schema/pgsql/query_replies.sql | 2 +- schema/pgsql/query_topics.sql | 2 +- schema/pgsql/query_users_replies.sql | 2 +- templates/topic_alt.html | 4 +- tickloop.go | 56 ++++++- 75 files changed, 1488 insertions(+), 600 deletions(-) rename common/{ => meta}/meta_store.go (84%) create mode 100644 common/recalc.go diff --git a/cmd/elasticsearch/setup.go b/cmd/elasticsearch/setup.go index 5ec91683..8c219721 100644 --- a/cmd/elasticsearch/setup.go +++ b/cmd/elasticsearch/setup.go @@ -197,7 +197,7 @@ func setupData(client *elastic.Client) error { } 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{} err := rows.Scan(&t.ID, &t.Title, &t.Content, &t.CreatedBy, &t.IP) if err != nil { @@ -233,7 +233,7 @@ func setupData(client *elastic.Client) error { rf(rin[i]) } 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{} err := rows.Scan(&r.ID, &r.TID, &r.Content, &r.CreatedBy, &r.IP) if err != nil { diff --git a/cmd/query_gen/main.go b/cmd/query_gen/main.go index a12d5bb9..a01f6d84 100644 --- a/cmd/query_gen/main.go +++ b/cmd/query_gen/main.go @@ -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", "", "") diff --git a/cmd/query_gen/tables.go b/cmd/query_gen/tables.go index f125e67c..29938a30 100644 --- a/cmd/query_gen/tables.go +++ b/cmd/query_gen/tables.go @@ -39,7 +39,8 @@ func createTables(adapter qgen.Adapter) (err error) { // TODO: Drop these columns? tC{"url_prefix", "varchar", 20, false, false, "''"}, tC{"url_name", "varchar", 100, false, false, "''"}, - + //tC{"pub_key", "text", 0, false, false, "''"}, + tC{"level", "smallint", 0, false, false, "0"}, tC{"score", "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"}, // TODO: Add an index for this 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{"likeCount", "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{"lastEditBy", "int", 0, false, false, "0"}, 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{"attachCount", "int", 0, false, false, "0"}, 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{"option", "int", 0, false, false, "0"}, tC{"castAt", "createdAt", 0, false, false, ""}, - tC{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, + tC{"ip", "varchar", 200, false, false, "''"}, }, 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{"lastEdit", "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{"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{"elementID", "int", 0, false, false, ""}, /* the ID of the element being acted upon */ tC{"createdAt", "createdAt", 0, false, false, ""}, + tC{"extra", "varchar", 200, false, false, "''"}, }, []tblKey{ tblKey{"asid", "primary", "", false}, diff --git a/common/activity_stream.go b/common/activity_stream.go index 5b4ab188..a35d03f1 100644 --- a/common/activity_stream.go +++ b/common/activity_stream.go @@ -1,47 +1,92 @@ package common -import "database/sql" -import "github.com/Azareal/Gosora/query_gen" +import ( + "database/sql" + + qgen "github.com/Azareal/Gosora/query_gen" +) var Activity ActivityStream type ActivityStream interface { Add(a Alert) (int, 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) } type DefaultActivityStream struct { - add *sql.Stmt - get *sql.Stmt - count *sql.Stmt + add *sql.Stmt + get *sql.Stmt + delete *sql.Stmt + deleteByParams *sql.Stmt + deleteByParamsExtra *sql.Stmt + aidsByParamsExtra *sql.Stmt + count *sql.Stmt } func NewDefaultActivityStream(acc *qgen.Accumulator) (*DefaultActivityStream, error) { as := "activity_stream" return &DefaultActivityStream{ - add: acc.Insert(as).Columns("actor, targetUser, event, elementType, elementID, createdAt").Fields("?,?,?,?,?,UTC_TIMESTAMP()").Prepare(), - get: acc.Select(as).Columns("actor, targetUser, event, elementType, elementID, createdAt").Where("asid = ?").Prepare(), - count: acc.Count(as).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,extra").Where("asid=?").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() } 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 { return 0, err } - lastID, err := res.LastInsertId() return int(lastID), err } func (s *DefaultActivityStream) Get(id int) (Alert, error) { 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 } +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 // Count returns the total number of activity stream items func (s *DefaultActivityStream) Count() (count int) { @@ -50,4 +95,4 @@ func (s *DefaultActivityStream) Count() (count int) { LogError(err) } return count -} \ No newline at end of file +} diff --git a/common/alerts.go b/common/alerts.go index fb28bf73..d7a62eb5 100644 --- a/common/alerts.go +++ b/common/alerts.go @@ -12,10 +12,11 @@ import ( "strconv" "strings" "time" + //"fmt" "github.com/Azareal/Gosora/common/phrases" - "github.com/Azareal/Gosora/query_gen" + qgen "github.com/Azareal/Gosora/query_gen" ) type Alert struct { @@ -25,15 +26,16 @@ type Alert struct { Event string ElementType string ElementID int - CreatedAt time.Time + CreatedAt time.Time + Extra string Actor *User } type AlertStmts struct { - notifyWatchers *sql.Stmt - notifyOne *sql.Stmt - getWatchers *sql.Stmt + notifyWatchers *sql.Stmt + notifyOne *sql.Stmt + getWatchers *sql.Stmt } var alertStmts AlertStmts @@ -45,10 +47,10 @@ func init() { alertStmts = AlertStmts{ notifyWatchers: acc.SimpleInsertInnerJoin( 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(), - 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 = ?", "", ""), + 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=?", "", ""), } return acc.FirstError() }) @@ -123,6 +125,7 @@ func BuildAlert(alert Alert, user User /* The current user */) (out string, err case "post": topic, err := TopicByReplyID(alert.ElementID) 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")) } 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 } -func buildAlertString(msg string, sub []string, path string, avatar string, asid int) string { - var substring string +func buildAlertString(msg string, sub []string, path, avatar string, asid int) string { + var subString string for _, item := range sub { - substring += "\"" + escapeTextInJson(item) + "\"," + subString += "\"" + escapeTextInJson(item) + "\"," } - if len(substring) > 0 { - substring = substring[:len(substring)-1] + if len(subString) > 0 { + 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 { - id, err := Activity.Add(Alert{ActorID: actor, TargetUserID: targetUser, Event: event, ElementType: elementType, ElementID: elementID}) +func AddActivityAndNotifyAll(a Alert) error { + id, err := Activity.Add(a) if err != nil { return err } return NotifyWatchers(id) } -func AddActivityAndNotifyTarget(alert Alert) error { - id, err := Activity.Add(alert) +func AddActivityAndNotifyTarget(a Alert) error { + id, err := Activity.Add(a) if err != nil { return err } - err = NotifyOne(alert.TargetUserID, id) + err = NotifyOne(a.TargetUserID, id) if err != nil { return err } - alert.ASID = id + a.ASID = id // Live alerts, if the target is online and WebSockets is enabled if EnableWebsockets { - go func() { - _ = WsHub.pushAlert(alert.TargetUserID, alert) - //fmt.Println("err:",err) - }() + go func() { + _ = WsHub.pushAlert(a.TargetUserID, a) + //fmt.Println("err:",err) + }() } return nil } -func NotifyOne(watcher int, asid int) error { +func NotifyOne(watcher, asid int) error { _, err := alertStmts.notifyOne.Exec(watcher, asid) return err } @@ -236,3 +239,7 @@ func notifyWatchers(asid int) { } _ = WsHub.pushAlerts(uids, alert) } + +func DismissAlert(uid, aid int) { + _ = WsHub.PushMessage(uid, `{"event":"dismiss-alert","id":`+strconv.Itoa(aid)+`}`) +} \ No newline at end of file diff --git a/common/attachments.go b/common/attachments.go index de9a8af5..27acce02 100644 --- a/common/attachments.go +++ b/common/attachments.go @@ -3,8 +3,8 @@ package common import ( "database/sql" "errors" - "strings" "os" + "strings" qgen "github.com/Azareal/Gosora/query_gen" ) @@ -28,12 +28,12 @@ type AttachmentStore interface { MiniGetList(originTable string, originID int) (alist []*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) - MoveTo(sectionID int, originID int, originTable string) error + MoveTo(sectionID, originID int, originTable string) error MoveToByExtra(sectionID int, originTable, extra string) error Count() int CountIn(originTable string, oid int) int CountInPath(path string) int - Delete(aid int) error + Delete(id int) error } 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(), add: acc.Insert(a).Columns("sectionID, sectionTable, originID, originTable, uploadedBy, path, extra").Fields("?,?,?,?,?,?,?").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(), - move: acc.Update(a).Set("sectionID = ?").Where("originID = ? AND originTable = ?").Prepare(), - moveByExtra: acc.Update(a).Set("sectionID = ?").Where("originTable = ? AND extra = ?").Prepare(), + move: acc.Update(a).Set("sectionID=?").Where("originID=? AND originTable=?").Prepare(), + moveByExtra: acc.Update(a).Set("sectionID=?").Where("originTable=? AND extra=?").Prepare(), delete: acc.Delete(a).Where("attachID=?").Prepare(), }, acc.FirstError() } @@ -151,7 +151,7 @@ func (s *DefaultAttachmentStore) Add(sectionID int, sectionTable string, originI 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) return err } @@ -185,8 +185,8 @@ func (s *DefaultAttachmentStore) CountInPath(path string) (count int) { return count } -func (s *DefaultAttachmentStore) Delete(aid int) error { - _, err := s.delete.Exec(aid) +func (s *DefaultAttachmentStore) Delete(id int) error { + _, err := s.delete.Exec(id) return err } @@ -208,6 +208,6 @@ func DeleteAttachment(aid int) error { return err } } - + return nil -} \ No newline at end of file +} diff --git a/common/audit_logs.go b/common/audit_logs.go index 21688f88..2db5e1ad 100644 --- a/common/audit_logs.go +++ b/common/audit_logs.go @@ -21,8 +21,8 @@ type LogItem struct { } type LogStore interface { - Create(action string, elementID int, elementType string, ip string, actorID int) (err error) - CreateExtra(action string, elementID int, elementType string, ip string, actorID int, extra string) (err error) + Create(action string, elementID int, elementType, ip string, actorID int) (err error) + CreateExtra(action string, elementID int, elementType, ip string, actorID int, extra string) (err error) Count() int 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? -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, "") } -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) return err } @@ -99,11 +99,11 @@ func NewAdminLogStore(acc *qgen.Accumulator) (*SQLAdminLogStore, error) { } // 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, "") } -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) return err } diff --git a/common/common.go b/common/common.go index e9bb1e4a..c435ad9e 100644 --- a/common/common.go +++ b/common/common.go @@ -14,11 +14,14 @@ import ( "sync/atomic" "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 Meta meta.MetaStore + // nolint I don't want to write comments for each of these o.o const Hour int = 60 * 60 const Day int = Hour * 24 @@ -57,7 +60,7 @@ type StringList []string // 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 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 @@ -68,7 +71,7 @@ var AllowedFileExts = StringList{ "bz2", "zip", "zipx", "gz", "7z", "tar", "cab", "rar", "kgb", "pea", "xz", "zz", // archives } 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{ "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{}) { 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() +} \ No newline at end of file diff --git a/common/conversations.go b/common/conversations.go index 3c2ac3e6..54233aed 100644 --- a/common/conversations.go +++ b/common/conversations.go @@ -32,17 +32,18 @@ type ConvoStmts struct { func init() { DbInits.Add(func(acc *qgen.Accumulator) error { + cpo := "conversations_posts" convoStmts = ConvoStmts{ - fetchPost: acc.Select("conversations_posts").Columns("cid, body, post, createdBy").Where("pid = ?").Prepare(), - getPosts: acc.Select("conversations_posts").Columns("pid, body, post, createdBy").Where("cid = ?").Limit("?,?").Prepare(), - countPosts: acc.Count("conversations_posts").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(), - has: acc.Count("conversations_participants").Where("uid = ? AND cid = ?").Prepare(), + fetchPost: acc.Select(cpo).Columns("cid,body,post,createdBy").Where("pid=?").Prepare(), + getPosts: acc.Select(cpo).Columns("pid,body,post,createdBy").Where("cid=?").Limit("?,?").Prepare(), + countPosts: acc.Count(cpo).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(), + has: acc.Count("conversations_participants").Where("uid=? AND cid=?").Prepare(), - editPost: acc.Update("conversations_posts").Set("body = ?, post = ?").Where("pid = ?").Prepare(), - createPost: acc.Insert("conversations_posts").Columns("cid, body, post, createdBy").Fields("?,?,?,?").Prepare(), - deletePost: acc.Delete("conversations_posts").Where("pid = ?").Prepare(), + editPost: acc.Update(cpo).Set("body=?,post=?").Where("pid=?").Prepare(), + createPost: acc.Insert(cpo).Columns("cid,body,post,createdBy").Fields("?,?,?,?").Prepare(), + deletePost: acc.Delete(cpo).Where("pid=?").Prepare(), getUsers: acc.Select("conversations_participants").Columns("uid").Where("cid = ?").Prepare(), } @@ -138,7 +139,7 @@ type ConversationExtra struct { type ConversationStore interface { 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) GetUserCount(uid int) (count int) Delete(id int) error @@ -160,12 +161,12 @@ type DefaultConversationStore struct { func NewDefaultConversationStore(acc *qgen.Accumulator) (*DefaultConversationStore, error) { return &DefaultConversationStore{ - 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", "?,?"), - getUserCount: acc.Count("conversations_participants").Where("uid = ?").Prepare(), - delete: acc.Delete("conversations").Where("cid = ?").Prepare(), - deletePosts: acc.Delete("conversations_posts").Where("cid = ?").Prepare(), - deleteParticipants: acc.Delete("conversations_participants").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", "?,?"), + getUserCount: acc.Count("conversations_participants").Where("uid=?").Prepare(), + delete: acc.Delete("conversations").Where("cid=?").Prepare(), + deletePosts: acc.Delete("conversations_posts").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(), addParticipant: acc.Insert("conversations_participants").Columns("uid, cid").Fields("?,?").Prepare(), count: acc.Count("conversations").Prepare(), @@ -178,7 +179,7 @@ func (s *DefaultConversationStore) Get(id int) (*Conversation, error) { 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) if err != nil { return nil, err @@ -197,7 +198,7 @@ func (s *DefaultConversationStore) GetUser(uid int, offset int) (cos []*Conversa 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) if err != nil { return nil, err diff --git a/common/email.go b/common/email.go index eccf5360..d8f4fc53 100644 --- a/common/email.go +++ b/common/email.go @@ -6,11 +6,11 @@ import ( "net/mail" "net/smtp" "strings" - + 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" if Config.SslSchema { schema += "s" @@ -21,30 +21,30 @@ func SendActivationEmail(username string, email string, token string) error { return SendEmail(email, subject, msg) } -func SendValidationEmail(username string, email string, token string) error { +func SendValidationEmail(username, email, token string) error { schema := "http" if Config.SslSchema { schema += "s" } r := func(body *string) 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") r1 := r(&subject) - r1("name",Site.Name) + r1("name", Site.Name) body := p.GetAccountPhrase("ValidateEmailBody") r2 := r(&body) - r2("username",username) - r2("schema",schema) - r2("url",Site.URL) - r2("token",token) + r2("username", username) + r2("schema", schema) + r2("url", Site.URL) + r2("token", token) return SendEmail(email, subject, body) } // 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? ret, hasHook := GetHookTable().VhookNeedHook("email_send_intercept", email, subject, msg) if hasHook { diff --git a/common/email_store.go b/common/email_store.go index 4e75217f..08abfba7 100644 --- a/common/email_store.go +++ b/common/email_store.go @@ -18,28 +18,28 @@ type Email struct { type EmailStore interface { // TODO: Add an autoincrement key - Get(user *User, email string) (Email, error) - GetEmailsByUser(user *User) (emails []Email, err error) + Get(u *User, email string) (Email, error) + GetEmailsByUser(u *User) (emails []Email, err error) Add(uid int, email, token string) error Delete(uid int, email string) error VerifyEmail(email string) error } type DefaultEmailStore struct { - get *sql.Stmt + get *sql.Stmt getEmailsByUser *sql.Stmt - add *sql.Stmt - delete *sql.Stmt + add *sql.Stmt + delete *sql.Stmt verifyEmail *sql.Stmt } func NewDefaultEmailStore(acc *qgen.Accumulator) (*DefaultEmailStore, error) { e := "emails" 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(), - add: acc.Insert(e).Columns("uid,email,validated,token").Fields("?,?,?,?").Prepare(), - delete: acc.Delete(e).Where("uid=? AND email=?").Prepare(), + add: acc.Insert(e).Columns("uid,email,validated,token").Fields("?,?,?,?").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? 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) { - 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) return e, err } @@ -72,13 +72,13 @@ func (s *DefaultEmailStore) GetEmailsByUser(user *User) (emails []Email, err 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) return err } func (s *DefaultEmailStore) Delete(uid int, email string) error { - _, err := s.delete.Exec(uid,email) + _, err := s.delete.Exec(uid, email) return err } diff --git a/common/files.go b/common/files.go index 6c9d9710..d232f83c 100644 --- a/common/files.go +++ b/common/files.go @@ -59,7 +59,7 @@ func (list SFileList) JSTmplInit() error { tmplName := strings.TrimSuffix(path, ".jgo") 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) } @@ -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 - each := func(phrase string, handle func(index int)) { + each := func(phrase string, h func(index int)) { //fmt.Println("find each '" + phrase + "'") index := endBrace if index < 0 { @@ -121,7 +121,7 @@ func (list SFileList) JSTmplInit() error { if !foundIt { break } - handle(index) + h(index) } } 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) if err != nil { return err diff --git a/common/forum.go b/common/forum.go index 0a239aa3..1bdf8af9 100644 --- a/common/forum.go +++ b/common/forum.go @@ -7,7 +7,7 @@ import ( "strconv" "strings" - "github.com/Azareal/Gosora/query_gen" + qgen "github.com/Azareal/Gosora/query_gen" _ "github.com/go-sql-driver/mysql" ) @@ -27,9 +27,9 @@ type Forum struct { Link string Name string Desc string - Tmpl string + Tmpl string Active bool - Order int + Order int Preset string ParentID int ParentType string @@ -60,8 +60,8 @@ var forumStmts ForumStmts func init() { DbInits.Add(func(acc *qgen.Accumulator) error { forumStmts = ForumStmts{ - update: acc.Update("forums").Set("name = ?, desc = ?, active = ?, preset = ?").Where("fid = ?").Prepare(), - setPreset: acc.Update("forums").Set("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(), } return acc.FirstError() }) @@ -74,7 +74,7 @@ func (f *Forum) Copy() (fcopy Forum) { } // 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 == "" { name = f.Name } @@ -137,6 +137,7 @@ func (sf SortForum) Len() int { func (sf SortForum) Swap(i, j int) { sf[i], sf[j] = sf[j], sf[i] } + /*func (sf SortForum) Less(i,j int) bool { l := sf.less(i,j) 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 -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} } diff --git a/common/ip_search.go b/common/ip_search.go index 59bcf09e..d88f7574 100644 --- a/common/ip_search.go +++ b/common/ip_search.go @@ -25,9 +25,9 @@ func NewDefaultIPSearcher() (*DefaultIPSearcher, error) { uu := "users" return &DefaultIPSearcher{ 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(), - searchReplies: acc.Select(uu).Columns("uid").InQ("uid", acc.Select("replies").Columns("createdBy").Where("ipaddress=?")).Prepare(), - searchUsersReplies: acc.Select(uu).Columns("uid").InQ("uid", acc.Select("users_replies").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("ip=?")).Prepare(), + searchUsersReplies: acc.Select(uu).Columns("uid").InQ("uid", acc.Select("users_replies").Columns("createdBy").Where("ip=?")).Prepare(), }, acc.FirstError() } diff --git a/common/likes.go b/common/likes.go index fce98326..347be566 100644 --- a/common/likes.go +++ b/common/likes.go @@ -10,22 +10,25 @@ var Likes LikeStore type LikeStore interface { BulkExists(ids []int, sentBy int, targetType string) ([]int, error) + Delete(targetID int, targetType string) error Count() (count int) } type DefaultLikeStore struct { - count *sql.Stmt + count *sql.Stmt + delete *sql.Stmt } func NewDefaultLikeStore(acc *qgen.Accumulator) (*DefaultLikeStore, error) { return &DefaultLikeStore{ - count: acc.Count("likes").Prepare(), + count: acc.Count("likes").Prepare(), + delete: acc.Delete("likes").Where("targetItem=? AND targetType=?").Prepare(), }, acc.FirstError() } // TODO: Write a test for this 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 { return nil, nil } else if err != nil { @@ -43,6 +46,11 @@ func (s *DefaultLikeStore) BulkExists(ids []int, sentBy int, targetType string) 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 // Count returns the total number of likes globally func (s *DefaultLikeStore) Count() (count int) { diff --git a/common/menu_store.go b/common/menu_store.go index 0462e496..613c0ea7 100644 --- a/common/menu_store.go +++ b/common/menu_store.go @@ -5,7 +5,7 @@ import ( "strconv" "sync/atomic" - "github.com/Azareal/Gosora/query_gen" + qgen "github.com/Azareal/Gosora/query_gen" ) 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) { - 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} 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 { diff --git a/common/meta_store.go b/common/meta/meta_store.go similarity index 84% rename from common/meta_store.go rename to common/meta/meta_store.go index 6150ea91..e1a54ccc 100644 --- a/common/meta_store.go +++ b/common/meta/meta_store.go @@ -1,14 +1,15 @@ package common -import "database/sql" -import "github.com/Azareal/Gosora/query_gen" +import ( + "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 type MetaStore interface { Get(name string) (val string, err error) - Set(name string, val string) error + Set(name, val string) error } 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 -func (s *DefaultMetaStore) Set(name string, val string) error { +func (s *DefaultMetaStore) Set(name, val string) error { _, err := s.Get(name) if err == sql.ErrNoRows { _, err := s.add.Exec(name) diff --git a/common/misc_logs.go b/common/misc_logs.go index a3ffb115..b34e35bf 100644 --- a/common/misc_logs.go +++ b/common/misc_logs.go @@ -31,7 +31,7 @@ func init() { DbInits.Add(func(acc *qgen.Accumulator) error { rl := "registration_logs" 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(), } return acc.FirstError() @@ -57,7 +57,7 @@ func (l *RegLogItem) Create() (id int, err error) { type RegLogStore interface { Count() (count int) - GetOffset(offset int, perPage int) (logs []RegLogItem, err error) + GetOffset(offset, perPage int) (logs []RegLogItem, err error) } type SQLRegLogStore struct { @@ -120,8 +120,8 @@ func init() { DbInits.Add(func(acc *qgen.Accumulator) error { ll := "login_logs" loginLogStmts = LoginLogStmts{ - update: acc.Update(ll).Set("uid = ?, success = ?").Where("lid = ?").Prepare(), - create: acc.Insert(ll).Columns("uid, success, ipaddress, doneAt").Fields("?,?,?,UTC_TIMESTAMP()").Prepare(), + update: acc.Update(ll).Set("uid=?,success=?").Where("lid=?").Prepare(), + create: acc.Insert(ll).Columns("uid,success,ipaddress,doneAt").Fields("?,?,?,UTC_TIMESTAMP()").Prepare(), } return acc.FirstError() }) @@ -147,7 +147,7 @@ func (l *LoginLogItem) Create() (id int, err error) { type LoginLogStore interface { Count() (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 { @@ -160,8 +160,8 @@ func NewLoginLogStore(acc *qgen.Accumulator) (*SQLLoginLogStore, error) { ll := "login_logs" return &SQLLoginLogStore{ count: acc.Count(ll).Prepare(), - countForUser: acc.Count(ll).Where("uid = ?").Prepare(), - getOffsetByUser: acc.Select(ll).Columns("lid, success, ipaddress, doneAt").Where("uid = ?").Orderby("doneAt DESC").Limit("?,?").Prepare(), + countForUser: acc.Count(ll).Where("uid=?").Prepare(), + getOffsetByUser: acc.Select(ll).Columns("lid,success,ipaddress,doneAt").Where("uid=?").Orderby("doneAt DESC").Limit("?,?").Prepare(), }, acc.FirstError() } @@ -181,7 +181,7 @@ func (s *SQLLoginLogStore) CountUser(uid int) (count int) { 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) if err != nil { return logs, err diff --git a/common/page_store.go b/common/page_store.go index e51eec19..6a22db90 100644 --- a/common/page_store.go +++ b/common/page_store.go @@ -18,8 +18,8 @@ var customPageStmts CustomPageStmts func init() { DbInits.Add(func(acc *qgen.Accumulator) error { customPageStmts = CustomPageStmts{ - 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(), + 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(), } return acc.FirstError() }) @@ -74,7 +74,7 @@ type PageStore interface { Count() (count int) Get(id int) (*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 Delete(id int) error } @@ -91,11 +91,11 @@ type DefaultPageStore struct { func NewDefaultPageStore(acc *qgen.Accumulator) (*DefaultPageStore, error) { pa := "pages" return &DefaultPageStore{ - 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(), + 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(), getOffset: acc.Select(pa).Columns("pid, name, title, body, allowedGroups, menuID").Orderby("pid DESC").Limit("?,?").Prepare(), count: acc.Count(pa).Prepare(), - delete: acc.Delete(pa).Where("pid = ?").Prepare(), + delete: acc.Delete(pa).Where("pid=?").Prepare(), }, acc.FirstError() } diff --git a/common/parser.go b/common/parser.go index 53f71fa9..d851ee17 100644 --- a/common/parser.go +++ b/common/parser.go @@ -108,7 +108,7 @@ type TagToAction struct { } // 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 if i < len(runes) { return i, true @@ -117,7 +117,7 @@ func tryStepForward(i int, step int, runes []rune) (int, bool) { } // 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 { return i, false } @@ -369,7 +369,7 @@ func PreparseMessage(msg string) string { // TODO: Test this // 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) { return runes[cur+skip] } @@ -972,7 +972,7 @@ func parseMediaString(data string, settings *ParseSettings) (media MediaEmbed, o } // 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) { return 0, 1 } diff --git a/common/permissions.go b/common/permissions.go index 405f1d38..6346b9b6 100644 --- a/common/permissions.go +++ b/common/permissions.go @@ -174,7 +174,7 @@ func PresetToLang(preset string) string { // TODO: Is this racey? // TODO: Test this along with the rest of the perms system -func RebuildGroupPermissions(group *Group) error { +func RebuildGroupPermissions(g *Group) error { var permstr []byte log.Print("Reloading a group") @@ -185,7 +185,7 @@ func RebuildGroupPermissions(group *Group) error { } defer getGroupPerms.Close() - err = getGroupPerms.QueryRow(group.ID).Scan(&permstr) + err = getGroupPerms.QueryRow(g.ID).Scan(&permstr) if err != nil { return err } @@ -197,15 +197,15 @@ func RebuildGroupPermissions(group *Group) error { if err != nil { return err } - group.Perms = tmpPerms + g.Perms = tmpPerms return nil } -func OverridePerms(perms *Perms, status bool) { +func OverridePerms(p *Perms, status bool) { if status { - *perms = AllPerms + *p = AllPerms } else { - *perms = BlankPerms + *p = BlankPerms } } diff --git a/common/pluginlangs.go b/common/pluginlangs.go index 7a8d2bab..6762835c 100644 --- a/common/pluginlangs.go +++ b/common/pluginlangs.go @@ -66,18 +66,17 @@ func InitPluginLangs() error { continue } - e := func(field string, name string) error { - return errors.New("The "+field+" field must not be blank on plugin '" + name + "'") + e := func(field, name string) error { + return errors.New("The " + field + " field must not be blank on plugin '" + name + "'") } - if plugin.UName == "" { - return e("UName",pluginItem) + return e("UName", pluginItem) } if plugin.Name == "" { - return e("Name",pluginItem) + return e("Name", pluginItem) } if plugin.Author == "" { - return e("Author",pluginItem) + return e("Author", pluginItem) } if plugin.Main == "" { return errors.New("Couldn't find a main file for plugin '" + pluginItem + "'") diff --git a/common/poll.go b/common/poll.go index 4a82fa65..9e8095c1 100644 --- a/common/poll.go +++ b/common/poll.go @@ -22,7 +22,7 @@ type Poll struct { 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 } diff --git a/common/poll_store.go b/common/poll_store.go index f44215aa..7753ab91 100644 --- a/common/poll_store.go +++ b/common/poll_store.go @@ -61,9 +61,9 @@ func NewDefaultPollStore(cache PollCache) (*DefaultPollStore, error) { exists: acc.Select("polls").Columns("pollID").Where("pollID = ?").Prepare(), createPoll: acc.Insert("polls").Columns("parentID, parentTable, type, options").Fields("?,?,?,?").Prepare(), createPollOption: acc.Insert("polls_options").Columns("pollID, option, votes").Fields("?,?,0").Prepare(), - addVote: acc.Insert("polls_votes").Columns("pollID, uid, option, castAt, ipaddress").Fields("?,?,?,UTC_TIMESTAMP(),?").Prepare(), + 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(), - 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", "", ""), }, acc.FirstError() } diff --git a/common/profile_reply.go b/common/profile_reply.go index 387da248..e4cb44e5 100644 --- a/common/profile_reply.go +++ b/common/profile_reply.go @@ -3,6 +3,7 @@ package common import ( "database/sql" "html" + "strconv" "time" qgen "github.com/Azareal/Gosora/query_gen" @@ -45,9 +46,20 @@ func BlankProfileReply(id int) *ProfileReply { } // TODO: Write tests for this -// TODO: Remove alerts. func (r *ProfileReply) Delete() error { _, 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 } diff --git a/common/profile_reply_store.go b/common/profile_reply_store.go index b59a8cc0..542d2ca1 100644 --- a/common/profile_reply_store.go +++ b/common/profile_reply_store.go @@ -10,6 +10,7 @@ var Prstore ProfileReplyStore type ProfileReplyStore interface { Get(id int) (*ProfileReply, error) + Exists(id int) bool Create(profileID int, content string, createdBy int, ip string) (id int, err error) Count() (count int) } @@ -18,6 +19,7 @@ type ProfileReplyStore interface { // TODO: Add more methods to this like Create() type SQLProfileReplyStore struct { get *sql.Stmt + exists *sql.Stmt create *sql.Stmt count *sql.Stmt } @@ -25,8 +27,9 @@ type SQLProfileReplyStore struct { func NewSQLProfileReplyStore(acc *qgen.Accumulator) (*SQLProfileReplyStore, error) { ur := "users_replies" return &SQLProfileReplyStore{ - get: acc.Select(ur).Columns("uid, content, createdBy, createdAt, lastEdit, lastEditBy, ipaddress").Where("rid = ?").Prepare(), - create: acc.Insert(ur).Columns("uid, content, parsed_content, createdAt, createdBy, ipaddress").Fields("?,?,?,UTC_TIMESTAMP(),?,?").Prepare(), + get: acc.Select(ur).Columns("uid, content, createdBy, createdAt, lastEdit, lastEditBy, ip").Where("rid=?").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(), }, acc.FirstError() } @@ -37,6 +40,14 @@ func (s *SQLProfileReplyStore) Get(id int) (*ProfileReply, error) { 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) { if Config.DisablePostIP { ip = "0" diff --git a/common/ratelimit.go b/common/ratelimit.go index dfb0080b..0c7d3226 100644 --- a/common/ratelimit.go +++ b/common/ratelimit.go @@ -12,7 +12,7 @@ var ErrExceededRateLimit = errors.New("You're exceeding a rate limit. Please wai // TODO: Persist rate limits to disk type RateLimiter interface { - LimitIP(limit string, ip string) error + LimitIP(limit, ip string) 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] if !ok { return ErrBadRateLimiter diff --git a/common/recalc.go b/common/recalc.go new file mode 100644 index 00000000..43c65d3e --- /dev/null +++ b/common/recalc.go @@ -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 +} diff --git a/common/reply.go b/common/reply.go index f4d76880..d84535de 100644 --- a/common/reply.go +++ b/common/reply.go @@ -11,6 +11,7 @@ import ( "errors" "html" "time" + "strconv" qgen "github.com/Azareal/Gosora/query_gen" ) @@ -69,6 +70,8 @@ type ReplyStmts struct { updateTopicReplies *sql.Stmt updateTopicReplies2 *sql.Stmt + + getAidsOfReply *sql.Stmt } 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. 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(), + + getAidsOfReply: acc.Select("attachments").Columns("attachID").Where("originID=? AND originTable='replies'").Prepare(), } return acc.FirstError() }) @@ -120,7 +125,6 @@ func (r *Reply) Like(uid int) (err error) { } // TODO: Refresh topic list? -// TODO: Restructure alerts so we can delete the "x replied to topic" ones too. func (r *Reply) Delete() error { creator, err := Users.Get(r.CreatedBy) if err == nil { @@ -158,6 +162,14 @@ func (r *Reply) Delete() error { if err != nil { 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) if err != nil { return err diff --git a/common/reply_store.go b/common/reply_store.go index 34e022ad..186eadaf 100644 --- a/common/reply_store.go +++ b/common/reply_store.go @@ -11,8 +11,13 @@ var Rstore ReplyStore type ReplyStore interface { 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) + CountUser(uid int) (count int) + CountMegaUser(uid int) (count int) + CountBigUser(uid int) (count int) SetCache(cache ReplyCache) GetCache() ReplyCache @@ -21,9 +26,13 @@ type ReplyStore interface { type SQLReplyStore struct { cache ReplyCache - get *sql.Stmt - create *sql.Stmt - count *sql.Stmt + get *sql.Stmt + getAll *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) { @@ -32,10 +41,14 @@ func NewSQLReplyStore(acc *qgen.Accumulator, cache ReplyCache) (*SQLReplyStore, } re := "replies" return &SQLReplyStore{ - cache: cache, - get: acc.Select(re).Columns("tid, content, createdBy, createdAt, lastEdit, lastEditBy, ipaddress, 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(), - count: acc.Count(re).Prepare(), + cache: cache, + get: acc.Select(re).Columns("tid, content, createdBy, createdAt, lastEdit, lastEditBy, ip, likeCount, attachCount, actionType").Where("rid=?").Prepare(), + getAll: acc.Select(re).Columns("rid,tid, content, createdBy, createdAt, lastEdit, lastEditBy, ip, likeCount, attachCount, actionType").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() } @@ -53,8 +66,38 @@ func (s *SQLReplyStore) Get(id int) (*Reply, error) { 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 -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 { 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 // Count returns the total number of topic replies on these forums func (s *SQLReplyStore) Count() (count int) { - err := s.count.QueryRow().Scan(&count) - if err != nil { - LogError(err) - } - return count + return Countf(s.count) +} +func (s *SQLReplyStore) CountUser(uid int) (count int) { + return Countf(s.countUser, uid) +} +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) { diff --git a/common/report_store.go b/common/report_store.go index 9da5139a..b7360ba1 100644 --- a/common/report_store.go +++ b/common/report_store.go @@ -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 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 { @@ -28,13 +28,13 @@ type DefaultReportStore struct { func NewDefaultReportStore(acc *qgen.Accumulator) (*DefaultReportStore, error) { t := "topics" 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(), - exists: acc.Count(t).Where("data = ? AND data != '' AND parentID = ?").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(), }, 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 -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 err = s.exists.QueryRow(itemType+"_"+strconv.Itoa(itemID), ReportForumID).Scan(&count) if err != nil && err != sql.ErrNoRows { diff --git a/common/subscription.go b/common/subscription.go index c48ebe31..a532df14 100644 --- a/common/subscription.go +++ b/common/subscription.go @@ -10,21 +10,39 @@ var Subscriptions SubscriptionStore // ? Should we have a subscription store for each zone? topic, forum, etc? 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 { - add *sql.Stmt + add *sql.Stmt + delete *sql.Stmt + deleteResource *sql.Stmt } func NewDefaultSubscriptionStore() (*DefaultSubscriptionStore, error) { acc := qgen.NewAcc() + ast := "activity_subscriptions" 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() } -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) 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 +} diff --git a/common/theme_list.go b/common/theme_list.go index 7bb5e87b..5d21594a 100644 --- a/common/theme_list.go +++ b/common/theme_list.go @@ -15,7 +15,7 @@ import ( "sync" "sync/atomic" - "github.com/Azareal/Gosora/query_gen" + qgen "github.com/Azareal/Gosora/query_gen" ) // TODO: Something more thread-safe @@ -44,8 +44,8 @@ func init() { t := "themes" themeStmts = ThemeStmts{ getAll: acc.Select(t).Columns("uname,default").Prepare(), - isDefault: acc.Select(t).Columns("default").Where("uname = ?").Prepare(), - update: acc.Update(t).Set("default = ?").Where("uname = ?").Prepare(), + isDefault: acc.Select(t).Columns("default").Where("uname=?").Prepare(), + update: acc.Update(t).Set("default=?").Where("uname=?").Prepare(), add: acc.Insert(t).Columns("uname,default").Fields("?,?").Prepare(), } return acc.FirstError() diff --git a/common/thumbnailer.go b/common/thumbnailer.go index b2b64e96..ef0cd25d 100644 --- a/common/thumbnailer.go +++ b/common/thumbnailer.go @@ -31,12 +31,12 @@ func ThumbTask(thumbChan chan bool) { // Has the avatar been removed or already been processed by the thumbnailer? 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 } _, err = os.Stat("./uploads/avatar_" + strconv.Itoa(u.ID) + u.RawAvatar) if os.IsNotExist(err) { - _, _ = acc.Delete("users_avatar_queue").Where("uid = ?").Run(uid) + _, _ = acc.Delete("users_avatar_queue").Where("uid=?").Run(uid) return nil } else if err != nil { return errors.WithStack(err) @@ -63,7 +63,7 @@ func ThumbTask(thumbChan chan bool) { if err != nil { 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) }) if err != nil { @@ -86,18 +86,18 @@ func ThumbTask(thumbChan chan bool) { var Thumbnailer ThumbnailerInt 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 { } -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 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 } @@ -109,7 +109,7 @@ func NewCaireThumbnailer() *CaireThumbnailer { return &CaireThumbnailer{} } -func precodeImage(format string, inPath string, tmpPath string) error { +func precodeImage(format, inPath, tmpPath string) error { imageFile, err := os.Open(inPath) if err != nil { return err @@ -139,7 +139,7 @@ func precodeImage(format string, inPath string, tmpPath string) error { 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) if err != nil { return err diff --git a/common/topic.go b/common/topic.go index be760145..2384e6f8 100644 --- a/common/topic.go +++ b/common/topic.go @@ -15,6 +15,8 @@ import ( "strings" "time" + //"log" + p "github.com/Azareal/Gosora/common/phrases" qgen "github.com/Azareal/Gosora/query_gen" ) @@ -197,9 +199,9 @@ type TopicStmts struct { createLike *sql.Stmt addLikesToTopic *sql.Stmt delete *sql.Stmt + deleteReplies *sql.Stmt deleteLikesForTopic *sql.Stmt deleteActivity *sql.Stmt - deleteActivitySubs *sql.Stmt edit *sql.Stmt setPoll *sql.Stmt createAction *sql.Stmt @@ -215,8 +217,8 @@ func init() { t := "topics" topicStmts = TopicStmts{ 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", "?,?"), - addReplies: acc.Update(t).Set("postCount = postCount + ?, lastReplyBy = ?, lastReplyAt = UTC_TIMESTAMP()").Where("tid = ?").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.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(), updateLastReply: acc.Update(t).Set("lastReplyID=?").Where("lastReplyID > ? AND tid = ?").Prepare(), lock: acc.Update(t).Set("is_closed=1").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(), 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(), - 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(), + deleteReplies: acc.Delete("replies").Where("tid=?").Prepare(), deleteLikesForTopic: acc.Delete("likes").Where("targetItem=? AND targetType='topics'").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? 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 = ?", "", ""), - 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 = ?", "", ""), + 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.ip, t.views, t.postCount, t.likeCount, t.poll, t.data", "r.tid=t.tid", "rid=?", "", ""), } return acc.FirstError() }) @@ -322,8 +324,17 @@ func (t *Topic) Like(score, uid int) (err error) { return err } -// TODO: Implement this +// TODO: Use a transaction 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() return nil } @@ -345,43 +356,72 @@ func handleLikedTopicReplies(tid int) error { if err != nil { return err } + err = Activity.DeleteByParams("like", rid, "post") + if err != nil { + return err + } } return rows.Err() } func handleTopicAttachments(tid int) error { - f := func(stmt *sql.Stmt) error { - 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) + err := handleAttachments(userStmts.getAttachmentsOfTopic, tid) if err != nil { 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 func (t *Topic) Delete() error { - creator, err := Users.Get(t.CreatedBy) + /*creator, err := Users.Get(t.CreatedBy) if err == nil { err = creator.DecreasePostStats(WordCount(t.Content), true) if err != nil { @@ -389,10 +429,10 @@ func (t *Topic) Delete() error { } } else if err != ErrNoRows { return err - } + }*/ // TODO: Clear reply cache too - _, err = topicStmts.delete.Exec(t.ID) + _, err := topicStmts.delete.Exec(t.ID) t.cacheRemove() if err != nil { return err @@ -405,7 +445,30 @@ func (t *Topic) Delete() error { if err != nil { 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 { return err } @@ -413,7 +476,7 @@ func (t *Topic) Delete() error { if err != nil { return err } - _, err = topicStmts.deleteActivitySubs.Exec(t.ID) + err = Subscriptions.DeleteResource(t.ID, "topic") if err != nil { return err } diff --git a/common/topic_store.go b/common/topic_store.go index 20923276..c508fba8 100644 --- a/common/topic_store.go +++ b/common/topic_store.go @@ -29,13 +29,16 @@ type TopicStore interface { BypassGet(id int) (*Topic, error) BulkGetMap(ids []int) (list map[int]*Topic, err error) 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 Reload(id int) error // Too much SQL logic to move into TopicCache // TODO: Implement these two methods //Replies(tid int) ([]*Reply, error) //RepliesRange(tid int, lower int, higher int) ([]*Reply, error) Count() int + CountUser(uid int) int + CountMegaUser(uid int) int + CountBigUser(uid int) int SetCache(cache TopicCache) GetCache() TopicCache @@ -44,10 +47,12 @@ type TopicStore interface { type DefaultTopicStore struct { cache TopicCache - get *sql.Stmt - exists *sql.Stmt - count *sql.Stmt - create *sql.Stmt + get *sql.Stmt + exists *sql.Stmt + count *sql.Stmt + countUser *sql.Stmt + countWordUser *sql.Stmt + create *sql.Stmt } // NewDefaultTopicStore gives you a new instance of DefaultTopicStore @@ -58,38 +63,40 @@ func NewDefaultTopicStore(cache TopicCache) (*DefaultTopicStore, error) { } t := "topics" return &DefaultTopicStore{ - 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(), - exists: acc.Exists(t, "tid").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(), + cache: cache, + 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(), + count: acc.Count(t).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() } func (s *DefaultTopicStore) DirtyGet(id int) *Topic { - topic, err := s.cache.Get(id) + t, err := s.cache.Get(id) if err == nil { - return topic + return t } - topic, err = s.BypassGet(id) + t, err = s.BypassGet(id) if err == nil { - _ = s.cache.Set(topic) - return topic + _ = s.cache.Set(t) + return t } return BlankTopic() } // TODO: Log weird cache errors? -func (s *DefaultTopicStore) Get(id int) (topic *Topic, err error) { - topic, err = s.cache.Get(id) +func (s *DefaultTopicStore) Get(id int) (t *Topic, err error) { + t, err = s.cache.Get(id) if err == nil { - return topic, nil + return t, nil } - topic, err = s.BypassGet(id) + t, err = s.BypassGet(id) 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 @@ -102,6 +109,15 @@ func (s *DefaultTopicStore) BypassGet(id int) (*Topic, error) { 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 func (s *DefaultTopicStore) BulkGetMap(ids []int) (list map[int]*Topic, err error) { 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] - 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 { return list, err } @@ -197,7 +213,7 @@ func (s *DefaultTopicStore) Exists(id int) bool { 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 == "" { 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 func (s *DefaultTopicStore) Count() (count int) { - err := s.count.QueryRow().Scan(&count) - if err != nil { - LogError(err) - } - return count + return Countf(s.count) +} +func (s *DefaultTopicStore) CountUser(uid int) (count int) { + return Countf(s.countUser, uid) +} +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) { diff --git a/common/user.go b/common/user.go index 7d12fdca..c5f3ed6e 100644 --- a/common/user.go +++ b/common/user.go @@ -12,6 +12,7 @@ import ( "strconv" "strings" "time" + //"log" qgen "github.com/Azareal/Gosora/query_gen" "github.com/go-sql-driver/mysql" @@ -137,6 +138,7 @@ type UserStmts struct { incTopics *sql.Stmt updateLevel *sql.Stmt resetStats *sql.Stmt + setStats *sql.Stmt decLiked *sql.Stmt updateLastIP *sql.Stmt @@ -152,6 +154,7 @@ type UserStmts struct { getLikedRepliesOfTopic *sql.Stmt getAttachmentsOfTopic *sql.Stmt getAttachmentsOfTopic2 *sql.Stmt + getRepliesOfTopic *sql.Stmt } var userStmts UserStmts @@ -168,6 +171,8 @@ func init() { 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 + // Stat Statements + // TODO: Do +0 to avoid having as many statements? incScore: acc.Update(u).Set("score=score+?").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(), @@ -178,6 +183,7 @@ func init() { incTopics: acc.SimpleUpdate(u, "topics=topics+?", 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(), + 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(), 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(), - deletePosts: acc.Select("topics").Columns("tid,parentID,poll").Where("createdBy=?").Prepare(), - deleteProfilePosts: acc.Select("users_replies").Columns("rid").Where("createdBy=?").Prepare(), + // Delete All Posts Statements + 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(), getLikedRepliesOfTopic: acc.Select("replies").Columns("rid").Where("tid=? AND likeCount>0").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(), + getRepliesOfTopic: acc.Select("replies").Columns("words").Where("createdBy!=? AND tid=?").Prepare(), } return acc.FirstError() }) @@ -325,6 +333,7 @@ func (u *User) Delete() error { return nil } +// TODO: dismiss-event func (u *User) DeletePosts() error { rows, err := userStmts.deletePosts.Query(u.ID) if err != nil { @@ -336,9 +345,10 @@ func (u *User) DeletePosts() error { updatedForums := make(map[int]int) // forum[count] tc := Topics.GetCache() + umap := make(map[int]struct{}) for rows.Next() { - var tid, parentID, poll int - err := rows.Scan(&tid, &parentID, &poll) + var tid, parentID, postCount,poll int + err := rows.Scan(&tid, &parentID, &postCount, &poll) if err != nil { return err } @@ -356,15 +366,25 @@ func (u *User) DeletePosts() error { if err != nil { return err } - err = handleLikedTopicReplies(tid) - if err != nil { - return err - } err = handleTopicAttachments(tid) if err != nil { 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 { return err } @@ -386,6 +406,12 @@ func (u *User) DeletePosts() error { if err != nil { return err } + for uid, _ := range umap { + err = (&User{ID: uid}).RecalcPostStats() + if err != nil { + return err + } + } for fid, count := range updatedForums { err := Forums.RemoveTopics(fid, count) if err != nil && err != ErrNoRows { @@ -400,8 +426,8 @@ func (u *User) DeletePosts() error { defer rows.Close() for rows.Next() { - var rid int - err := rows.Scan(&rid) + var rid, uid int + err := rows.Scan(&rid,&uid) if err != nil { return err } @@ -409,7 +435,12 @@ func (u *User) DeletePosts() error { if err != nil { 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 { return err @@ -454,6 +485,10 @@ func (u *User) DeletePosts() error { if err != nil { return err } + err = Activity.DeleteByParamsExtra("reply",tid,"topic",strconv.Itoa(rid)) + if err != nil { + return err + } _, err = replyStmts.deleteActivitySubs.Exec(rid) if err != nil { return err @@ -561,6 +596,42 @@ func (u *User) IncreasePostStats(wcount int, topic bool) (err error) { 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) { baseScore := -1 if topic { diff --git a/common/user_store.go b/common/user_store.go index e343269b..9d253683 100644 --- a/common/user_store.go +++ b/common/user_store.go @@ -21,6 +21,7 @@ type UserStore interface { GetByName(name string) (*User, error) Exists(id int) bool GetOffset(offset, perPage int) ([]*User, error) + Each(f func(*User) error) error //BulkGet(ids []int) ([]*User, error) BulkGetMap(ids []int) (map[int]*User, error) BypassGet(id int) (*User, error) @@ -38,6 +39,7 @@ type DefaultUserStore struct { get *sql.Stmt getByName *sql.Stmt getOffset *sql.Stmt + getAll *sql.Stmt exists *sql.Stmt register *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(), 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(), + 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(), 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(), @@ -143,6 +146,29 @@ func (s *DefaultUserStore) GetOffset(offset, perPage int) (users []*User, err er } 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: ID of 0 should always error? @@ -265,7 +291,7 @@ func (s *DefaultUserStore) Exists(id int) bool { // TODO: Change active to a bool? // 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? // ? 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 func (s *DefaultUserStore) Count() (count int) { - err := s.count.QueryRow().Scan(&count) - if err != nil { - LogError(err) - } - return count + return Countf(s.count) } func (s *DefaultUserStore) SetCache(cache UserCache) { diff --git a/common/widget.go b/common/widget.go index fc2cb90a..e64312b6 100644 --- a/common/widget.go +++ b/common/widget.go @@ -3,11 +3,11 @@ package common import ( "database/sql" "encoding/json" - "strings" "strconv" + "strings" "sync/atomic" - "github.com/Azareal/Gosora/query_gen" + qgen "github.com/Azareal/Gosora/query_gen" ) type WidgetStmts struct { @@ -16,7 +16,7 @@ type WidgetStmts struct { delete *sql.Stmt create *sql.Stmt update *sql.Stmt - + //qgen.SimpleModel } @@ -29,9 +29,9 @@ func init() { //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(), //model: acc.SimpleModel(w,"position,type,active,location,data","wid"), - delete: acc.Delete(w).Where("wid = ?").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(), + delete: acc.Delete(w).Where("wid=?").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(), } return acc.FirstError() }) @@ -87,7 +87,7 @@ func (w *Widget) Allowed(zone string, zoneid int) bool { if len(loc) == 0 { continue } - sloc := strings.Split(":",loc) + sloc := strings.Split(":", loc) if len(sloc) > 1 { iloc, _ := strconv.Atoi(sloc[1]) if zoneid != 0 && iloc != zoneid { diff --git a/common/word_filters.go b/common/word_filters.go index 1fe2f3fb..30d338c9 100644 --- a/common/word_filters.go +++ b/common/word_filters.go @@ -26,9 +26,9 @@ type WordFilterStore interface { ReloadAll() error GetAll() (filters map[int]*WordFilter, err error) Get(id int) (*WordFilter, error) - Create(find string, replace string) (int, error) + Create(find, replace string) (int, error) Delete(id int) error - Update(id int, find string, replace string) error + Update(id int, find, replace string) error Length() int EstCount() int Count() (count int) @@ -49,10 +49,10 @@ func NewDefaultWordFilterStore(acc *qgen.Accumulator) (*DefaultWordFilterStore, wf := "word_filters" store := &DefaultWordFilterStore{ 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(), - delete: acc.Delete(wf).Where("wfid = ?").Prepare(), - update: acc.Update(wf).Set("find = ?, replacement = ?").Where("wfid = ?").Prepare(), + delete: acc.Delete(wf).Where("wfid=?").Prepare(), + update: acc.Update(wf).Set("find=?,replacement=?").Where("wfid=?").Prepare(), count: acc.Count(wf).Prepare(), } // 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 -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) if err != nil { return 0, err @@ -130,7 +130,7 @@ func (s *DefaultWordFilterStore) Delete(id int) error { 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) if err != nil { return err diff --git a/gen_router.go b/gen_router.go index 715614a0..210dcf3b 100644 --- a/gen_router.go +++ b/gen_router.go @@ -158,6 +158,7 @@ var RouteMap = map[string]interface{}{ "routes.UnlockTopicSubmit": routes.UnlockTopicSubmit, "routes.MoveTopicSubmit": routes.MoveTopicSubmit, "routes.LikeTopicSubmit": routes.LikeTopicSubmit, + "routes.UnlikeTopicSubmit": routes.UnlikeTopicSubmit, "routes.AddAttachToTopicSubmit": routes.AddAttachToTopicSubmit, "routes.RemoveAttachFromTopicSubmit": routes.RemoveAttachFromTopicSubmit, "routes.ViewTopic": routes.ViewTopic, @@ -330,39 +331,40 @@ var routeMapEnum = map[string]int{ "routes.UnlockTopicSubmit": 132, "routes.MoveTopicSubmit": 133, "routes.LikeTopicSubmit": 134, - "routes.AddAttachToTopicSubmit": 135, - "routes.RemoveAttachFromTopicSubmit": 136, - "routes.ViewTopic": 137, - "routes.CreateReplySubmit": 138, - "routes.ReplyEditSubmit": 139, - "routes.ReplyDeleteSubmit": 140, - "routes.ReplyLikeSubmit": 141, - "routes.AddAttachToReplySubmit": 142, - "routes.RemoveAttachFromReplySubmit": 143, - "routes.ProfileReplyCreateSubmit": 144, - "routes.ProfileReplyEditSubmit": 145, - "routes.ProfileReplyDeleteSubmit": 146, - "routes.PollVote": 147, - "routes.PollResults": 148, - "routes.AccountLogin": 149, - "routes.AccountRegister": 150, - "routes.AccountLogout": 151, - "routes.AccountLoginSubmit": 152, - "routes.AccountLoginMFAVerify": 153, - "routes.AccountLoginMFAVerifySubmit": 154, - "routes.AccountRegisterSubmit": 155, - "routes.AccountPasswordReset": 156, - "routes.AccountPasswordResetSubmit": 157, - "routes.AccountPasswordResetToken": 158, - "routes.AccountPasswordResetTokenSubmit": 159, - "routes.DynamicRoute": 160, - "routes.UploadedFile": 161, - "routes.StaticFile": 162, - "routes.RobotsTxt": 163, - "routes.SitemapXml": 164, - "routes.OpenSearchXml": 165, - "routes.BadRoute": 166, - "routes.HTTPSRedirect": 167, + "routes.UnlikeTopicSubmit": 135, + "routes.AddAttachToTopicSubmit": 136, + "routes.RemoveAttachFromTopicSubmit": 137, + "routes.ViewTopic": 138, + "routes.CreateReplySubmit": 139, + "routes.ReplyEditSubmit": 140, + "routes.ReplyDeleteSubmit": 141, + "routes.ReplyLikeSubmit": 142, + "routes.AddAttachToReplySubmit": 143, + "routes.RemoveAttachFromReplySubmit": 144, + "routes.ProfileReplyCreateSubmit": 145, + "routes.ProfileReplyEditSubmit": 146, + "routes.ProfileReplyDeleteSubmit": 147, + "routes.PollVote": 148, + "routes.PollResults": 149, + "routes.AccountLogin": 150, + "routes.AccountRegister": 151, + "routes.AccountLogout": 152, + "routes.AccountLoginSubmit": 153, + "routes.AccountLoginMFAVerify": 154, + "routes.AccountLoginMFAVerifySubmit": 155, + "routes.AccountRegisterSubmit": 156, + "routes.AccountPasswordReset": 157, + "routes.AccountPasswordResetSubmit": 158, + "routes.AccountPasswordResetToken": 159, + "routes.AccountPasswordResetTokenSubmit": 160, + "routes.DynamicRoute": 161, + "routes.UploadedFile": 162, + "routes.StaticFile": 163, + "routes.RobotsTxt": 164, + "routes.SitemapXml": 165, + "routes.OpenSearchXml": 166, + "routes.BadRoute": 167, + "routes.HTTPSRedirect": 168, } var reverseRouteMapEnum = map[int]string{ 0: "routes.Overview", @@ -500,39 +502,40 @@ var reverseRouteMapEnum = map[int]string{ 132: "routes.UnlockTopicSubmit", 133: "routes.MoveTopicSubmit", 134: "routes.LikeTopicSubmit", - 135: "routes.AddAttachToTopicSubmit", - 136: "routes.RemoveAttachFromTopicSubmit", - 137: "routes.ViewTopic", - 138: "routes.CreateReplySubmit", - 139: "routes.ReplyEditSubmit", - 140: "routes.ReplyDeleteSubmit", - 141: "routes.ReplyLikeSubmit", - 142: "routes.AddAttachToReplySubmit", - 143: "routes.RemoveAttachFromReplySubmit", - 144: "routes.ProfileReplyCreateSubmit", - 145: "routes.ProfileReplyEditSubmit", - 146: "routes.ProfileReplyDeleteSubmit", - 147: "routes.PollVote", - 148: "routes.PollResults", - 149: "routes.AccountLogin", - 150: "routes.AccountRegister", - 151: "routes.AccountLogout", - 152: "routes.AccountLoginSubmit", - 153: "routes.AccountLoginMFAVerify", - 154: "routes.AccountLoginMFAVerifySubmit", - 155: "routes.AccountRegisterSubmit", - 156: "routes.AccountPasswordReset", - 157: "routes.AccountPasswordResetSubmit", - 158: "routes.AccountPasswordResetToken", - 159: "routes.AccountPasswordResetTokenSubmit", - 160: "routes.DynamicRoute", - 161: "routes.UploadedFile", - 162: "routes.StaticFile", - 163: "routes.RobotsTxt", - 164: "routes.SitemapXml", - 165: "routes.OpenSearchXml", - 166: "routes.BadRoute", - 167: "routes.HTTPSRedirect", + 135: "routes.UnlikeTopicSubmit", + 136: "routes.AddAttachToTopicSubmit", + 137: "routes.RemoveAttachFromTopicSubmit", + 138: "routes.ViewTopic", + 139: "routes.CreateReplySubmit", + 140: "routes.ReplyEditSubmit", + 141: "routes.ReplyDeleteSubmit", + 142: "routes.ReplyLikeSubmit", + 143: "routes.AddAttachToReplySubmit", + 144: "routes.RemoveAttachFromReplySubmit", + 145: "routes.ProfileReplyCreateSubmit", + 146: "routes.ProfileReplyEditSubmit", + 147: "routes.ProfileReplyDeleteSubmit", + 148: "routes.PollVote", + 149: "routes.PollResults", + 150: "routes.AccountLogin", + 151: "routes.AccountRegister", + 152: "routes.AccountLogout", + 153: "routes.AccountLoginSubmit", + 154: "routes.AccountLoginMFAVerify", + 155: "routes.AccountLoginMFAVerifySubmit", + 156: "routes.AccountRegisterSubmit", + 157: "routes.AccountPasswordReset", + 158: "routes.AccountPasswordResetSubmit", + 159: "routes.AccountPasswordResetToken", + 160: "routes.AccountPasswordResetTokenSubmit", + 161: "routes.DynamicRoute", + 162: "routes.UploadedFile", + 163: "routes.StaticFile", + 164: "routes.RobotsTxt", + 165: "routes.SitemapXml", + 166: "routes.OpenSearchXml", + 167: "routes.BadRoute", + 168: "routes.HTTPSRedirect", } var osMapEnum = map[string]int{ "unknown": 0, @@ -690,7 +693,7 @@ type HTTPSRedirect struct {} func (red *HTTPSRedirect) ServeHTTP(w http.ResponseWriter, req *http.Request) { w.Header().Set("Connection", "close") - co.RouteViewCounter.Bump(167) + co.RouteViewCounter.Bump(168) dest := "https://" + req.Host + req.URL.String() http.Redirect(w, req, dest, http.StatusTemporaryRedirect) } @@ -898,7 +901,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { co.GlobalViewCounter.Bump() if prefix == "/s" { //old prefix: /static - co.RouteViewCounter.Bump(162) + co.RouteViewCounter.Bump(163) req.URL.Path += extraData routes.StaticFile(w, req) return @@ -2274,6 +2277,19 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c co.RouteViewCounter.Bump(134) 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/": err = c.MemberOnly(w,req,user) if err != nil { @@ -2289,7 +2305,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(135) + co.RouteViewCounter.Bump(136) err = routes.AddAttachToTopicSubmit(w,req,user,extraData) case "/topic/attach/remove/submit/": err = c.NoSessionMismatch(w,req,user) @@ -2302,10 +2318,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(136) + co.RouteViewCounter.Bump(137) err = routes.RemoveAttachFromTopicSubmit(w,req,user,extraData) default: - co.RouteViewCounter.Bump(137) + co.RouteViewCounter.Bump(138) head, err := c.UserCheck(w,req,&user) if err != nil { return err @@ -2329,7 +2345,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(138) + co.RouteViewCounter.Bump(139) err = routes.CreateReplySubmit(w,req,user) case "/reply/edit/submit/": err = c.NoSessionMismatch(w,req,user) @@ -2342,7 +2358,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(139) + co.RouteViewCounter.Bump(140) err = routes.ReplyEditSubmit(w,req,user,extraData) case "/reply/delete/submit/": err = c.NoSessionMismatch(w,req,user) @@ -2355,7 +2371,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(140) + co.RouteViewCounter.Bump(141) err = routes.ReplyDeleteSubmit(w,req,user,extraData) case "/reply/like/submit/": err = c.NoSessionMismatch(w,req,user) @@ -2368,7 +2384,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(141) + co.RouteViewCounter.Bump(142) err = routes.ReplyLikeSubmit(w,req,user,extraData) case "/reply/attach/add/submit/": err = c.MemberOnly(w,req,user) @@ -2385,7 +2401,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(142) + co.RouteViewCounter.Bump(143) err = routes.AddAttachToReplySubmit(w,req,user,extraData) case "/reply/attach/remove/submit/": err = c.NoSessionMismatch(w,req,user) @@ -2398,7 +2414,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(143) + co.RouteViewCounter.Bump(144) err = routes.RemoveAttachFromReplySubmit(w,req,user,extraData) } case "/profile": @@ -2414,7 +2430,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(144) + co.RouteViewCounter.Bump(145) err = routes.ProfileReplyCreateSubmit(w,req,user) case "/profile/reply/edit/submit/": err = c.NoSessionMismatch(w,req,user) @@ -2427,7 +2443,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(145) + co.RouteViewCounter.Bump(146) err = routes.ProfileReplyEditSubmit(w,req,user,extraData) case "/profile/reply/delete/submit/": err = c.NoSessionMismatch(w,req,user) @@ -2440,7 +2456,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(146) + co.RouteViewCounter.Bump(147) err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData) } case "/poll": @@ -2456,23 +2472,23 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(147) + co.RouteViewCounter.Bump(148) err = routes.PollVote(w,req,user,extraData) case "/poll/results/": - co.RouteViewCounter.Bump(148) + co.RouteViewCounter.Bump(149) err = routes.PollResults(w,req,user,extraData) } case "/accounts": switch(req.URL.Path) { case "/accounts/login/": - co.RouteViewCounter.Bump(149) + co.RouteViewCounter.Bump(150) head, err := c.UserCheck(w,req,&user) if err != nil { return err } err = routes.AccountLogin(w,req,user,head) case "/accounts/create/": - co.RouteViewCounter.Bump(150) + co.RouteViewCounter.Bump(151) head, err := c.UserCheck(w,req,&user) if err != nil { return err @@ -2489,7 +2505,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(151) + co.RouteViewCounter.Bump(152) err = routes.AccountLogout(w,req,user) case "/accounts/login/submit/": err = c.ParseForm(w,req,user) @@ -2497,10 +2513,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(152) + co.RouteViewCounter.Bump(153) err = routes.AccountLoginSubmit(w,req,user) case "/accounts/mfa_verify/": - co.RouteViewCounter.Bump(153) + co.RouteViewCounter.Bump(154) head, err := c.UserCheck(w,req,&user) if err != nil { return err @@ -2512,7 +2528,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(154) + co.RouteViewCounter.Bump(155) err = routes.AccountLoginMFAVerifySubmit(w,req,user) case "/accounts/create/submit/": err = c.ParseForm(w,req,user) @@ -2520,10 +2536,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(155) + co.RouteViewCounter.Bump(156) err = routes.AccountRegisterSubmit(w,req,user) case "/accounts/password-reset/": - co.RouteViewCounter.Bump(156) + co.RouteViewCounter.Bump(157) head, err := c.UserCheck(w,req,&user) if err != nil { return err @@ -2535,10 +2551,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(157) + co.RouteViewCounter.Bump(158) err = routes.AccountPasswordResetSubmit(w,req,user) case "/accounts/password-reset/token/": - co.RouteViewCounter.Bump(158) + co.RouteViewCounter.Bump(159) head, err := c.UserCheck(w,req,&user) if err != nil { return err @@ -2550,7 +2566,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - co.RouteViewCounter.Bump(159) + co.RouteViewCounter.Bump(160) err = routes.AccountPasswordResetTokenSubmit(w,req,user) } /*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-Encoding") } - co.RouteViewCounter.Bump(161) + co.RouteViewCounter.Bump(162) req.URL.Path += extraData // TODO: Find a way to propagate errors up from this? 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 switch(extraData) { case "robots.txt": - co.RouteViewCounter.Bump(163) + co.RouteViewCounter.Bump(164) return routes.RobotsTxt(w,req) case "favicon.ico": 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) return nil case "opensearch.xml": - co.RouteViewCounter.Bump(165) + co.RouteViewCounter.Bump(166) return routes.OpenSearchXml(w,req) /*case "sitemap.xml": - co.RouteViewCounter.Bump(164) + co.RouteViewCounter.Bump(165) return routes.SitemapXml(w,req)*/ } return c.NotFound(w,req,nil) @@ -2605,7 +2621,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c r.RUnlock() 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 return handle(w,req,user) } @@ -2616,7 +2632,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c } else { r.DumpRequest(req,"Bad Route") } - co.RouteViewCounter.Bump(166) + co.RouteViewCounter.Bump(167) return c.NotFound(w,req,nil) } return err diff --git a/general_test.go b/general_test.go index 3a5c7853..50e8ee5f 100644 --- a/general_test.go +++ b/general_test.go @@ -791,7 +791,7 @@ func BenchmarkQueryTopicParallel(b *testing.B) { b.RunParallel(func(pb *testing.PB) { var tu c.TopicUser 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 { log.Fatal("No rows found!") return @@ -812,7 +812,7 @@ func BenchmarkQueryPreparedTopicParallel(b *testing.B) { b.RunParallel(func(pb *testing.PB) { 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 { b.Fatal(err) } @@ -873,7 +873,7 @@ func BenchmarkQueriesSerial(b *testing.B) { var tu c.TopicUser b.Run("topic", func(b *testing.B) { 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 { b.Fatal("No rows found!") return @@ -885,7 +885,7 @@ func BenchmarkQueriesSerial(b *testing.B) { }) b.Run("topic_replies", func(b *testing.B) { 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 { b.Fatal(err) return @@ -907,7 +907,7 @@ func BenchmarkQueriesSerial(b *testing.B) { var group int b.Run("topic_replies_scan", func(b *testing.B) { 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 { b.Fatal(err) return diff --git a/main.go b/main.go index 910b0b10..d7041da8 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,7 @@ import ( _ "github.com/Azareal/Gosora/extend" co "github.com/Azareal/Gosora/common/counters" p "github.com/Azareal/Gosora/common/phrases" + meta "github.com/Azareal/Gosora/common/meta" "github.com/Azareal/Gosora/query_gen" "github.com/Azareal/Gosora/routes" "github.com/fsnotify/fsnotify" @@ -264,6 +265,10 @@ func storeInit() (err error) { } // TODO: Let the admin choose other thumbnailers, maybe ones defined in plugins c.Thumbnailer = c.NewCaireThumbnailer() + c.Recalc, err = c.NewDefaultRecalc(acc) + if err != nil { + return errors.WithStack(err) + } log.Print("Initialising the view counters") co.GlobalViewCounter, err = co.NewGlobalViewCounter(acc) @@ -312,7 +317,7 @@ func storeInit() (err error) { } log.Print("Initialising the meta store") - c.Meta, err = c.NewDefaultMetaStore(acc) + c.Meta, err = meta.NewDefaultMetaStore(acc) if err != nil { 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") // 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 diff --git a/misc_test.go b/misc_test.go index b4a037de..74069816 100644 --- a/misc_test.go +++ b/misc_test.go @@ -917,7 +917,7 @@ func TestReplyStore(t *testing.T) { 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) { 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)) @@ -1028,20 +1028,20 @@ func testProfileReplyStore(t *testing.T, newID int, ip string) { expectNilErr(t, err) 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) - expect(t, profileReply.ID == newID, fmt.Sprintf("The profile reply should have an ID of %d not %d", newID, profileReply.ID)) - expect(t, profileReply.ParentID == 1, fmt.Sprintf("The parent ID of the profile reply should be 1 not %d", profileReply.ParentID)) - expect(t, profileReply.Content == "Haha", fmt.Sprintf("The profile reply's contents should be 'Haha' not '%s'", profileReply.Content)) - expect(t, profileReply.CreatedBy == 1, fmt.Sprintf("The profile reply's creator should be 1 not %d", profileReply.CreatedBy)) - expect(t, profileReply.IP == ip, fmt.Sprintf("The profile reply's IP should be '%s' not '%s'", ip, profileReply.IP)) + expect(t, pr.ID == newID, fmt.Sprintf("The profile reply should have an ID of %d not %d", newID, pr.ID)) + expect(t, pr.ParentID == 1, fmt.Sprintf("The parent ID of the profile reply should be 1 not %d", pr.ParentID)) + expect(t, pr.Content == "Haha", fmt.Sprintf("The profile reply's contents should be 'Haha' not '%s'", pr.Content)) + expect(t, pr.CreatedBy == 1, fmt.Sprintf("The profile reply's creator should be 1 not %d", pr.CreatedBy)) + 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) _, err = c.Prstore.Get(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) { diff --git a/patcher/patches.go b/patcher/patches.go index 0ddfc672..9018da2e 100644 --- a/patcher/patches.go +++ b/patcher/patches.go @@ -4,7 +4,10 @@ import ( "bufio" "database/sql" "strconv" + "strings" + "unicode" + meta "github.com/Azareal/Gosora/common/meta" qgen "github.com/Azareal/Gosora/query_gen" ) @@ -43,6 +46,7 @@ func init() { addPatch(26, patch26) addPatch(27, patch27) addPatch(28, patch28) + addPatch(29, patch29) } func patch0(scanner *bufio.Scanner) (err error) { @@ -749,4 +753,108 @@ func patch27(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)) -} \ No newline at end of file +} + +// 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)) + +} diff --git a/query_gen/builder.go b/query_gen/builder.go index a95938e0..aa354397 100644 --- a/query_gen/builder.go +++ b/query_gen/builder.go @@ -18,58 +18,58 @@ type builder struct { adapter Adapter } -func (build *builder) Accumulator() *Accumulator { - return &Accumulator{build.conn, build.adapter, nil} +func (b *builder) Accumulator() *Accumulator { + return &Accumulator{b.conn, b.adapter, nil} } // TODO: Move this method out of builder? -func (build *builder) Init(adapter string, config map[string]string) error { - err := build.SetAdapter(adapter) +func (b *builder) Init(adapter string, config map[string]string) error { + err := b.SetAdapter(adapter) if err != nil { return err } - conn, err := build.adapter.BuildConn(config) - build.conn = conn - log.Print("err: ", err) // Is the problem here somehow? + conn, err := b.adapter.BuildConn(config) + b.conn = conn + log.Print("err:", err) // Is the problem here somehow? return err } -func (build *builder) SetConn(conn *sql.DB) { - build.conn = conn +func (b *builder) SetConn(conn *sql.DB) { + b.conn = conn } -func (build *builder) GetConn() *sql.DB { - return build.conn +func (b *builder) GetConn() *sql.DB { + return b.conn } -func (build *builder) SetAdapter(name string) error { +func (b *builder) SetAdapter(name string) error { adap, err := GetAdapter(name) if err != nil { return err } - build.adapter = adap + b.adapter = adap return nil } -func (build *builder) GetAdapter() Adapter { - return build.adapter +func (b *builder) GetAdapter() Adapter { + return b.adapter } -func (build *builder) DbVersion() (dbVersion string) { - build.conn.QueryRow(build.adapter.DbVersion()).Scan(&dbVersion) +func (b *builder) DbVersion() (dbVersion string) { + b.conn.QueryRow(b.adapter.DbVersion()).Scan(&dbVersion) return dbVersion } -func (build *builder) Begin() (*sql.Tx, error) { - return build.conn.Begin() +func (b *builder) Begin() (*sql.Tx, error) { + return b.conn.Begin() } -func (build *builder) Tx(handler func(*TransactionBuilder) error) error { - tx, err := build.conn.Begin() +func (b *builder) Tx(h func(*TransactionBuilder) error) error { + tx, err := b.conn.Begin() if err != nil { return err } - err = handler(&TransactionBuilder{tx, build.adapter, nil}) + err = h(&TransactionBuilder{tx, b.adapter, nil}) if err != nil { tx.Rollback() return err @@ -77,83 +77,99 @@ func (build *builder) Tx(handler func(*TransactionBuilder) error) error { 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 { 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) { - return build.prepare(build.adapter.SimpleSelect("", table, columns, where, orderby, limit)) +func (b *builder) SimpleSelect(table, columns, where, orderby, limit string) (stmt *sql.Stmt, err error) { + 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) { - return build.prepare(build.adapter.SimpleCount("", table, where, limit)) +func (b *builder) SimpleCount(table, where, limit string) (stmt *sql.Stmt, err error) { + 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) { - return build.prepare(build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit)) +func (b *builder) SimpleLeftJoin(table1, table2, columns, joiners, where, orderby, limit string) (stmt *sql.Stmt, err error) { + 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) { - return build.prepare(build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit)) +func (b *builder) SimpleInnerJoin(table1, table2, columns, joiners, where, orderby, limit string) (stmt *sql.Stmt, err error) { + return b.prepare(b.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit)) } -func (build *builder) DropTable(table string) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.DropTable("", table)) +func (b *builder) DropTable(table string) (stmt *sql.Stmt, err error) { + 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)) } -func (build *builder) AddColumn(table string, column DBTableColumn, key *DBTableKey) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.AddColumn("", table, column, key)) +func (b *builder) AddColumn(table string, column DBTableColumn, key *DBTableKey) (stmt *sql.Stmt, err error) { + return b.prepare(b.adapter.AddColumn("", table, column, key)) } -func (build *builder) AddIndex(table string, iname string, colname string) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.AddIndex("", table, iname, colname)) +func (b *builder) DropColumn(table, colName string) (stmt *sql.Stmt, err error) { + return b.prepare(b.adapter.DropColumn("", table, colName)) } -func (build *builder) AddKey(table string, column string, key DBTableKey) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.AddKey("", table, column, key)) +func (b *builder) RenameColumn(table, oldName, newName string) (stmt *sql.Stmt, err error) { + 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) { - return build.prepare(build.adapter.AddForeignKey("", table, column, ftable, fcolumn, cascade)) +func (b *builder) ChangeColumn(table, colName string, col DBTableColumn) (stmt *sql.Stmt, err error) { + return b.prepare(b.adapter.ChangeColumn("", table, colName, col)) } -func (build *builder) SimpleInsert(table string, columns string, fields string) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.SimpleInsert("", table, columns, fields)) +func (b *builder) SetDefaultColumn(table, colName, colType, defaultStr string) (stmt *sql.Stmt, err error) { + return b.prepare(b.adapter.SetDefaultColumn("", table, colName, colType, defaultStr)) } -func (build *builder) SimpleInsertSelect(ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.SimpleInsertSelect("", ins, sel)) +func (b *builder) AddIndex(table, iname, colname string) (stmt *sql.Stmt, err error) { + return b.prepare(b.adapter.AddIndex("", table, iname, colname)) } -func (build *builder) SimpleInsertLeftJoin(ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.SimpleInsertLeftJoin("", ins, sel)) +func (b *builder) AddKey(table, column string, key DBTableKey) (stmt *sql.Stmt, err error) { + return b.prepare(b.adapter.AddKey("", table, column, key)) } -func (build *builder) SimpleInsertInnerJoin(ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.SimpleInsertInnerJoin("", ins, sel)) +func (b *builder) AddForeignKey(table, column, ftable, fcolumn string, cascade bool) (stmt *sql.Stmt, err error) { + 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) { - return build.prepare(build.adapter.SimpleUpdate(qUpdate(table, set, where))) +func (b *builder) SimpleInsert(table, columns, fields string) (stmt *sql.Stmt, err error) { + return b.prepare(b.adapter.SimpleInsert("", table, columns, fields)) } -func (build *builder) SimpleDelete(table string, where string) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.SimpleDelete("", table, where)) +func (b *builder) SimpleInsertSelect(ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) { + 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 -func (build *builder) Purge(table string) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.Purge("", table)) +func (b *builder) Purge(table string) (stmt *sql.Stmt, err error) { + 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 { 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 -func (build *builder) SimpleSelectTx(tx *sql.Tx, table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleSelect("", table, columns, where, orderby, limit) - return build.prepareTx(tx, res, err) +func (b *builder) SimpleSelectTx(tx *sql.Tx, table, columns, where, orderby, limit string) (stmt *sql.Stmt, err error) { + res, err := b.adapter.SimpleSelect("", table, columns, where, orderby, limit) + return b.prepareTx(tx, res, err) } -func (build *builder) SimpleCountTx(tx *sql.Tx, table string, where string, limit string) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleCount("", table, where, limit) - return build.prepareTx(tx, res, err) +func (b *builder) SimpleCountTx(tx *sql.Tx, table, where, limit string) (stmt *sql.Stmt, err error) { + res, err := b.adapter.SimpleCount("", table, where, limit) + 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) { - res, err := build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit) - return build.prepareTx(tx, res, err) +func (b *builder) SimpleLeftJoinTx(tx *sql.Tx, table1, table2, columns, joiners, where, orderby, limit string) (stmt *sql.Stmt, err error) { + res, err := b.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit) + 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) { - res, err := build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit) - return build.prepareTx(tx, res, err) +func (b *builder) SimpleInnerJoinTx(tx *sql.Tx, table1, table2, columns, joiners, where, orderby, limit string) (stmt *sql.Stmt, err error) { + res, err := b.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit) + 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) { - res, err := build.adapter.CreateTable("", table, charset, collation, columns, keys) - return build.prepareTx(tx, res, err) +func (b *builder) CreateTableTx(tx *sql.Tx, table, charset, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt, err error) { + res, err := b.adapter.CreateTable("", table, charset, collation, columns, keys) + return b.prepareTx(tx, res, err) } -func (build *builder) SimpleInsertTx(tx *sql.Tx, table string, columns string, fields string) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleInsert("", table, columns, fields) - return build.prepareTx(tx, res, err) +func (b *builder) SimpleInsertTx(tx *sql.Tx, table, columns, fields string) (stmt *sql.Stmt, err error) { + res, err := b.adapter.SimpleInsert("", table, columns, fields) + return b.prepareTx(tx, res, err) } -func (build *builder) SimpleInsertSelectTx(tx *sql.Tx, ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleInsertSelect("", ins, sel) - return build.prepareTx(tx, res, err) +func (b *builder) SimpleInsertSelectTx(tx *sql.Tx, ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) { + res, err := b.adapter.SimpleInsertSelect("", ins, sel) + return b.prepareTx(tx, res, err) } -func (build *builder) SimpleInsertLeftJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleInsertLeftJoin("", ins, sel) - return build.prepareTx(tx, res, err) +func (b *builder) SimpleInsertLeftJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { + res, err := b.adapter.SimpleInsertLeftJoin("", ins, sel) + return b.prepareTx(tx, res, err) } -func (build *builder) SimpleInsertInnerJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleInsertInnerJoin("", ins, sel) - return build.prepareTx(tx, res, err) +func (b *builder) SimpleInsertInnerJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { + res, err := b.adapter.SimpleInsertInnerJoin("", ins, sel) + return b.prepareTx(tx, res, err) } -func (build *builder) SimpleUpdateTx(tx *sql.Tx, table string, set string, where string) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleUpdate(qUpdate(table, set, where)) - return build.prepareTx(tx, res, err) +func (b *builder) SimpleUpdateTx(tx *sql.Tx, table, set, where string) (stmt *sql.Stmt, err error) { + res, err := b.adapter.SimpleUpdate(qUpdate(table, set, where)) + return b.prepareTx(tx, res, err) } -func (build *builder) SimpleDeleteTx(tx *sql.Tx, table string, where string) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleDelete("", table, where) - return build.prepareTx(tx, res, err) +func (b *builder) SimpleDeleteTx(tx *sql.Tx, table, where string) (stmt *sql.Stmt, err error) { + res, err := b.adapter.SimpleDelete("", table, where) + return b.prepareTx(tx, res, err) } // 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) { - res, err := build.adapter.Purge("", table) - return build.prepareTx(tx, res, err) +func (b *builder) PurgeTx(tx *sql.Tx, table string) (stmt *sql.Stmt, err error) { + res, err := b.adapter.Purge("", table) + return b.prepareTx(tx, res, err) } diff --git a/query_gen/mssql.go b/query_gen/mssql.go index c6f41b1d..fdca4820 100644 --- a/query_gen/mssql.go +++ b/query_gen/mssql.go @@ -56,7 +56,7 @@ func (a *MssqlAdapter) DropTable(name, table string) (string, error) { // TODO: Add support for foreign keys? // 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 -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 == "" { 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: 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 == "" { 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 } +// 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: Test to make sure everything works here func (a *MssqlAdapter) AddIndex(name, table, iname, colname string) (string, error) { diff --git a/query_gen/mysql.go b/query_gen/mysql.go index bae07024..9fe93391 100644 --- a/query_gen/mysql.go +++ b/query_gen/mysql.go @@ -92,7 +92,7 @@ func (a *MysqlAdapter) DropTable(name, table string) (string, error) { 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 == "" { 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 } -func (a *MysqlAdapter) parseColumn(column DBTableColumn) (col DBTableColumn, size string, end string) { - // Make it easier to support Cassandra in the future - if column.Type == "createdAt" { - column.Type = "datetime" - // MySQL doesn't support this x.x - /*if column.Default == "" { - column.Default = "UTC_TIMESTAMP()" - }*/ - } else if column.Type == "json" { - column.Type = "text" +func (a *MysqlAdapter) DropColumn(name, table, colName string) (string, error) { + q := "ALTER TABLE `" + table + "` DROP COLUMN `" + colName + "`;" + a.pushStatement(name, "drop-column", q) + return q, nil +} + +// ! Currently broken in MariaDB. Planned. +func (a *MysqlAdapter) RenameColumn(name, table, oldName, newName string) (string, error) { + q := "ALTER TABLE `" + table + "` RENAME COLUMN `" + oldName + "` TO `" + newName + "`;" + 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 { - size = "(" + strconv.Itoa(column.Size) + ")" + if defaultStr == "" { + 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 - if column.Default != "" && column.Type != "text" { + if col.Default != "" && col.Type != "text" { 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 - } else */if a.stringyType(column.Type) && column.Default != "''" { - end += "'" + column.Default + "'" + } else */if a.stringyType(col.Type) && col.Default != "''" { + end += "'" + col.Default + "'" } else { - end += column.Default + end += col.Default } } - if column.Null { + if col.Null { end += " null" } else { end += " not null" } - if column.AutoIncrement { + if col.AutoIncrement { end += " AUTO_INCREMENT" } - return column, size, end + return col, size, end } // TODO: Support AFTER column // 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 == "" { return "", errors.New("You need a name for this table") } - column, size, end := a.parseColumn(column) - q := "ALTER TABLE `" + table + "` ADD COLUMN " + "`" + column.Name + "` " + column.Type + size + end + col, size, end := a.parseColumn(col) + q := "ALTER TABLE `" + table + "` ADD COLUMN " + "`" + col.Name + "` " + col.Type + size + end if key != nil { 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 // 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 == "" { 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 } -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) { if e != nil || !val { return @@ -338,7 +380,7 @@ func (a *MysqlAdapter) SimpleReplace(name, table, columns, fields string) (strin for _, field := range processFields(fields) { 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 a.pushStatement(name, "replace", q) @@ -362,8 +404,7 @@ func (a *MysqlAdapter) SimpleUpsert(name, table, columns, fields, where string) q := "INSERT INTO `" + table + "`(" parsedFields := processFields(fields) - var insertColumns string - var insertValues string + var insertColumns, insertValues string setBit := ") ON DUPLICATE KEY UPDATE " for columnID, col := range processColumns(columns) { @@ -667,7 +708,7 @@ func (a *MysqlAdapter) complexSelect(preBuilder *selectPrebuilder, sb *strings.B 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 == "" { 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 } -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 == "" { return "", errors.New("You need a name for the left table") } diff --git a/query_gen/pgsql.go b/query_gen/pgsql.go index 98222cf0..f282cb20 100644 --- a/query_gen/pgsql.go +++ b/query_gen/pgsql.go @@ -42,7 +42,7 @@ func (a *PgsqlAdapter) DbVersion() string { return "SELECT version()" } -func (a *PgsqlAdapter) DropTable(name string, table string) (string, error) { +func (a *PgsqlAdapter) DropTable(name, table string) (string, error) { if 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 // 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 == "" { 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 -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 == "" { return "", errors.New("You need a name for this table") } 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: 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 == "" { 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: 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 == "" { 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: 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) { if e != nil || !val { return @@ -167,7 +190,7 @@ func (a *PgsqlAdapter) AddForeignKey(name string, table string, column string, f // 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 -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 == "" { 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 -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 == "" { 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 -func (a *PgsqlAdapter) SimpleDelete(name string, table string, where string) (string, error) { +func (a *PgsqlAdapter) SimpleDelete(name, table, where string) (string, error) { if 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 // 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 == "" { 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 -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 == "" { 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 -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 == "" { 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 -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 == "" { 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 -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 == "" { 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 -func (a *PgsqlAdapter) pushStatement(name string, stype string, q string) { +func (a *PgsqlAdapter) pushStatement(name, stype, q string) { if name == "" { return } diff --git a/query_gen/querygen.go b/query_gen/querygen.go index e0ee5df3..948c1c0f 100644 --- a/query_gen/querygen.go +++ b/query_gen/querygen.go @@ -130,22 +130,26 @@ type Adapter interface { BuildConn(config map[string]string) (*sql.DB, error) DbVersion() string - DropTable(name string, table string) (string, error) - CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) + DropTable(name, table string) (string, error) + CreateTable(name, table, charset, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) // TODO: Some way to add indices and keys // TODO: Test this - AddColumn(name string, table string, column DBTableColumn, key *DBTableKey) (string, error) - AddIndex(name string, table string, iname string, colname string) (string, error) - AddKey(name string, table string, column string, key DBTableKey) (string, error) - AddForeignKey(name string, table string, column string, ftable string, fcolumn string, cascade bool) (out string, e error) - SimpleInsert(name string, table string, columns string, fields string) (string, error) + AddColumn(name, table string, column DBTableColumn, key *DBTableKey) (string, error) + DropColumn(name, table, colname string) (string, error) + RenameColumn(name, table, oldName, newName string) (string, error) + ChangeColumn(name, table, colName string, col DBTableColumn) (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) SimpleUpdateSelect(b *updatePrebuilder) (string, error) // ! Experimental - SimpleDelete(name string, table string, where string) (string, error) - Purge(name string, table string) (string, error) - SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) + SimpleDelete(name, table, where string) (string, error) + Purge(name, table string) (string, error) + SimpleSelect(name, table, columns, where, orderby, limit string) (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) SimpleInsertSelect(string, DBInsert, DBSelect) (string, error) SimpleInsertLeftJoin(string, DBInsert, DBJoin) (string, error) @@ -192,9 +196,9 @@ func PrepareMySQLUpsertCallback(db *sql.DB, query string) (*MySQLUpsertCallback, type LitStr string // TODO: Test this -func InterfaceMapToInsertStrings(data map[string]interface{}, order string) (cols string, values string) { - var done = make(map[string]bool) - var addValue = func(value interface{}) { +func InterfaceMapToInsertStrings(data map[string]interface{}, order string) (cols, values string) { + done := make(map[string]bool) + addValue := func(value interface{}) { switch value := value.(type) { case string: values += "'" + strings.Replace(value, "'", "\\'", -1) + "'," diff --git a/router_gen/routes.go b/router_gen/routes.go index aa3c4b58..edfbdd20 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -127,6 +127,7 @@ func topicRoutes() *RouteGroup { Action("routes.UnlockTopicSubmit", "/topic/unlock/submit/", "extraData"), Action("routes.MoveTopicSubmit", "/topic/move/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)"), Action("routes.RemoveAttachFromTopicSubmit", "/topic/attach/remove/submit/", "extraData"), ) diff --git a/routes.go b/routes.go index bd55bc86..b8a695c0 100644 --- a/routes.go +++ b/routes.go @@ -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 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) // TODO: Split this into it's own function diff --git a/routes/profile_reply.go b/routes/profile_reply.go index 40cbf2b2..c8bafa6f 100644 --- a/routes/profile_reply.go +++ b/routes/profile_reply.go @@ -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) } // 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 { return c.InternalError(err, w, r) } // ! 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) if err != nil { return c.InternalError(err, w, r) diff --git a/routes/reply.go b/routes/reply.go index 8ec2bc8d..53c38046 100644 --- a/routes/reply.go +++ b/routes/reply.go @@ -136,7 +136,7 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user c.User) c.Ro 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 { return c.InternalErrorJSQ(err, w, r, js) } diff --git a/routes/topic.go b/routes/topic.go index b847e844..2f6fcb9c 100644 --- a/routes/topic.go +++ b/routes/topic.go @@ -346,10 +346,10 @@ func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, user c.User) c.Ro return c.NoPermissions(w, r, user) } - tname := c.SanitiseSingleLine(r.PostFormValue("name")) + name := c.SanitiseSingleLine(r.PostFormValue("name")) content := c.PreparseMessage(r.PostFormValue("content")) // 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 { switch err { case c.ErrNoRows: @@ -959,3 +959,55 @@ func LikeTopicSubmit(w http.ResponseWriter, r *http.Request, user c.User, stid s } 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 +} diff --git a/schema/mssql/inserts.sql b/schema/mssql/inserts.sql index 795e463d..0811face 100644 --- a/schema/mssql/inserts.sql +++ b/schema/mssql/inserts.sql @@ -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 (5,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 [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 [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],[ip]) VALUES (1,'A reply!','A reply!',GETUTCDATE(),1,GETUTCDATE(),0,0,'::1'); 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],[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); diff --git a/schema/mssql/query_activity_stream.sql b/schema/mssql/query_activity_stream.sql index 944f990c..29f89934 100644 --- a/schema/mssql/query_activity_stream.sql +++ b/schema/mssql/query_activity_stream.sql @@ -6,5 +6,6 @@ CREATE TABLE [activity_stream] ( [elementType] nvarchar (50) not null, [elementID] int not null, [createdAt] datetime not null, + [extra] nvarchar (200) DEFAULT '' not null, primary key([asid]) ); \ No newline at end of file diff --git a/schema/mssql/query_polls_votes.sql b/schema/mssql/query_polls_votes.sql index 567fbc32..e63055f0 100644 --- a/schema/mssql/query_polls_votes.sql +++ b/schema/mssql/query_polls_votes.sql @@ -3,5 +3,5 @@ CREATE TABLE [polls_votes] ( [uid] int not null, [option] int DEFAULT 0 not null, [castAt] datetime not null, - [ipaddress] nvarchar (200) DEFAULT '0.0.0.0.0' not null + [ip] nvarchar (200) DEFAULT '' not null ); \ No newline at end of file diff --git a/schema/mssql/query_replies.sql b/schema/mssql/query_replies.sql index d10dbc64..251b866f 100644 --- a/schema/mssql/query_replies.sql +++ b/schema/mssql/query_replies.sql @@ -8,7 +8,7 @@ CREATE TABLE [replies] ( [lastEdit] int DEFAULT 0 not null, [lastEditBy] int DEFAULT 0 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, [attachCount] int DEFAULT 0 not null, [words] int DEFAULT 1 not null, diff --git a/schema/mssql/query_topics.sql b/schema/mssql/query_topics.sql index 019078ea..cd4de826 100644 --- a/schema/mssql/query_topics.sql +++ b/schema/mssql/query_topics.sql @@ -11,7 +11,7 @@ CREATE TABLE [topics] ( [is_closed] bit DEFAULT 0 not null, [sticky] bit DEFAULT 0 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, [likeCount] int DEFAULT 0 not null, [attachCount] int DEFAULT 0 not null, diff --git a/schema/mssql/query_users_replies.sql b/schema/mssql/query_users_replies.sql index 3ded1423..ed0af8f4 100644 --- a/schema/mssql/query_users_replies.sql +++ b/schema/mssql/query_users_replies.sql @@ -7,6 +7,6 @@ CREATE TABLE [users_replies] ( [createdBy] int not null, [lastEdit] 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]) ); \ No newline at end of file diff --git a/schema/mysql/inserts.sql b/schema/mysql/inserts.sql index 11bdac1b..69af871e 100644 --- a/schema/mysql/inserts.sql +++ b/schema/mysql/inserts.sql @@ -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 (5,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 `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 `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`,`ip`) VALUES (1,'A reply!','A reply!',UTC_TIMESTAMP(),1,UTC_TIMESTAMP(),0,0,'::1'); 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`,`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); diff --git a/schema/mysql/query_activity_stream.sql b/schema/mysql/query_activity_stream.sql index 30ab0b59..e5112ac1 100644 --- a/schema/mysql/query_activity_stream.sql +++ b/schema/mysql/query_activity_stream.sql @@ -6,5 +6,6 @@ CREATE TABLE `activity_stream` ( `elementType` varchar(50) not null, `elementID` int not null, `createdAt` datetime not null, + `extra` varchar(200) DEFAULT '' not null, primary key(`asid`) ); \ No newline at end of file diff --git a/schema/mysql/query_polls_votes.sql b/schema/mysql/query_polls_votes.sql index 364940af..c8b9e95f 100644 --- a/schema/mysql/query_polls_votes.sql +++ b/schema/mysql/query_polls_votes.sql @@ -3,5 +3,5 @@ CREATE TABLE `polls_votes` ( `uid` int not null, `option` int DEFAULT 0 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; \ No newline at end of file diff --git a/schema/mysql/query_replies.sql b/schema/mysql/query_replies.sql index 4341ade3..207f16b7 100644 --- a/schema/mysql/query_replies.sql +++ b/schema/mysql/query_replies.sql @@ -8,7 +8,7 @@ CREATE TABLE `replies` ( `lastEdit` int DEFAULT 0 not null, `lastEditBy` int DEFAULT 0 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, `attachCount` int DEFAULT 0 not null, `words` int DEFAULT 1 not null, diff --git a/schema/mysql/query_topics.sql b/schema/mysql/query_topics.sql index 563ee144..2cde46c4 100644 --- a/schema/mysql/query_topics.sql +++ b/schema/mysql/query_topics.sql @@ -11,7 +11,7 @@ CREATE TABLE `topics` ( `is_closed` boolean DEFAULT 0 not null, `sticky` boolean DEFAULT 0 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, `likeCount` int DEFAULT 0 not null, `attachCount` int DEFAULT 0 not null, diff --git a/schema/mysql/query_users_replies.sql b/schema/mysql/query_users_replies.sql index 6d5130b4..7fd5d471 100644 --- a/schema/mysql/query_users_replies.sql +++ b/schema/mysql/query_users_replies.sql @@ -7,6 +7,6 @@ CREATE TABLE `users_replies` ( `createdBy` int not null, `lastEdit` 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`) ) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci; \ No newline at end of file diff --git a/schema/pgsql/inserts.sql b/schema/pgsql/inserts.sql index 75e8f633..5e32e921 100644 --- a/schema/pgsql/inserts.sql +++ b/schema/pgsql/inserts.sql @@ -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 (5,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 "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 "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","ip") VALUES (1,'A reply!','A reply!',UTC_TIMESTAMP(),1,UTC_TIMESTAMP(),0,0,'::1'); 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","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); diff --git a/schema/pgsql/query_activity_stream.sql b/schema/pgsql/query_activity_stream.sql index 72c6801b..63881468 100644 --- a/schema/pgsql/query_activity_stream.sql +++ b/schema/pgsql/query_activity_stream.sql @@ -6,5 +6,6 @@ CREATE TABLE "activity_stream" ( `elementType` varchar (50) not null, `elementID` int not null, `createdAt` timestamp not null, + `extra` varchar (200) DEFAULT '' not null, primary key(`asid`) ); \ No newline at end of file diff --git a/schema/pgsql/query_polls_votes.sql b/schema/pgsql/query_polls_votes.sql index df709c36..e38adfa0 100644 --- a/schema/pgsql/query_polls_votes.sql +++ b/schema/pgsql/query_polls_votes.sql @@ -3,5 +3,5 @@ CREATE TABLE "polls_votes" ( `uid` int not null, `option` int DEFAULT 0 not null, `castAt` timestamp not null, - `ipaddress` varchar (200) DEFAULT '0.0.0.0.0' not null + `ip` varchar (200) DEFAULT '' not null ); \ No newline at end of file diff --git a/schema/pgsql/query_replies.sql b/schema/pgsql/query_replies.sql index 3d36452f..33e8fbe8 100644 --- a/schema/pgsql/query_replies.sql +++ b/schema/pgsql/query_replies.sql @@ -8,7 +8,7 @@ CREATE TABLE "replies" ( `lastEdit` int DEFAULT 0 not null, `lastEditBy` int DEFAULT 0 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, `attachCount` int DEFAULT 0 not null, `words` int DEFAULT 1 not null, diff --git a/schema/pgsql/query_topics.sql b/schema/pgsql/query_topics.sql index 9fff0b9e..75994d70 100644 --- a/schema/pgsql/query_topics.sql +++ b/schema/pgsql/query_topics.sql @@ -11,7 +11,7 @@ CREATE TABLE "topics" ( `is_closed` boolean DEFAULT 0 not null, `sticky` boolean DEFAULT 0 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, `likeCount` int DEFAULT 0 not null, `attachCount` int DEFAULT 0 not null, diff --git a/schema/pgsql/query_users_replies.sql b/schema/pgsql/query_users_replies.sql index eefa2350..a5182e81 100644 --- a/schema/pgsql/query_users_replies.sql +++ b/schema/pgsql/query_users_replies.sql @@ -7,6 +7,6 @@ CREATE TABLE "users_replies" ( `createdBy` int not null, `lastEdit` 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`) ); \ No newline at end of file diff --git a/templates/topic_alt.html b/templates/topic_alt.html index b2f4133a..0a7d4ffd 100644 --- a/templates/topic_alt.html +++ b/templates/topic_alt.html @@ -83,7 +83,9 @@
{{if .CurrentUser.Loggedin}} - {{if .CurrentUser.Perms.LikeItem}}{{if ne .CurrentUser.ID .Topic.CreatedBy}}{{end}}{{end}} + {{if .CurrentUser.Perms.LikeItem}}{{if ne .CurrentUser.ID .Topic.CreatedBy}} + {{if .Topic.Liked}}{{else}}{{end}} + {{end}}{{end}} {{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}} {{if .CurrentUser.Perms.EditTopic}}{{end}} diff --git a/tickloop.go b/tickloop.go index 36e564b8..78d66fb9 100644 --- a/tickloop.go +++ b/tickloop.go @@ -2,7 +2,6 @@ package main import ( "database/sql" - "errors" "log" "strconv" "sync/atomic" @@ -10,6 +9,7 @@ import ( c "github.com/Azareal/Gosora/common" 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? @@ -189,7 +189,7 @@ func dailies() { if c.Config.DisablePostIP { 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 { c.LogError(err) } @@ -200,7 +200,7 @@ func dailies() { } else if c.Config.PostIPCutoff > -1 { // TODO: Use unixtime to remove this MySQLesque logic? 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 { c.LogError(err) } @@ -211,13 +211,13 @@ func dailies() { } 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 { c.LogError(err) } } else if c.Config.PollIPCutoff > -1 { // 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 { c.LogError(err) } @@ -237,7 +237,7 @@ func dailies() { c.LogError(err) }*/ 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 { 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 +}