diff --git a/common/alerts.go b/common/alerts.go index cc045d52..417c8f3a 100644 --- a/common/alerts.go +++ b/common/alerts.go @@ -12,10 +12,21 @@ import ( "strconv" "strings" - "github.com/Azareal/Gosora/query_gen" "github.com/Azareal/Gosora/common/phrases" + "github.com/Azareal/Gosora/query_gen" ) +type Alert struct { + ASID int + ActorID int + TargetUserID int + Event string + ElementType string + ElementID int + + Actor *User +} + type AlertStmts struct { addActivity *sql.Stmt notifyWatchers *sql.Stmt @@ -50,81 +61,82 @@ func escapeTextInJson(in string) string { return strings.Replace(in, "/", "\\/", -1) } -func BuildAlert(asid int, event string, elementType string, actorID int, targetUserID int, elementID int, user User /* The current user */) (string, error) { +func BuildAlert(alert Alert, user User /* The current user */) (out string, err error) { var targetUser *User - - actor, err := Users.Get(actorID) - if err != nil { - return "", errors.New(phrases.GetErrorPhrase("alerts_no_actor")) + if alert.Actor == nil { + alert.Actor, err = Users.Get(alert.ActorID) + if err != nil { + return "", errors.New(phrases.GetErrorPhrase("alerts_no_actor")) + } } - /*if elementType != "forum" { - targetUser, err = users.Get(targetUserID) + /*if alert.ElementType != "forum" { + targetUser, err = users.Get(alert.TargetUserID) if err != nil { LocalErrorJS("Unable to find the target user",w,r) return } }*/ - if event == "friend_invite" { - return buildAlertString(phrases.GetTmplPhrase("alerts.new_friend_invite"), []string{actor.Name}, actor.Link, actor.Avatar, asid), nil + if alert.Event == "friend_invite" { + return buildAlertString(phrases.GetTmplPhrase("alerts.new_friend_invite"), []string{alert.Actor.Name}, alert.Actor.Link, alert.Actor.Avatar, alert.ASID), nil } // Not that many events for us to handle in a forum - if elementType == "forum" { - if event == "reply" { - topic, err := Topics.Get(elementID) + if alert.ElementType == "forum" { + if alert.Event == "reply" { + topic, err := Topics.Get(alert.ElementID) if err != nil { - DebugLogf("Unable to find linked topic %d", elementID) + DebugLogf("Unable to find linked topic %d", alert.ElementID) return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic")) } // Store the forum ID in the targetUser column instead of making a new one? o.O // Add an additional column for extra information later on when we add the ability to link directly to posts. We don't need the forum data for now... - return buildAlertString(phrases.GetTmplPhrase("alerts.forum_new_topic"), []string{actor.Name, topic.Title}, topic.Link, actor.Avatar, asid), nil + return buildAlertString(phrases.GetTmplPhrase("alerts.forum_new_topic"), []string{alert.Actor.Name, topic.Title}, topic.Link, alert.Actor.Avatar, alert.ASID), nil } - return buildAlertString(phrases.GetTmplPhrase("alerts.forum_unknown_action"), []string{actor.Name}, "", actor.Avatar, asid), nil + return buildAlertString(phrases.GetTmplPhrase("alerts.forum_unknown_action"), []string{alert.Actor.Name}, "", alert.Actor.Avatar, alert.ASID), nil } var url, area string - var phraseName = "alerts." + elementType - switch elementType { + var phraseName = "alerts." + alert.ElementType + switch alert.ElementType { case "topic": - topic, err := Topics.Get(elementID) + topic, err := Topics.Get(alert.ElementID) if err != nil { - DebugLogf("Unable to find linked topic %d", elementID) + DebugLogf("Unable to find linked topic %d", alert.ElementID) return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic")) } url = topic.Link area = topic.Title - if targetUserID == user.ID { + if alert.TargetUserID == user.ID { phraseName += "_own" } case "user": - targetUser, err = Users.Get(elementID) + targetUser, err = Users.Get(alert.ElementID) if err != nil { - DebugLogf("Unable to find target user %d", elementID) + DebugLogf("Unable to find target user %d", alert.ElementID) return "", errors.New(phrases.GetErrorPhrase("alerts_no_target_user")) } area = targetUser.Name url = targetUser.Link - if targetUserID == user.ID { + if alert.TargetUserID == user.ID { phraseName += "_own" } case "post": - topic, err := TopicByReplyID(elementID) + topic, err := TopicByReplyID(alert.ElementID) if err != nil { return "", errors.New(phrases.GetErrorPhrase("alerts_no_linked_topic_by_reply")) } url = topic.Link area = topic.Title - if targetUserID == user.ID { + if alert.TargetUserID == user.ID { phraseName += "_own" } default: return "", errors.New(phrases.GetErrorPhrase("alerts_invalid_elementtype")) } - switch event { + switch alert.Event { case "like": phraseName += "_like" case "mention": @@ -133,7 +145,7 @@ func BuildAlert(asid int, event string, elementType string, actorID int, targetU phraseName += "_reply" } - return buildAlertString(phrases.GetTmplPhrase(phraseName), []string{actor.Name, area}, url, actor.Avatar, asid), nil + return buildAlertString(phrases.GetTmplPhrase(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 { @@ -160,22 +172,25 @@ func AddActivityAndNotifyAll(actor int, targetUser int, event string, elementTyp return NotifyWatchers(lastID) } -func AddActivityAndNotifyTarget(actor int, targetUser int, event string, elementType string, elementID int) error { - res, err := alertStmts.addActivity.Exec(actor, targetUser, event, elementType, elementID) - if err != nil { - return err - } - lastID, err := res.LastInsertId() - if err != nil { - return err - } - err = NotifyOne(targetUser, lastID) +func AddActivityAndNotifyTarget(alert Alert) error { + res, err := alertStmts.addActivity.Exec(alert.ActorID, alert.TargetUserID, alert.Event, alert.ElementType, alert.ElementID) if err != nil { return err } + lastID, err := res.LastInsertId() + if err != nil { + return err + } + + err = NotifyOne(alert.TargetUserID, lastID) + if err != nil { + return err + } + alert.ASID = int(lastID) + // Live alerts, if the target is online and WebSockets is enabled - _ = WsHub.pushAlert(targetUser, int(lastID), event, elementType, actor, targetUser, elementID) + _ = WsHub.pushAlert(alert.TargetUserID, alert) return nil } @@ -221,13 +236,12 @@ func notifyWatchers(asid int64) { return } - var actorID, targetUserID, elementID int - var event, elementType string - err = alertStmts.getActivityEntry.QueryRow(asid).Scan(&actorID, &targetUserID, &event, &elementType, &elementID) + var alert = Alert{ASID: int(asid)} + err = alertStmts.getActivityEntry.QueryRow(asid).Scan(&alert.ActorID, &alert.TargetUserID, &alert.Event, &alert.ElementType, &alert.ElementID) if err != nil && err != ErrNoRows { LogError(err) return } - _ = WsHub.pushAlerts(uids, int(asid), event, elementType, actorID, targetUserID, elementID) + _ = WsHub.pushAlerts(uids, alert) } diff --git a/common/files.go b/common/files.go index fba5adfc..60cfa3a7 100644 --- a/common/files.go +++ b/common/files.go @@ -147,6 +147,13 @@ func (list SFileList) JSTmplInit() error { data[braceAt] = ' ' // Blank this one too } }) + each(" = []byte(", func(index int) { + braceAt, hasEndBrace := skipUntilIfExists(data, index, ')') + // TODO: Make sure we don't go onto the next line in case someone misplaced a brace + if hasEndBrace { + data[braceAt] = ' ' // Blank it + } + }) each("w.Write(", func(index int) { braceAt, hasEndBrace := skipUntilIfExists(data, index, ')') // TODO: Make sure we don't go onto the next line in case someone misplaced a brace @@ -185,6 +192,8 @@ func (list SFileList) JSTmplInit() error { data = replace(data, shortName+"_tmpl_phrase_id = RegisterTmplPhraseNames([]string{", "[") data = replace(data, "var plist = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "let plist = tmplPhrases[\""+tmplName+"\"];") //data = replace(data, "var phrases = GetTmplPhrasesBytes("+shortName+"_tmpl_phrase_id)", "let phrases = tmplPhrases[\""+tmplName+"\"];\nconsole.log('tmplName:','"+tmplName+"')\nconsole.log('phrases:', phrases);") + data = replace(data, "var cached_var_", "let cached_var_") + data = replace(data, " = []byte(", " = ") data = replace(data, "if ", "if(") data = replace(data, "return nil", "return out") data = replace(data, " )", ")") diff --git a/common/tasks.go b/common/tasks.go index f1957047..97e39c96 100644 --- a/common/tasks.go +++ b/common/tasks.go @@ -96,9 +96,9 @@ func HandleExpiredScheduledGroups() error { // TODO: Does this even work? func HandleServerSync() error { // We don't want to run any unnecessary queries when there is nothing to synchronise - /*if Config.ServerCount > 1 { + if Config.ServerCount == 1 { return nil - }*/ + } var lastUpdate time.Time err := taskStmts.getSync.QueryRow().Scan(&lastUpdate) diff --git a/common/templates/context.go b/common/templates/context.go index 68ab718e..53329e58 100644 --- a/common/templates/context.go +++ b/common/templates/context.go @@ -4,6 +4,11 @@ import ( "reflect" ) +// For use in generated code +type FragLite struct { + Body string +} + type Fragment struct { Body string TemplateName string @@ -23,6 +28,7 @@ type CContext struct { VarHolder string HoldReflect reflect.Value TemplateName string + LoopDepth int OutBuf *[]OutBufferFrame } @@ -35,3 +41,36 @@ func (con *CContext) PushText(body string, fragIndex int, fragOutIndex int) (ind *con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "text", con.TemplateName, fragIndex, fragOutIndex}) return len(*con.OutBuf) - 1 } + +func (con *CContext) PushPhrase(body string, langIndex int) (index int) { + *con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "lang", con.TemplateName, langIndex, nil}) + return len(*con.OutBuf) - 1 +} + +func (con *CContext) StartLoop(body string) (index int) { + con.LoopDepth++ + return con.Push("startloop", body) +} + +func (con *CContext) EndLoop(body string) (index int) { + return con.Push("endloop", body) +} + +func (con *CContext) StartTemplate(body string) (index int) { + *con.OutBuf = append(*con.OutBuf, OutBufferFrame{body, "starttemplate", con.TemplateName, nil, nil}) + return len(*con.OutBuf) - 1 +} + +func (con *CContext) EndTemplate(body string) (index int) { + return con.Push("endtemplate", body) +} + +func (con *CContext) AttachVars(vars string, index int) { + outBuf := *con.OutBuf + node := outBuf[index] + if node.Type != "starttemplate" && node.Type != "startloop" { + panic("not a starttemplate node") + } + node.Body += vars + outBuf[index] = node +} diff --git a/common/templates/templates.go b/common/templates/templates.go index 8fa71476..3f1e90b3 100644 --- a/common/templates/templates.go +++ b/common/templates/templates.go @@ -189,7 +189,10 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe c.TemplateFragmentCount = make(map[string]int) } + startIndex := con.StartTemplate("") c.rootIterate(c.templateList[fname], con) + con.EndTemplate("") + c.afterTemplate(con, startIndex) c.TemplateFragmentCount[fname] = c.fragmentCursor[fname] + 1 _, ok := c.FragOnce[fname] @@ -280,6 +283,9 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe skipBlock.Frags[frame.Extra.(int)] = skipBlock.LastCount } writeTextFrame(frame.TemplateName, frame.Extra.(int)-skip) + } else if frame.Type == "varsub" || frame.Type == "cvarsub" { + c.detail(frame.Type + " frame") + fout += "w.Write(" + frame.Body + ")\n" } else { c.detail(frame.Type + " frame") fout += frame.Body @@ -425,12 +431,24 @@ func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) { var startIf = func(item reflect.Value, useCopy bool) { con.Push("startif", "if len("+expr+") != 0 {\n") - con.Push("startloop", "for _, item := range "+expr+" {\n") + startIndex := con.StartLoop("for _, item := range " + expr + " {\n") ccon := con - ccon.VarHolder = "item" + var depth string + if ccon.VarHolder == "item" { + depth = strings.TrimPrefix(ccon.VarHolder, "item") + if depth != "" { + idepth, err := strconv.Atoi(depth) + if err != nil { + panic(err) + } + depth = strconv.Itoa(idepth + 1) + } + } + ccon.VarHolder = "item" + depth ccon.HoldReflect = item c.compileSwitch(ccon, node.List) - con.Push("endloop", "}\n") + con.EndLoop("}\n") + c.afterTemplate(con, startIndex) if node.ElseList != nil { con.Push("endif", "}") con.Push("startelse", " else {\n") @@ -440,7 +458,7 @@ func (c *CTemplateSet) compileRangeNode(con CContext, node *parse.RangeNode) { c.compileSwitch(ccon, node.ElseList) con.Push("endelse", "}\n") } else { - con.Push("endloop", "}\n") + con.Push("endif", "}\n") } } @@ -1098,10 +1116,10 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V base = "[]byte(strconv.Itoa(" + varname + "))" case reflect.Bool: con.Push("startif", "if "+varname+" {\n") - con.Push("varsub", "w.Write([]byte(\"true\"))") + con.Push("varsub", "[]byte(\"true\")") con.Push("endif", "} ") con.Push("startelse", "else {\n") - con.Push("varsub", "w.Write([]byte(\"false\"))") + con.Push("varsub", "[]byte(\"false\")") con.Push("endelse", "}\n") return case reflect.String: @@ -1121,7 +1139,6 @@ func (c *CTemplateSet) compileVarSub(con CContext, varname string, val reflect.V fmt.Println("Unknown Type:", val.Type().Name()) panic("-- I don't know what this variable's type is o.o\n") } - base = "w.Write(" + base + ")\n" c.detail("base: ", base) if assLines == "" { con.Push("varsub", base) @@ -1179,13 +1196,18 @@ func (c *CTemplateSet) compileSubTemplate(pcon CContext, node *parse.TemplateNod c.templateList[fname] = tree subtree := c.templateList[fname] c.detail("subtree.Root", subtree.Root) - c.localVars[fname] = make(map[string]VarItemReflect) c.localVars[fname]["."] = VarItemReflect{".", con.VarHolder, con.HoldReflect} c.fragmentCursor[fname] = 0 - con.Push("starttemplate", "{\n") + + var startBit, endBit string + if con.LoopDepth != 0 { + startBit = "{\n" + endBit = "}\n" + } + con.StartTemplate(startBit) c.rootIterate(subtree, con) - con.Push("endtemplate", "}\n") + con.EndTemplate(endBit) c.TemplateFragmentCount[fname] = c.fragmentCursor[fname] + 1 _, ok := c.FragOnce[fname] @@ -1194,6 +1216,75 @@ func (c *CTemplateSet) compileSubTemplate(pcon CContext, node *parse.TemplateNod } } +func (c *CTemplateSet) afterTemplate(con CContext, startIndex int) { + c.dumpCall("afterTemplate", con, startIndex) + defer c.retCall("afterTemplate") + + var loopDepth = 0 + var outBuf = *con.OutBuf + var varcounts = make(map[string]int) + var loopStart = startIndex + + if outBuf[startIndex].Type == "startloop" && (len(outBuf) > startIndex+1) { + loopStart++ + } + + // Exclude varsubs within loops for now + for i := loopStart; i < len(outBuf); i++ { + item := outBuf[i] + c.detail("item:", item) + if item.Type == "startloop" { + loopDepth++ + c.detail("loopDepth:", loopDepth) + } else if item.Type == "endloop" { + loopDepth-- + c.detail("loopDepth:", loopDepth) + if loopDepth == -1 { + break + } + } else if item.Type == "varsub" && loopDepth == 0 { + count := varcounts[item.Body] + varcounts[item.Body] = count + 1 + c.detail("count " + strconv.Itoa(count) + " for " + item.Body) + c.detail("loopDepth:", loopDepth) + } + } + + var varstr string + var i int + var varmap = make(map[string]int) + for name, count := range varcounts { + if count > 1 { + varstr += "var cached_var_" + strconv.Itoa(i) + " = " + name + "\n" + varmap[name] = i + i++ + } + } + + // Exclude varsubs within loops for now + loopDepth = 0 + for i := loopStart; i < len(outBuf); i++ { + item := outBuf[i] + if item.Type == "startloop" { + loopDepth++ + } else if item.Type == "endloop" { + loopDepth-- + if loopDepth == -1 { + break + } + } else if item.Type == "varsub" && loopDepth == 0 { + index, ok := varmap[item.Body] + if ok { + item.Body = "cached_var_" + strconv.Itoa(index) + item.Type = "cvarsub" + outBuf[i] = item + } + } + } + + con.AttachVars(varstr, startIndex) +} + // TODO: Should we rethink the way the log methods work or their names? func (c *CTemplateSet) detail(args ...interface{}) { diff --git a/common/topic.go b/common/topic.go index 2ed673ec..6275c5e2 100644 --- a/common/topic.go +++ b/common/topic.go @@ -345,17 +345,18 @@ func TopicByReplyID(rid int) (*Topic, error) { // TODO: Refactor the caller to take a Topic and a User rather than a combined TopicUser // TODO: Load LastReplyAt everywhere in here? -func GetTopicUser(tid int) (TopicUser, error) { +func GetTopicUser(user *User, tid int) (tu TopicUser, err error) { tcache := Topics.GetCache() ucache := Users.GetCache() if tcache != nil && ucache != nil { topic, err := tcache.Get(tid) if err == nil { - user, err := Users.Get(topic.CreatedBy) - if err != nil { - return TopicUser{ID: tid}, err + if topic.CreatedBy != user.ID { + user, err = Users.Get(topic.CreatedBy) + if err != nil { + return TopicUser{ID: tid}, err + } } - // We might be better off just passing separate topic and user structs to the caller? return copyTopicToTopicUser(topic, user), nil } else if ucache.Length() < ucache.GetCapacity() { @@ -363,16 +364,18 @@ func GetTopicUser(tid int) (TopicUser, error) { if err != nil { return TopicUser{ID: tid}, err } - user, err := Users.Get(topic.CreatedBy) - if err != nil { - return TopicUser{ID: tid}, err + if topic.CreatedBy != user.ID { + user, err = Users.Get(topic.CreatedBy) + if err != nil { + return TopicUser{ID: tid}, err + } } return copyTopicToTopicUser(topic, user), nil } } - tu := TopicUser{ID: tid} - err := topicStmts.getTopicUser.QueryRow(tid).Scan(&tu.Title, &tu.Content, &tu.CreatedBy, &tu.CreatedAt, &tu.IsClosed, &tu.Sticky, &tu.ParentID, &tu.IPAddress, &tu.ViewCount, &tu.PostCount, &tu.LikeCount, &tu.Poll, &tu.CreatedByName, &tu.Avatar, &tu.Group, &tu.URLPrefix, &tu.URLName, &tu.Level) + tu = TopicUser{ID: tid} + err = topicStmts.getTopicUser.QueryRow(tid).Scan(&tu.Title, &tu.Content, &tu.CreatedBy, &tu.CreatedAt, &tu.IsClosed, &tu.Sticky, &tu.ParentID, &tu.IPAddress, &tu.ViewCount, &tu.PostCount, &tu.LikeCount, &tu.Poll, &tu.CreatedByName, &tu.Avatar, &tu.Group, &tu.URLPrefix, &tu.URLName, &tu.Level) tu.Avatar, tu.MicroAvatar = BuildAvatar(tu.CreatedBy, tu.Avatar) tu.Link = BuildTopicURL(NameToSlug(tu.Title), tu.ID) tu.UserLink = BuildProfileURL(NameToSlug(tu.CreatedByName), tu.CreatedBy) diff --git a/common/ws_hub.go b/common/ws_hub.go index 8fe4a532..6e2bbc7f 100644 --- a/common/ws_hub.go +++ b/common/ws_hub.go @@ -340,21 +340,19 @@ func (hub *WsHubImpl) PushMessage(targetUser int, msg string) error { return wsUser.WriteAll(msg) } -func (hub *WsHubImpl) pushAlert(targetUser int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error { +func (hub *WsHubImpl) pushAlert(targetUser int, alert Alert) error { wsUser, err := hub.getUser(targetUser) if err != nil { return err } - - alert, err := BuildAlert(asid, event, elementType, actorID, targetUserID, elementID, *wsUser.User) + astr, err := BuildAlert(alert, *wsUser.User) if err != nil { return err } - - return wsUser.WriteAll(alert) + return wsUser.WriteAll(astr) } -func (hub *WsHubImpl) pushAlerts(users []int, asid int, event string, elementType string, actorID int, targetUserID int, elementID int) error { +func (hub *WsHubImpl) pushAlerts(users []int, alert Alert) error { wsUsers, err := hub.getUsers(users) if err != nil { return err @@ -365,8 +363,7 @@ func (hub *WsHubImpl) pushAlerts(users []int, asid int, event string, elementTyp if wsUser == nil { continue } - - alert, err := BuildAlert(asid, event, elementType, actorID, targetUserID, elementID, *wsUser.User) + alert, err := BuildAlert(alert, *wsUser.User) if err != nil { errs = append(errs, err) } diff --git a/main.go b/main.go index 7565d0d5..0ef5e787 100644 --- a/main.go +++ b/main.go @@ -377,7 +377,7 @@ func main() { go tickLoop(thumbChan, halfSecondTicker, secondTicker, fifteenMinuteTicker, hourTicker) // Resource Management Goroutine - /*go func() { + go func() { ucache := common.Users.GetCache() tcache := common.Topics.GetCache() if ucache == nil && tcache == nil { @@ -393,17 +393,15 @@ func main() { if ucache != nil { ucap := ucache.GetCapacity() if ucache.Length() <= ucap || common.Users.GlobalCount() <= ucap { - countNotDealloc = false + couldNotDealloc = false continue } - lastEvictedCount = ucache.DeallocOverflow(countNotDealloc) - countNotDealloc = (lastEvictedCount == 0) - } else { - countNotDealloc = false + lastEvictedCount = ucache.DeallocOverflow(couldNotDealloc) + couldNotDealloc = (lastEvictedCount == 0) } } } - }()*/ + }() log.Print("Initialising the router") router, err = NewGenRouter(http.FileServer(http.Dir("./uploads"))) diff --git a/misc_test.go b/misc_test.go index 1c5ab8dd..c93150d7 100644 --- a/misc_test.go +++ b/misc_test.go @@ -1213,137 +1213,3 @@ func TestWordCount(t *testing.T) { } } } - -func TestPreparser(t *testing.T) { - var msgList = &METriList{nil} - - // Note: The open tag is evaluated without knowledge of the close tag for efficiency and simplicity, so the parser autofills the associated close tag when it finds an open tag without a partner - msgList.Add("", "") - msgList.Add(" ", "") - msgList.Add(" hi", "hi") - msgList.Add("hi ", "hi") - msgList.Add("hi", "hi") - msgList.Add(":grinning:", "😀") - msgList.Add("😀", "😀") - msgList.Add(" ", "") - msgList.Add("
", "") - msgList.Add("
", "") - msgList.Add("", "") - - msgList.Add("<", "<") - msgList.Add(">", ">") - msgList.Add("", "") + msgList.Add("
", "") + msgList.Add("", "") + + msgList.Add("<", "<") + msgList.Add(">", ">") + msgList.Add("