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 +}