diff --git a/.gitignore b/.gitignore index a962e99b..d9bd2617 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ logs/* *.log .DS_Store .vscode/launch.json +config.go diff --git a/README.md b/README.md index 6cad7054..c30ce412 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,11 @@ We recommend changing the root password (that is the password for the user 'root It's entirely possible that your host might already have MySQL, so you might be able to skip this step, particularly if it's a managed VPS or a shared host (contrary to popular belief, it is possible, although the ecosystem in this regard is extremely immature). Or they might have a quicker and easier method of setting up MySQL. +# Downloading + +At some point, we'll have releases which you can download, but right now, you'll have to use the `git clone` command as mentioned down in the advanced setup section to download a copy of Gosora. + + # Installation Instructions *Linux* diff --git a/build_templates.bat b/build_templates.bat index 68d74b24..c1419c68 100644 --- a/build_templates.bat +++ b/build_templates.bat @@ -1,2 +1,11 @@ +echo Building the templates gosora.exe -build-templates + +echo Rebuilding the executable +go build -o gosora.exe +if %errorlevel% neq 0 ( + pause + exit /b %errorlevel% +) + pause \ No newline at end of file diff --git a/common/reply.go b/common/reply.go index a12c9fc5..763e44b5 100644 --- a/common/reply.go +++ b/common/reply.go @@ -104,6 +104,10 @@ func (reply *Reply) Like(uid int) (err error) { return err } _, err = replyStmts.addLikesToReply.Exec(1, reply.ID) + if err != nil { + return err + } + _, err = userStmts.incrementLiked.Exec(1, uid) return err } diff --git a/common/template_init.go b/common/template_init.go index f5c20736..89eb020e 100644 --- a/common/template_init.go +++ b/common/template_init.go @@ -132,10 +132,10 @@ func CompileTemplates() error { // Schemas to train the template compiler on what to expect // TODO: Add support for interface{}s - user := User{62, BuildProfileURL("fake-user", 62), "Fake User", "compiler@localhost", 0, false, false, false, false, false, false, GuestPerms, make(map[string]bool), "", false, BuildAvatar(62, ""), "", "", "", "", 0, 0, "0.0.0.0.0", 0} + user := User{62, BuildProfileURL("fake-user", 62), "Fake User", "compiler@localhost", 0, false, false, false, false, false, false, GuestPerms, make(map[string]bool), "", false, BuildAvatar(62, ""), "", "", "", "", 0, 0, 0, "0.0.0.0.0", 0} // TODO: Do a more accurate level calculation for this? - user2 := User{1, BuildProfileURL("admin-alice", 1), "Admin Alice", "alice@localhost", 1, true, true, true, true, false, false, AllPerms, make(map[string]bool), "", true, BuildAvatar(1, ""), "", "", "", "", 58, 1000, "127.0.0.1", 0} - user3 := User{2, BuildProfileURL("admin-fred", 62), "Admin Fred", "fred@localhost", 1, true, true, true, true, false, false, AllPerms, make(map[string]bool), "", true, BuildAvatar(2, ""), "", "", "", "", 42, 900, "::1", 0} + user2 := User{1, BuildProfileURL("admin-alice", 1), "Admin Alice", "alice@localhost", 1, true, true, true, true, false, false, AllPerms, make(map[string]bool), "", true, BuildAvatar(1, ""), "", "", "", "", 58, 1000, 0, "127.0.0.1", 0} + user3 := User{2, BuildProfileURL("admin-fred", 62), "Admin Fred", "fred@localhost", 1, true, true, true, true, false, false, AllPerms, make(map[string]bool), "", true, BuildAvatar(2, ""), "", "", "", "", 42, 900, 0, "::1", 0} headerVars := &HeaderVars{ Site: Site, Settings: SettingBox.Load().(SettingMap), @@ -373,6 +373,10 @@ func InitTemplates() error { return GetTmplPhrase(phraseName) // TODO: Log non-existent phrases? } + fmap["scope"] = func(name interface{}) interface{} { + return "" + } + // The interpreted templates... DebugLog("Loading the template files...") Templates.Funcs(fmap) diff --git a/common/templates/templates.go b/common/templates/templates.go index 25cc8465..07434510 100644 --- a/common/templates/templates.go +++ b/common/templates/templates.go @@ -90,6 +90,7 @@ func (c *CTemplateSet) Compile(name string, fileDir string, expects string, expe "divide": true, "dock": true, "lang": true, + "scope": true, } c.importMap = map[string]string{ @@ -687,6 +688,9 @@ ArgLoop: out = "w.Write(phrases[" + strconv.Itoa(len(c.langIndexToName)-1) + "])\n" literal = true break ArgLoop + case "scope": + literal = true + break ArgLoop default: c.detail("Variable!") if len(node.Args) > (pos + 1) { diff --git a/common/topic.go b/common/topic.go index d2c7bf97..97fbd2bc 100644 --- a/common/topic.go +++ b/common/topic.go @@ -141,7 +141,7 @@ func init() { stick: acc.Update("topics").Set("sticky = 1").Where("tid = ?").Prepare(), unstick: acc.Update("topics").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").Fields("?,?,?,?").Prepare(), + createLike: acc.Insert("likes").Columns("weight, targetItem, targetType, sentBy, createdAt").Fields("?,?,?,?,UTC_TIMESTAMP()").Prepare(), addLikesToTopic: acc.Update("topics").Set("likeCount = likeCount + ?").Where("tid = ?").Prepare(), delete: acc.Delete("topics").Where("tid = ?").Prepare(), edit: acc.Update("topics").Set("title = ?, content = ?, parsed_content = ?").Where("tid = ?").Prepare(), // TODO: Only run the content update bits on non-polls, does this matter? @@ -205,20 +205,24 @@ func (topic *Topic) Unstick() (err error) { // TODO: Test this // TODO: Use a transaction for this func (topic *Topic) Like(score int, uid int) (err error) { - var tid int // Unused - err = topicStmts.hasLikedTopic.QueryRow(uid, topic.ID).Scan(&tid) + var disp int // Unused + err = topicStmts.hasLikedTopic.QueryRow(uid, topic.ID).Scan(&disp) if err != nil && err != ErrNoRows { return err } else if err != ErrNoRows { return ErrAlreadyLiked } - _, err = topicStmts.createLike.Exec(score, tid, "topics", uid) + _, err = topicStmts.createLike.Exec(score, topic.ID, "topics", uid) if err != nil { return err } - _, err = topicStmts.addLikesToTopic.Exec(1, tid) + _, err = topicStmts.addLikesToTopic.Exec(1, topic.ID) + if err != nil { + return err + } + _, err = userStmts.incrementLiked.Exec(1, uid) topic.cacheRemove() return err } diff --git a/common/user.go b/common/user.go index 0216930d..ad02df33 100644 --- a/common/user.go +++ b/common/user.go @@ -54,6 +54,7 @@ type User struct { Tag string Level int Score int + Liked int LastIP string // ! This part of the UserCache data might fall out of date TempGroup int } @@ -71,6 +72,8 @@ type UserStmts struct { incrementPosts *sql.Stmt incrementBigposts *sql.Stmt incrementMegaposts *sql.Stmt + incrementLiked *sql.Stmt + decrementLiked *sql.Stmt updateLastIP *sql.Stmt setPassword *sql.Stmt @@ -92,7 +95,10 @@ func init() { incrementPosts: acc.SimpleUpdate("users", "posts = posts + ?", "uid = ?"), incrementBigposts: acc.SimpleUpdate("users", "posts = posts + ?, bigposts = bigposts + ?", "uid = ?"), incrementMegaposts: acc.SimpleUpdate("users", "posts = posts + ?, bigposts = bigposts + ?, megaposts = megaposts + ?", "uid = ?"), - updateLastIP: acc.SimpleUpdate("users", "last_ip = ?", "uid = ?"), + incrementLiked: acc.SimpleUpdate("users", "liked = liked + ?, lastLiked = UTC_TIMESTAMP()", "uid = ?"), + decrementLiked: acc.SimpleUpdate("users", "liked = liked - ?", "uid = ?"), + //recalcLastLiked: acc... + updateLastIP: acc.SimpleUpdate("users", "last_ip = ?", "uid = ?"), setPassword: acc.SimpleUpdate("users", "password = ?, salt = ?", "uid = ?"), } diff --git a/common/user_store.go b/common/user_store.go index e8bd6667..2ed8dba1 100644 --- a/common/user_store.go +++ b/common/user_store.go @@ -50,7 +50,7 @@ func NewDefaultUserStore(cache UserCache) (*DefaultUserStore, error) { // TODO: Add an admin version of registerStmt with more flexibility? return &DefaultUserStore{ cache: cache, - get: acc.SimpleSelect("users", "name, group, active, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group", "uid = ?", "", ""), + get: acc.SimpleSelect("users", "name, group, active, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, liked, last_ip, temp_group", "uid = ?", "", ""), exists: acc.SimpleSelect("users", "uid", "uid = ?", "", ""), register: acc.SimpleInsert("users", "name, email, password, salt, group, is_super_admin, session, active, message, createdAt, lastActiveAt", "?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()"), // TODO: Implement user_count on users_groups here usernameExists: acc.SimpleSelect("users", "name", "name = ?", "", ""), @@ -65,7 +65,7 @@ func (mus *DefaultUserStore) DirtyGet(id int) *User { } user = &User{ID: id, Loggedin: true} - err = mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Active, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) + err = mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Active, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Liked, &user.LastIP, &user.TempGroup) user.Init() if err == nil { @@ -83,7 +83,7 @@ func (mus *DefaultUserStore) Get(id int) (*User, error) { } user = &User{ID: id, Loggedin: true} - err = mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Active, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) + err = mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Active, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Liked, &user.LastIP, &user.TempGroup) user.Init() if err == nil { @@ -127,14 +127,14 @@ func (mus *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err erro qlist = qlist[0 : len(qlist)-1] acc := qgen.Builder.Accumulator() - rows, err := acc.Select("users").Columns("uid, name, group, active, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, last_ip, temp_group").Where("uid IN(" + qlist + ")").Query(uidList...) + rows, err := acc.Select("users").Columns("uid, name, group, active, is_super_admin, session, email, avatar, message, url_prefix, url_name, level, score, liked, last_ip, temp_group").Where("uid IN(" + qlist + ")").Query(uidList...) if err != nil { return list, err } for rows.Next() { user := &User{Loggedin: true} - err := rows.Scan(&user.ID, &user.Name, &user.Group, &user.Active, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) + err := rows.Scan(&user.ID, &user.Name, &user.Group, &user.Active, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Liked, &user.LastIP, &user.TempGroup) if err != nil { return list, err } @@ -175,7 +175,7 @@ func (mus *DefaultUserStore) BulkGetMap(ids []int) (list map[int]*User, err erro func (mus *DefaultUserStore) BypassGet(id int) (*User, error) { user := &User{ID: id, Loggedin: true} - err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Active, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) + err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Active, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Liked, &user.LastIP, &user.TempGroup) user.Init() return user, err @@ -183,7 +183,7 @@ func (mus *DefaultUserStore) BypassGet(id int) (*User, error) { func (mus *DefaultUserStore) Reload(id int) error { user := &User{ID: id, Loggedin: true} - err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Active, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.LastIP, &user.TempGroup) + err := mus.get.QueryRow(id).Scan(&user.Name, &user.Group, &user.Active, &user.IsSuperAdmin, &user.Session, &user.Email, &user.Avatar, &user.Message, &user.URLPrefix, &user.URLName, &user.Level, &user.Score, &user.Liked, &user.LastIP, &user.TempGroup) if err != nil { mus.cache.Remove(id) return err diff --git a/database.go b/database.go index c29d1883..c79c760d 100644 --- a/database.go +++ b/database.go @@ -10,7 +10,6 @@ import ( var stmts *Stmts var db *sql.DB -var dbVersion string var dbAdapter string // ErrNoRows is an alias of sql.ErrNoRows, just in case we end up with non-database/sql datastores diff --git a/gen_router.go b/gen_router.go index 2c55e642..3d437545 100644 --- a/gen_router.go +++ b/gen_router.go @@ -462,6 +462,32 @@ func init() { counters.SetReverseOSMapEnum(reverseOSMapEnum) } +type WriterIntercept struct { + w http.ResponseWriter + code int +} + +func NewWriterIntercept(w http.ResponseWriter) *WriterIntercept { + return &WriterIntercept{w:w,code:200} +} + +func (writ *WriterIntercept) Header() http.Header { + return writ.w.Header() +} + +func (writ *WriterIntercept) Write(pieces []byte) (int, error) { + return writ.w.Write(pieces) +} + +func (writ *WriterIntercept) WriteHeader(code int) { + writ.w.WriteHeader(code) + writ.code = code +} + +func (writ *WriterIntercept) GetCode() int { + return writ.code +} + type GenRouter struct { UploadHandler func(http.ResponseWriter, *http.Request) extraRoutes map[string]func(http.ResponseWriter, *http.Request, common.User) common.RouteError @@ -471,7 +497,14 @@ type GenRouter struct { func NewGenRouter(uploads http.Handler) *GenRouter { return &GenRouter{ - UploadHandler: http.StripPrefix("/uploads/",uploads).ServeHTTP, + UploadHandler: func(w http.ResponseWriter, req *http.Request) { + writ := NewWriterIntercept(w) + http.StripPrefix("/uploads/",uploads).ServeHTTP(writ,req) + if writ.GetCode() == 200 { + w.Header().Set("Cache-Control", "max-age=" + strconv.Itoa(common.Day)) + w.Header().Set("Vary", "Accept-Encoding") + } + }, extraRoutes: make(map[string]func(http.ResponseWriter, *http.Request, common.User) common.RouteError), } } @@ -1514,6 +1547,12 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } + err = common.ParseForm(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + counters.RouteViewCounter.Bump(83) err = routeLikeTopicSubmit(w,req,user,extraData) default: @@ -1588,6 +1627,12 @@ func (router *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } + err = common.ParseForm(w,req,user) + if err != nil { + router.handleError(err,w,req,user) + return + } + counters.RouteViewCounter.Bump(88) err = routeReplyLikeSubmit(w,req,user,extraData) } diff --git a/member_routes.go b/member_routes.go index c85f1746..79a01020 100644 --- a/member_routes.go +++ b/member_routes.go @@ -16,16 +16,17 @@ import ( // TODO: Refactor this func routeLikeTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError { + isJs := (r.PostFormValue("isJs") == "1") tid, err := strconv.Atoi(stid) if err != nil { - return common.PreError("Topic IDs can only ever be numbers.", w, r) + return common.PreErrorJSQ("Topic IDs can only ever be numbers.", w, r, isJs) } topic, err := common.Topics.Get(tid) if err == ErrNoRows { - return common.PreError("The requested topic doesn't exist.", w, r) + return common.PreErrorJSQ("The requested topic doesn't exist.", w, r, isJs) } else if err != nil { - return common.InternalError(err, w, r) + return common.InternalErrorJSQ(err, w, r, isJs) } // TODO: Add hooks to make use of headerLite @@ -34,55 +35,62 @@ func routeLikeTopicSubmit(w http.ResponseWriter, r *http.Request, user common.Us return ferr } if !user.Perms.ViewTopic || !user.Perms.LikeItem { - return common.NoPermissions(w, r, user) + return common.NoPermissionsJSQ(w, r, user, isJs) } if topic.CreatedBy == user.ID { - return common.LocalError("You can't like your own topics", w, r, user) + return common.LocalErrorJSQ("You can't like your own topics", w, r, user, isJs) } _, err = common.Users.Get(topic.CreatedBy) if err != nil && err == ErrNoRows { - return common.LocalError("The target user doesn't exist", w, r, user) + return common.LocalErrorJSQ("The target user doesn't exist", w, r, user, isJs) } else if err != nil { - return common.InternalError(err, w, r) + return common.InternalErrorJSQ(err, w, r, isJs) } score := 1 err = topic.Like(score, user.ID) + //log.Print("likeErr: ", err) if err == common.ErrAlreadyLiked { - return common.LocalError("You already liked this", w, r, user) + return common.LocalErrorJSQ("You already liked this", w, r, user, isJs) } else if err != nil { - return common.InternalError(err, w, r) + return common.InternalErrorJSQ(err, w, r, isJs) } err = common.AddActivityAndNotifyTarget(user.ID, topic.CreatedBy, "like", "topic", tid) if err != nil { - return common.InternalError(err, w, r) + return common.InternalErrorJSQ(err, w, r, isJs) } - http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) + if !isJs { + http.Redirect(w, r, "/topic/"+strconv.Itoa(tid), http.StatusSeeOther) + } else { + _, _ = w.Write(successJSONBytes) + } return nil } func routeReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user common.User, srid string) common.RouteError { + isJs := (r.PostFormValue("isJs") == "1") + rid, err := strconv.Atoi(srid) if err != nil { - return common.PreError("The provided Reply ID is not a valid number.", w, r) + return common.PreErrorJSQ("The provided Reply ID is not a valid number.", w, r, isJs) } reply, err := common.Rstore.Get(rid) if err == ErrNoRows { - return common.PreError("You can't like something which doesn't exist!", w, r) + return common.PreErrorJSQ("You can't like something which doesn't exist!", w, r, isJs) } else if err != nil { - return common.InternalError(err, w, r) + return common.InternalErrorJSQ(err, w, r, isJs) } var fid int err = stmts.getTopicFID.QueryRow(reply.ParentID).Scan(&fid) if err == ErrNoRows { - return common.PreError("The parent topic doesn't exist.", w, r) + return common.PreErrorJSQ("The parent topic doesn't exist.", w, r, isJs) } else if err != nil { - return common.InternalError(err, w, r) + return common.InternalErrorJSQ(err, w, r, isJs) } // TODO: Add hooks to make use of headerLite @@ -91,32 +99,36 @@ func routeReplyLikeSubmit(w http.ResponseWriter, r *http.Request, user common.Us return ferr } if !user.Perms.ViewTopic || !user.Perms.LikeItem { - return common.NoPermissions(w, r, user) + return common.NoPermissionsJSQ(w, r, user, isJs) } if reply.CreatedBy == user.ID { - return common.LocalError("You can't like your own replies", w, r, user) + return common.LocalErrorJSQ("You can't like your own replies", w, r, user, isJs) } _, err = common.Users.Get(reply.CreatedBy) if err != nil && err != ErrNoRows { - return common.LocalError("The target user doesn't exist", w, r, user) + return common.LocalErrorJSQ("The target user doesn't exist", w, r, user, isJs) } else if err != nil { - return common.InternalError(err, w, r) + return common.InternalErrorJSQ(err, w, r, isJs) } err = reply.Like(user.ID) if err == common.ErrAlreadyLiked { - return common.LocalError("You've already liked this!", w, r, user) + return common.LocalErrorJSQ("You've already liked this!", w, r, user, isJs) } else if err != nil { - return common.InternalError(err, w, r) + return common.InternalErrorJSQ(err, w, r, isJs) } err = common.AddActivityAndNotifyTarget(user.ID, reply.CreatedBy, "like", "post", rid) if err != nil { - return common.InternalError(err, w, r) + return common.InternalErrorJSQ(err, w, r, isJs) } - http.Redirect(w, r, "/topic/"+strconv.Itoa(reply.ParentID), http.StatusSeeOther) + if !isJs { + http.Redirect(w, r, "/topic/"+strconv.Itoa(reply.ParentID), http.StatusSeeOther) + } else { + _, _ = w.Write(successJSONBytes) + } return nil } diff --git a/mssql.go b/mssql.go index 08e62100..82fc6ead 100644 --- a/mssql.go +++ b/mssql.go @@ -47,8 +47,6 @@ func initMSSQL() (err error) { return err } - // TODO: Fetch the database version - // Set the number of max open connections db.SetMaxOpenConns(64) db.SetMaxIdleConns(32) @@ -76,7 +74,7 @@ func initMSSQL() (err error) { // TODO: Is there a less noisy way of doing this for tests? log.Print("Preparing getActivityFeedByWatcher statement.") - getActivityFeedByWatcherStmt, err = db.Prepare("SELECT activity_stream_matches.asid, activity_stream.actor, activity_stream.targetUser, activity_stream.event, activity_stream.elementType, activity_stream.elementID FROM [activity_stream_matches] INNER JOIN [activity_stream] ON activity_stream_matches.asid = activity_stream.asid AND activity_stream_matches.watcher != activity_stream.actor WHERE [watcher] = ? ORDER BY activity_stream.asid ASC OFFSET 0 ROWS FETCH NEXT 8 ROWS ONLY") + getActivityFeedByWatcherStmt, err = db.Prepare("SELECT activity_stream_matches.asid, activity_stream.actor, activity_stream.targetUser, activity_stream.event, activity_stream.elementType, activity_stream.elementID FROM [activity_stream_matches] INNER JOIN [activity_stream] ON activity_stream_matches.asid = activity_stream.asid AND activity_stream_matches.watcher != activity_stream.actor WHERE [watcher] = ? ORDER BY activity_stream.asid DESC OFFSET 0 ROWS FETCH NEXT 8 ROWS ONLY") if err != nil { return err } diff --git a/mysql.go b/mysql.go index 68ad040e..96998b06 100644 --- a/mysql.go +++ b/mysql.go @@ -9,11 +9,8 @@ package main import ( - "database/sql" "log" - //import "time" - "./common" "./query_gen/lib" _ "github.com/go-sql-driver/mysql" @@ -27,28 +24,20 @@ func init() { } func initMySQL() (err error) { - var _dbpassword string - if common.DbConfig.Password != "" { - _dbpassword = ":" + common.DbConfig.Password - } - - // TODO: Move this bit to the query gen lib - // Open the database connection - db, err = sql.Open("mysql", common.DbConfig.Username+_dbpassword+"@tcp("+common.DbConfig.Host+":"+common.DbConfig.Port+")/"+common.DbConfig.Dbname+"?collation="+dbCollation+"&parseTime=true") + err = qgen.Builder.Init("mysql", map[string]string{ + "host": common.DbConfig.Host, + "port": common.DbConfig.Port, + "name": common.DbConfig.Dbname, + "username": common.DbConfig.Username, + "password": common.DbConfig.Password, + "collation": dbCollation, + }) if err != nil { return err } - // Make sure that the connection is alive - err = db.Ping() - if err != nil { - return err - } - - // Fetch the database version - db.QueryRow("SELECT VERSION()").Scan(&dbVersion) - // Set the number of max open connections + db = qgen.Builder.GetConn() db.SetMaxOpenConns(64) db.SetMaxIdleConns(32) @@ -70,7 +59,7 @@ func initMySQL() (err error) { // TODO: Is there a less noisy way of doing this for tests? log.Print("Preparing getActivityFeedByWatcher statement.") - stmts.getActivityFeedByWatcher, err = db.Prepare("SELECT activity_stream_matches.asid, activity_stream.actor, activity_stream.targetUser, activity_stream.event, activity_stream.elementType, activity_stream.elementID FROM `activity_stream_matches` INNER JOIN `activity_stream` ON activity_stream_matches.asid = activity_stream.asid AND activity_stream_matches.watcher != activity_stream.actor WHERE `watcher` = ? ORDER BY activity_stream.asid ASC LIMIT 8") + stmts.getActivityFeedByWatcher, err = db.Prepare("SELECT activity_stream_matches.asid, activity_stream.actor, activity_stream.targetUser, activity_stream.event, activity_stream.elementType, activity_stream.elementID FROM `activity_stream_matches` INNER JOIN `activity_stream` ON activity_stream_matches.asid = activity_stream.asid AND activity_stream_matches.watcher != activity_stream.actor WHERE `watcher` = ? ORDER BY activity_stream.asid DESC LIMIT 8") if err != nil { return err } diff --git a/patcher/main.go b/patcher/main.go new file mode 100644 index 00000000..91ade485 --- /dev/null +++ b/patcher/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "runtime/debug" + + "../query_gen/lib" +) + +func main() { + scanner := bufio.NewScanner(os.Stdin) + + // Capture panics instead of closing the window at a superhuman speed before the user can read the message on Windows + defer func() { + r := recover() + if r != nil { + fmt.Println(r) + debug.PrintStack() + pressAnyKey(scanner) + return + } + }() + + err := patcher(scanner) + if err != nil { + fmt.Println(err) + } +} + +func pressAnyKey(scanner *bufio.Scanner) { + fmt.Println("Please press enter to exit...") + for scanner.Scan() { + _ = scanner.Text() + return + } +} + +func patcher(scanner *bufio.Scanner) error { + return nil +} + +/*func eachUserQuick(handle func(int)) error { + stmt, err := qgen.Builder.Select("users").Orderby("uid desc").Limit(1).Prepare() + if err != nil { + return err + } + + var topID int + err := stmt.QueryRow(topID) + if err != nil { + return err + } + + for i := 1; i <= topID; i++ { + err = handle(i) + if err != nil { + return err + } + } +}*/ + +func eachUser(handle func(int)) error { + stmt, err := qgen.Builder.Select("users").Prepare() + if err != nil { + return err + } + + rows, err := stmt.Query() + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var uid int + err := rows.Scan(&uid) + if err != nil { + return err + } + err = handle(uid) + if err != nil { + return err + } + } + return rows.Err() +} diff --git a/pgsql.go b/pgsql.go index 803b32e1..f956869c 100644 --- a/pgsql.go +++ b/pgsql.go @@ -14,10 +14,10 @@ import ( ) // TODO: Add support for SSL for all database drivers, not just pgsql -var db_sslmode = "disable" // verify-full +var dbSslmode = "disable" // verify-full func init() { - db_adapter = "pgsql" + dbAdapter = "pgsql" _initDatabase = initPgsql } @@ -28,7 +28,7 @@ func initPgsql() (err error) { _dbpassword = " password='" + _escape_bit(common.DbConfig.Password) + "'" } // TODO: Move this bit to the query gen lib - db, err = sql.Open("postgres", "host='"+_escape_bit(common.DbConfig.Host)+"' port='"+_escape_bit(common.DbConfig.Port)+"' user='"+_escape_bit(common.DbConfig.Username)+"' dbname='"+_escape_bit(common.Config.Dbname)+"'"+_dbpassword+" sslmode='"+db_sslmode+"'") + db, err = sql.Open("postgres", "host='"+_escape_bit(common.DbConfig.Host)+"' port='"+_escape_bit(common.DbConfig.Port)+"' user='"+_escape_bit(common.DbConfig.Username)+"' dbname='"+_escape_bit(common.Config.Dbname)+"'"+_dbpassword+" sslmode='"+dbSslmode+"'") if err != nil { return err } @@ -39,9 +39,6 @@ func initPgsql() (err error) { return err } - // Fetch the database version - db.QueryRow("SELECT VERSION()").Scan(&db_version) - // Set the number of max open connections. How many do we need? Might need to do some tests. db.SetMaxOpenConns(64) db.SetMaxIdleConns(32) diff --git a/public/global.js b/public/global.js index e7c17953..130e6449 100644 --- a/public/global.js +++ b/public/global.js @@ -153,7 +153,7 @@ function runWebSockets() { console.log("The WebSockets connection was closed"); } conn.onmessage = function(event) { - //console.log("WS_Message:", event.data); + //console.log("WSMessage:", event.data); if(event.data[0] == "{") { try { var data = JSON.parse(event.data); @@ -200,14 +200,15 @@ function runWebSockets() { var messages = event.data.split('\r'); for(var i = 0; i < messages.length; i++) { - //console.log("Message: ",messages[i]); - if(messages[i].startsWith("set ")) { - //msgblocks = messages[i].split(' ',3); - let msgblocks = SplitN(messages[i]," ",3); + let message = messages[i]; + //console.log("Message: ",message); + if(message.startsWith("set ")) { + //msgblocks = message.split(' ',3); + let msgblocks = SplitN(message," ",3); if(msgblocks.length < 3) continue; document.querySelector(msgblocks[1]).innerHTML = msgblocks[2]; - } else if(messages[i].startsWith("set-class ")) { - let msgblocks = SplitN(messages[i]," ",3); + } else if(message.startsWith("set-class ")) { + let msgblocks = SplitN(message," ",3); if(msgblocks.length < 3) continue; document.querySelector(msgblocks[1]).className = msgblocks[2]; } @@ -220,8 +221,45 @@ $(document).ready(function(){ if(window["WebSocket"]) runWebSockets(); else conn = false; - $(".open_edit").click(function(event){ - //console.log("clicked on .open_edit"); + $(".add_like").click(function(event) { + event.preventDefault(); + let likeButton = this; + let target = this.closest("a").getAttribute("href"); + console.log("target: ", target); + likeButton.classList.remove("add_like"); + likeButton.classList.add("remove_like"); + let controls = likeButton.closest(".controls"); + let hadLikes = controls.classList.contains("has_likes"); + if(!hadLikes) controls.classList.add("has_likes"); + let likeCountNode = controls.getElementsByClassName("like_count")[0]; + console.log("likeCountNode",likeCountNode); + likeCountNode.innerHTML = parseInt(likeCountNode.innerHTML) + 1; + + $.ajax({ + url: target, + type: "POST", + dataType: "json", + data: { isJs: 1 }, + error: ajaxError, + success: function (data, status, xhr) { + if("success" in data) { + if(data["success"] == "1") { + return; + } + } + // addNotice("Failed to add a like: {err}") + likeButton.classList.add("add_like"); + likeButton.classList.remove("remove_like"); + if(!hadLikes) controls.classList.remove("has_likes"); + likeCountNode.innerHTML = parseInt(likeCountNode.innerHTML) - 1; + console.log("data", data); + console.log("status", status); + console.log("xhr", xhr); + } + }); + }); + + $(".open_edit").click((event) => { event.preventDefault(); $(".hide_on_edit").hide(); $(".show_on_edit").show(); @@ -229,17 +267,17 @@ $(document).ready(function(){ $(".topic_item .submit_edit").click(function(event){ event.preventDefault(); - //console.log("clicked on .topic_item .submit_edit"); - $(".topic_name").html($(".topic_name_input").val()); - $(".topic_content").html($(".topic_content_input").val()); - $(".topic_status_e:not(.open_edit)").html($(".topic_status_input").val()); + let topicNameInput = $(".topic_name_input").val(); + $(".topic_name").html(topicNameInput); + $(".topic_name").attr(topicNameInput); + let topicContentInput = $('.topic_content_input').val(); + $(".topic_content").html(topicContentInput.replace(/(\n)+/g,"
")); + let topicStatusInput = $('.topic_status_input').val(); + $(".topic_status_e:not(.open_edit)").html(topicStatusInput); $(".hide_on_edit").show(); $(".show_on_edit").hide(); - let topicNameInput = $('.topic_name_input').val(); - let topicStatusInput = $('.topic_status_input').val(); - let topicContentInput = $('.topic_content_input').val(); let formAction = this.form.getAttribute("action"); //console.log("New Topic Name: ", topicNameInput); //console.log("New Topic Status: ", topicStatusInput); @@ -284,8 +322,7 @@ $(document).ready(function(){ }); }); - $(".edit_field").click(function(event) - { + $(".edit_field").click(function(event) { event.preventDefault(); let blockParent = $(this).closest('.editable_parent'); let block = blockParent.find('.editable_block').eq(0); @@ -395,7 +432,7 @@ $(document).ready(function(){ } }); - $(this).click(function() { + $(this).click(() => { $(".selectedAlert").removeClass("selectedAlert"); $("#back").removeClass("alertActive"); }); @@ -421,9 +458,7 @@ $(document).ready(function(){ document.getElementById("back").className += " alertActive" }); - $("input,textarea,select,option").keyup(function(event){ - event.stopPropagation(); - }) + $("input,textarea,select,option").keyup(event => event.stopPropagation()) $(".create_topic_link").click((event) => { event.preventDefault(); diff --git a/query_gen/lib/acc_builders.go b/query_gen/lib/acc_builders.go index 12920e1f..467b2fe8 100644 --- a/query_gen/lib/acc_builders.go +++ b/query_gen/lib/acc_builders.go @@ -1,6 +1,9 @@ package qgen -import "database/sql" +import ( + "database/sql" + "strconv" +) type accDeleteBuilder struct { table string @@ -10,7 +13,10 @@ type accDeleteBuilder struct { } func (delete *accDeleteBuilder) Where(where string) *accDeleteBuilder { - delete.where = where + if delete.where != "" { + delete.where += " AND " + } + delete.where += where return delete } @@ -32,7 +38,10 @@ func (update *accUpdateBuilder) Set(set string) *accUpdateBuilder { } func (update *accUpdateBuilder) Where(where string) *accUpdateBuilder { - update.where = where + if update.where != "" { + update.where += " AND " + } + update.where += where return update } @@ -59,6 +68,28 @@ func (selectItem *accSelectBuilder) Columns(columns string) *accSelectBuilder { } func (selectItem *accSelectBuilder) Where(where string) *accSelectBuilder { + if selectItem.where != "" { + selectItem.where += " AND " + } + selectItem.where += where + return selectItem +} + +// TODO: Don't implement the SQL at the accumulator level but the adapter level +func (selectItem *accSelectBuilder) In(column string, inList []int) *accSelectBuilder { + if len(inList) == 0 { + return selectItem + } + + var where = column + " IN(" + for _, item := range inList { + where += strconv.Itoa(item) + "," + } + where = where[:len(where)-1] + ")" + if selectItem.where != "" { + where += " AND " + selectItem.where + } + selectItem.where = where return selectItem } @@ -140,7 +171,10 @@ type accCountBuilder struct { } func (count *accCountBuilder) Where(where string) *accCountBuilder { - count.where = where + if count.where != "" { + count.where += " AND " + } + count.where += where return count } diff --git a/query_gen/lib/builder.go b/query_gen/lib/builder.go index 19032de7..a1a93edc 100644 --- a/query_gen/lib/builder.go +++ b/query_gen/lib/builder.go @@ -19,10 +19,25 @@ func (build *builder) Accumulator() *Accumulator { return &Accumulator{build.conn, build.adapter, nil} } +// TODO: Move this method out of builder? +func (build *builder) Init(adapter string, config map[string]string) error { + err := build.SetAdapter(adapter) + if err != nil { + return err + } + conn, err := build.adapter.BuildConn(config) + build.conn = conn + return err +} + func (build *builder) SetConn(conn *sql.DB) { build.conn = conn } +func (build *builder) GetConn() *sql.DB { + return build.conn +} + func (build *builder) SetAdapter(name string) error { adap, err := GetAdapter(name) if err != nil { @@ -36,6 +51,11 @@ func (build *builder) GetAdapter() Adapter { return build.adapter } +func (build *builder) DbVersion() (dbVersion string) { + build.conn.QueryRow(build.adapter.DbVersion()).Scan(&dbVersion) + return dbVersion +} + func (build *builder) Begin() (*sql.Tx, error) { return build.conn.Begin() } diff --git a/query_gen/lib/micro_builders.go b/query_gen/lib/micro_builders.go index 50640253..7c35e1a8 100644 --- a/query_gen/lib/micro_builders.go +++ b/query_gen/lib/micro_builders.go @@ -44,7 +44,10 @@ func (delete *deletePrebuilder) Table(table string) *deletePrebuilder { } func (delete *deletePrebuilder) Where(where string) *deletePrebuilder { - delete.where = where + if delete.where != "" { + delete.where += " AND " + } + delete.where += where return delete } @@ -76,7 +79,10 @@ func (update *updatePrebuilder) Set(set string) *updatePrebuilder { } func (update *updatePrebuilder) Where(where string) *updatePrebuilder { - update.where = where + if update.where != "" { + update.where += " AND " + } + update.where += where return update } @@ -113,7 +119,10 @@ func (selectItem *selectPrebuilder) Columns(columns string) *selectPrebuilder { } func (selectItem *selectPrebuilder) Where(where string) *selectPrebuilder { - selectItem.where = where + if selectItem.where != "" { + selectItem.where += " AND " + } + selectItem.where += where return selectItem } @@ -205,7 +214,10 @@ func (count *countPrebuilder) Table(table string) *countPrebuilder { } func (count *countPrebuilder) Where(where string) *countPrebuilder { - count.where = where + if count.where != "" { + count.where += " AND " + } + count.where += where return count } diff --git a/query_gen/lib/mssql.go b/query_gen/lib/mssql.go index d9ff2b22..af09c147 100644 --- a/query_gen/lib/mssql.go +++ b/query_gen/lib/mssql.go @@ -2,6 +2,7 @@ package qgen import ( + "database/sql" "errors" "log" "strconv" @@ -34,6 +35,15 @@ func (adapter *MssqlAdapter) GetStmts() map[string]DBStmt { return adapter.Buffer } +// TODO: Implement this +func (adapter *MssqlAdapter) BuildConn(config map[string]string) (*sql.DB, error) { + return nil, nil +} + +func (adapter *MssqlAdapter) DbVersion() string { + return "SELECT CONCAT(SERVERPROPERTY('productversion'), SERVERPROPERTY ('productlevel'), SERVERPROPERTY ('edition'))" +} + // 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 (adapter *MssqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) { @@ -249,7 +259,7 @@ func (adapter *MssqlAdapter) SimpleUpsert(name string, table string, columns str switch token.Type { case "substitute": querystr += " ?" - case "function", "operator", "number": + case "function", "operator", "number", "or": // TODO: Split the function case off to speed things up if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { token.Contents = "GETUTCDATE()" @@ -314,7 +324,7 @@ func (adapter *MssqlAdapter) SimpleUpdate(name string, table string, set string, switch token.Type { case "substitute": querystr += " ?" - case "function", "operator", "number": + case "function", "operator", "number", "or": // TODO: Split the function case off to speed things up if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { token.Contents = "GETUTCDATE()" @@ -339,7 +349,7 @@ func (adapter *MssqlAdapter) SimpleUpdate(name string, table string, set string, for _, loc := range processWhere(where) { for _, token := range loc.Expr { switch token.Type { - case "function", "operator", "number", "substitute": + case "function", "operator", "number", "substitute", "or": // TODO: Split the function case off to speed things up if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { token.Contents = "GETUTCDATE()" @@ -381,7 +391,7 @@ func (adapter *MssqlAdapter) SimpleDelete(name string, table string, where strin switch token.Type { case "substitute": querystr += " ?" - case "function", "operator", "number": + case "function", "operator", "number", "or": // TODO: Split the function case off to speed things up if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { token.Contents = "GETUTCDATE()" @@ -451,7 +461,7 @@ func (adapter *MssqlAdapter) SimpleSelect(name string, table string, columns str case "substitute": substituteCount++ querystr += " ?" + strconv.Itoa(substituteCount) - case "function", "operator", "number": + case "function", "operator", "number", "or": // TODO: Split the function case off to speed things up // MSSQL seems to convert the formats? so we'll compare it with a regular date. Do this with the other methods too? if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { @@ -576,7 +586,7 @@ func (adapter *MssqlAdapter) SimpleLeftJoin(name string, table1 string, table2 s case "substitute": substituteCount++ querystr += " ?" + strconv.Itoa(substituteCount) - case "function", "operator", "number": + case "function", "operator", "number", "or": // TODO: Split the function case off to speed things up if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { token.Contents = "GETUTCDATE()" @@ -706,7 +716,7 @@ func (adapter *MssqlAdapter) SimpleInnerJoin(name string, table1 string, table2 case "substitute": substituteCount++ querystr += " ?" + strconv.Itoa(substituteCount) - case "function", "operator", "number": + case "function", "operator", "number", "or": // TODO: Split the function case off to speed things up if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { token.Contents = "GETUTCDATE()" @@ -827,7 +837,7 @@ func (adapter *MssqlAdapter) SimpleInsertSelect(name string, ins DBInsert, sel D case "substitute": substituteCount++ querystr += " ?" + strconv.Itoa(substituteCount) - case "function", "operator", "number": + case "function", "operator", "number", "or": // TODO: Split the function case off to speed things up if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { token.Contents = "GETUTCDATE()" @@ -951,7 +961,7 @@ func (adapter *MssqlAdapter) simpleJoin(name string, ins DBInsert, sel DBJoin, j case "substitute": substituteCount++ querystr += " ?" + strconv.Itoa(substituteCount) - case "function", "operator", "number": + case "function", "operator", "number", "or": // TODO: Split the function case off to speed things up if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { token.Contents = "GETUTCDATE()" @@ -1046,7 +1056,7 @@ func (adapter *MssqlAdapter) SimpleCount(name string, table string, where string for _, loc := range processWhere(where) { for _, token := range loc.Expr { switch token.Type { - case "function", "operator", "number", "substitute": + case "function", "operator", "number", "substitute", "or": if strings.ToUpper(token.Contents) == "UTC_TIMESTAMP()" { token.Contents = "GETUTCDATE()" } diff --git a/query_gen/lib/mysql.go b/query_gen/lib/mysql.go index e13bae8c..cc66f6b3 100644 --- a/query_gen/lib/mysql.go +++ b/query_gen/lib/mysql.go @@ -1,13 +1,17 @@ /* WIP Under Construction */ package qgen -//import "fmt" import ( + "database/sql" "errors" "strconv" "strings" + + _ "github.com/go-sql-driver/mysql" ) +var ErrNoCollation = errors.New("You didn't provide a collation") + func init() { Registry = append(Registry, &MysqlAdapter{Name: "mysql", Buffer: make(map[string]DBStmt)}, @@ -33,6 +37,30 @@ func (adapter *MysqlAdapter) GetStmts() map[string]DBStmt { return adapter.Buffer } +func (adapter *MysqlAdapter) BuildConn(config map[string]string) (*sql.DB, error) { + dbCollation, ok := config["collation"] + if !ok { + return nil, ErrNoCollation + } + var dbpassword string + if config["password"] != "" { + dbpassword = ":" + config["password"] + } + + // Open the database connection + db, err := sql.Open("mysql", config["username"]+dbpassword+"@tcp("+config["host"]+":"+config["port"]+")/"+config["name"]+"?collation="+dbCollation+"&parseTime=true") + if err != nil { + return db, err + } + + // Make sure that the connection is alive + return db, db.Ping() +} + +func (adapter *MysqlAdapter) DbVersion() string { + return "SELECT VERSION()" +} + func (adapter *MysqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) { if name == "" { return "", errors.New("You need a name for this statement") @@ -239,7 +267,7 @@ func (adapter *MysqlAdapter) SimpleUpdate(name string, table string, set string, querystr += "`" + item.Column + "` =" for _, token := range item.Expr { switch token.Type { - case "function", "operator", "number", "substitute": + case "function", "operator", "number", "substitute", "or": querystr += " " + token.Contents case "column": querystr += " `" + token.Contents + "`" @@ -278,7 +306,7 @@ func (adapter *MysqlAdapter) SimpleDelete(name string, table string, where strin for _, loc := range processWhere(where) { for _, token := range loc.Expr { switch token.Type { - case "function", "operator", "number", "substitute": + case "function", "operator", "number", "substitute", "or": querystr += " " + token.Contents case "column": querystr += " `" + token.Contents + "`" @@ -316,7 +344,7 @@ func (adapter *MysqlAdapter) buildWhere(where string) (querystr string, err erro for _, loc := range processWhere(where) { for _, token := range loc.Expr { switch token.Type { - case "function", "operator", "number", "substitute": + case "function", "operator", "number", "substitute", "or": querystr += " " + token.Contents case "column": querystr += " `" + token.Contents + "`" @@ -344,7 +372,7 @@ func (adapter *MysqlAdapter) buildFlexiWhere(where string, dateCutoff *dateCutof for _, loc := range processWhere(where) { for _, token := range loc.Expr { switch token.Type { - case "function", "operator", "number", "substitute": + case "function", "operator", "number", "substitute", "or": querystr += " " + token.Contents case "column": querystr += " `" + token.Contents + "`" @@ -544,7 +572,7 @@ func (adapter *MysqlAdapter) buildJoinWhere(where string) (querystr string, err for _, loc := range processWhere(where) { for _, token := range loc.Expr { switch token.Type { - case "function", "operator", "number", "substitute": + case "function", "operator", "number", "substitute", "or": querystr += " " + token.Contents case "column": halves := strings.Split(token.Contents, ".") diff --git a/query_gen/lib/pgsql.go b/query_gen/lib/pgsql.go index f5822425..edcae3cc 100644 --- a/query_gen/lib/pgsql.go +++ b/query_gen/lib/pgsql.go @@ -1,9 +1,12 @@ /* WIP Under Really Heavy Construction */ package qgen -import "strings" -import "strconv" -import "errors" +import ( + "database/sql" + "errors" + "strconv" + "strings" +) func init() { Registry = append(Registry, @@ -30,6 +33,16 @@ func (adapter *PgsqlAdapter) GetStmts() map[string]DBStmt { return adapter.Buffer } +// TODO: Implement this +func (adapter *PgsqlAdapter) BuildConn(config map[string]string) (*sql.DB, error) { + return nil, nil +} + +// TODO: Implement this +func (adapter *PgsqlAdapter) DbVersion() string { + return "" +} + // 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 (adapter *PgsqlAdapter) CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) { @@ -167,7 +180,7 @@ func (adapter *PgsqlAdapter) SimpleUpdate(name string, table string, set string, token.Contents = "LOCALTIMESTAMP()" } querystr += " " + token.Contents - case "operator", "number", "substitute": + case "operator", "number", "substitute", "or": querystr += " " + token.Contents case "column": querystr += " `" + token.Contents + "`" @@ -193,7 +206,7 @@ func (adapter *PgsqlAdapter) SimpleUpdate(name string, table string, set string, token.Contents = "LOCALTIMESTAMP()" } querystr += " " + token.Contents - case "operator", "number", "substitute": + case "operator", "number", "substitute", "or": querystr += " " + token.Contents case "column": querystr += " `" + token.Contents + "`" diff --git a/query_gen/lib/querygen.go b/query_gen/lib/querygen.go index 96853ddf..1808f311 100644 --- a/query_gen/lib/querygen.go +++ b/query_gen/lib/querygen.go @@ -98,6 +98,9 @@ type DBStmt struct { type Adapter interface { GetName() string + BuildConn(config map[string]string) (*sql.DB, error) + DbVersion() string + CreateTable(name string, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (string, error) SimpleInsert(name string, table string, columns string, fields string) (string, error) SimpleUpdate(name string, table string, set string, where string) (string, error) diff --git a/query_gen/lib/utils.go b/query_gen/lib/utils.go index 4e64664f..9c77aff9 100644 --- a/query_gen/lib/utils.go +++ b/query_gen/lib/utils.go @@ -166,7 +166,12 @@ func (where *DBWhere) parseOperator(segment string, i int) int { // TODO: Make this case insensitive func normalizeAnd(in string) string { - return strings.Replace(in, " and ", " AND ", -1) + in = strings.Replace(in, " and ", " AND ", -1) + return strings.Replace(in, " && ", " AND ", -1) +} +func normalizeOr(in string) string { + in = strings.Replace(in, " or ", " OR ", -1) + return strings.Replace(in, " || ", " OR ", -1) } // TODO: Write tests for this @@ -175,6 +180,7 @@ func processWhere(wherestr string) (where []DBWhere) { return where } wherestr = normalizeAnd(wherestr) + wherestr = normalizeOr(wherestr) for _, segment := range strings.Split(wherestr, " AND ") { var tmpWhere = &DBWhere{[]DBToken{}} @@ -184,10 +190,16 @@ func processWhere(wherestr string) (where []DBWhere) { switch { case '0' <= char && char <= '9': i = tmpWhere.parseNumber(segment, i) + // TODO: Sniff the third byte offset from char or it's non-existent to avoid matching uppercase strings which start with OR + case char == 'O' && (i+1) < len(segment) && segment[i+1] == 'R': + tmpWhere.Expr = append(tmpWhere.Expr, DBToken{"OR", "or"}) + i += 1 case ('a' <= char && char <= 'z') || ('A' <= char && char <= 'Z') || char == '_': i = tmpWhere.parseColumn(segment, i) case char == '\'': i = tmpWhere.parseString(segment, i) + case char == ')' && i < (len(segment)-1): + tmpWhere.Expr = append(tmpWhere.Expr, DBToken{")", "operator"}) case isOpByte(char): i = tmpWhere.parseOperator(segment, i) case char == '?': @@ -335,11 +347,11 @@ func processLimit(limitstr string) (limiter DBLimit) { } func isOpByte(char byte) bool { - return char == '<' || char == '>' || char == '=' || char == '!' || char == '*' || char == '%' || char == '+' || char == '-' || char == '/' + return char == '<' || char == '>' || char == '=' || char == '!' || char == '*' || char == '%' || char == '+' || char == '-' || char == '/' || char == '(' || char == ')' } func isOpRune(char rune) bool { - return char == '<' || char == '>' || char == '=' || char == '!' || char == '*' || char == '%' || char == '+' || char == '-' || char == '/' + return char == '<' || char == '>' || char == '=' || char == '!' || char == '*' || char == '%' || char == '+' || char == '-' || char == '/' || char == '(' || char == ')' } func processFields(fieldstr string) (fields []DBField) { diff --git a/query_gen/tables.go b/query_gen/tables.go index 03c56411..161b3cd6 100644 --- a/query_gen/tables.go +++ b/query_gen/tables.go @@ -1,4 +1,3 @@ -/* WIP Under Construction */ package main import "./lib" @@ -11,7 +10,7 @@ func createTables(adapter qgen.Adapter) error { qgen.DBTableColumn{"password", "varchar", 100, false, false, ""}, qgen.DBTableColumn{"salt", "varchar", 80, false, false, "''"}, - qgen.DBTableColumn{"group", "int", 0, false, false, ""}, + qgen.DBTableColumn{"group", "int", 0, false, false, ""}, // TODO: Make this a foreign key qgen.DBTableColumn{"active", "boolean", 0, false, false, "0"}, qgen.DBTableColumn{"is_super_admin", "boolean", 0, false, false, "0"}, qgen.DBTableColumn{"createdAt", "createdAt", 0, false, false, ""}, @@ -30,6 +29,12 @@ func createTables(adapter qgen.Adapter) error { qgen.DBTableColumn{"bigposts", "int", 0, false, false, "0"}, qgen.DBTableColumn{"megaposts", "int", 0, false, false, "0"}, qgen.DBTableColumn{"topics", "int", 0, false, false, "0"}, + qgen.DBTableColumn{"liked", "int", 0, false, false, "0"}, + + // These two are to bound liked queries with little bits of information we know about the user to reduce the server load + qgen.DBTableColumn{"oldestItemLikedCreatedAt", "datetime", 0, false, false, ""}, // For internal use only, semantics may change + qgen.DBTableColumn{"lastLiked", "datetime", 0, false, false, ""}, // For internal use only, semantics may change + //qgen.DBTableColumn{"penalty_count","int",0,false,false,"0"}, qgen.DBTableColumn{"temp_group", "int", 0, false, false, "0"}, // For temporary groups, set this to zero when a temporary group isn't in effect }, @@ -106,7 +111,7 @@ func createTables(adapter qgen.Adapter) error { qgen.Install.CreateTable("emails", "", "", []qgen.DBTableColumn{ qgen.DBTableColumn{"email", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, + qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key qgen.DBTableColumn{"validated", "boolean", 0, false, false, "0"}, qgen.DBTableColumn{"token", "varchar", 200, false, false, "''"}, }, @@ -153,7 +158,7 @@ func createTables(adapter qgen.Adapter) error { qgen.DBTableColumn{"createdAt", "createdAt", 0, false, false, ""}, qgen.DBTableColumn{"lastReplyAt", "datetime", 0, false, false, ""}, qgen.DBTableColumn{"lastReplyBy", "int", 0, false, false, ""}, - qgen.DBTableColumn{"createdBy", "int", 0, false, false, ""}, + qgen.DBTableColumn{"createdBy", "int", 0, false, false, ""}, // TODO: Make this a foreign key qgen.DBTableColumn{"is_closed", "boolean", 0, false, false, "0"}, qgen.DBTableColumn{"sticky", "boolean", 0, false, false, "0"}, qgen.DBTableColumn{"parentID", "int", 0, false, false, "2"}, @@ -178,7 +183,7 @@ func createTables(adapter qgen.Adapter) error { qgen.DBTableColumn{"content", "text", 0, false, false, ""}, qgen.DBTableColumn{"parsed_content", "text", 0, false, false, ""}, qgen.DBTableColumn{"createdAt", "createdAt", 0, false, false, ""}, - qgen.DBTableColumn{"createdBy", "int", 0, false, false, ""}, + qgen.DBTableColumn{"createdBy", "int", 0, false, false, ""}, // TODO: Make this a foreign key qgen.DBTableColumn{"lastEdit", "int", 0, false, false, "0"}, qgen.DBTableColumn{"lastEditBy", "int", 0, false, false, "0"}, qgen.DBTableColumn{"lastUpdated", "datetime", 0, false, false, ""}, @@ -200,7 +205,7 @@ func createTables(adapter qgen.Adapter) error { qgen.DBTableColumn{"sectionTable", "varchar", 200, false, false, "forums"}, qgen.DBTableColumn{"originID", "int", 0, false, false, ""}, qgen.DBTableColumn{"originTable", "varchar", 200, false, false, "replies"}, - qgen.DBTableColumn{"uploadedBy", "int", 0, false, false, ""}, + qgen.DBTableColumn{"uploadedBy", "int", 0, false, false, ""}, // TODO; Make this a foreign key qgen.DBTableColumn{"path", "varchar", 200, false, false, ""}, }, []qgen.DBTableKey{ @@ -215,6 +220,7 @@ func createTables(adapter qgen.Adapter) error { qgen.DBTableColumn{"contentID", "int", 0, false, false, ""}, qgen.DBTableColumn{"contentType", "varchar", 100, false, false, "replies"}, qgen.DBTableColumn{"createdAt", "createdAt", 0, false, false, ""}, + // TODO: Add a createdBy column? }, []qgen.DBTableKey{ qgen.DBTableKey{"reviseID", "primary"}, @@ -247,7 +253,7 @@ func createTables(adapter qgen.Adapter) error { qgen.Install.CreateTable("polls_votes", "utf8mb4", "utf8mb4_general_ci", []qgen.DBTableColumn{ qgen.DBTableColumn{"pollID", "int", 0, false, false, ""}, - qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, + qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key qgen.DBTableColumn{"option", "int", 0, false, false, "0"}, qgen.DBTableColumn{"castAt", "createdAt", 0, false, false, ""}, qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, @@ -258,11 +264,11 @@ func createTables(adapter qgen.Adapter) error { qgen.Install.CreateTable("users_replies", "utf8mb4", "utf8mb4_general_ci", []qgen.DBTableColumn{ qgen.DBTableColumn{"rid", "int", 0, false, true, ""}, - qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, + qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key qgen.DBTableColumn{"content", "text", 0, false, false, ""}, qgen.DBTableColumn{"parsed_content", "text", 0, false, false, ""}, qgen.DBTableColumn{"createdAt", "createdAt", 0, false, false, ""}, - qgen.DBTableColumn{"createdBy", "int", 0, false, false, ""}, + qgen.DBTableColumn{"createdBy", "int", 0, false, false, ""}, // TODO: Make this a foreign key qgen.DBTableColumn{"lastEdit", "int", 0, false, false, ""}, qgen.DBTableColumn{"lastEditBy", "int", 0, false, false, ""}, qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, @@ -277,7 +283,8 @@ func createTables(adapter qgen.Adapter) error { qgen.DBTableColumn{"weight", "tinyint", 0, false, false, "1"}, qgen.DBTableColumn{"targetItem", "int", 0, false, false, ""}, qgen.DBTableColumn{"targetType", "varchar", 50, false, false, "replies"}, - qgen.DBTableColumn{"sentBy", "int", 0, false, false, ""}, + qgen.DBTableColumn{"sentBy", "int", 0, false, false, ""}, // TODO: Make this a foreign key + qgen.DBTableColumn{"createdAt", "createdAt", 0, false, false, ""}, qgen.DBTableColumn{"recalc", "tinyint", 0, false, false, "0"}, }, []qgen.DBTableKey{}, @@ -285,8 +292,8 @@ func createTables(adapter qgen.Adapter) error { qgen.Install.CreateTable("activity_stream_matches", "", "", []qgen.DBTableColumn{ - qgen.DBTableColumn{"watcher", "int", 0, false, false, ""}, - qgen.DBTableColumn{"asid", "int", 0, false, false, ""}, + qgen.DBTableColumn{"watcher", "int", 0, false, false, ""}, // TODO: Make this a foreign key + qgen.DBTableColumn{"asid", "int", 0, false, false, ""}, // TODO: Make this a foreign key }, []qgen.DBTableKey{}, ) @@ -294,7 +301,7 @@ func createTables(adapter qgen.Adapter) error { qgen.Install.CreateTable("activity_stream", "", "", []qgen.DBTableColumn{ qgen.DBTableColumn{"asid", "int", 0, false, true, ""}, - qgen.DBTableColumn{"actor", "int", 0, false, false, ""}, /* the one doing the act */ + qgen.DBTableColumn{"actor", "int", 0, false, false, ""}, /* the one doing the act */ // TODO: Make this a foreign key qgen.DBTableColumn{"targetUser", "int", 0, false, false, ""}, /* the user who created the item the actor is acting on, some items like forums may lack a targetUser field */ qgen.DBTableColumn{"event", "varchar", 50, false, false, ""}, /* mention, like, reply (as in the act of replying to an item, not the reply item type, you can "reply" to a forum by making a topic in it), friend_invite */ qgen.DBTableColumn{"elementType", "varchar", 50, false, false, ""}, /* topic, post (calling it post here to differentiate it from the 'reply' event), forum, user */ @@ -307,7 +314,7 @@ func createTables(adapter qgen.Adapter) error { qgen.Install.CreateTable("activity_subscriptions", "", "", []qgen.DBTableColumn{ - qgen.DBTableColumn{"user", "int", 0, false, false, ""}, + qgen.DBTableColumn{"user", "int", 0, false, false, ""}, // TODO: Make this a foreign key qgen.DBTableColumn{"targetID", "int", 0, false, false, ""}, /* the ID of the element being acted upon */ qgen.DBTableColumn{"targetType", "varchar", 50, false, false, ""}, /* topic, post (calling it post here to differentiate it from the 'reply' event), forum, user */ qgen.DBTableColumn{"level", "int", 0, false, false, "0"}, /* 0: Mentions (aka the global default for any post), 1: Replies To You, 2: All Replies*/ @@ -378,7 +385,7 @@ func createTables(adapter qgen.Adapter) error { qgen.DBTableColumn{"elementID", "int", 0, false, false, ""}, qgen.DBTableColumn{"elementType", "varchar", 100, false, false, ""}, qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"actorID", "int", 0, false, false, ""}, + qgen.DBTableColumn{"actorID", "int", 0, false, false, ""}, // TODO: Make this a foreign key qgen.DBTableColumn{"doneAt", "datetime", 0, false, false, ""}, }, []qgen.DBTableKey{}, @@ -390,7 +397,7 @@ func createTables(adapter qgen.Adapter) error { qgen.DBTableColumn{"elementID", "int", 0, false, false, ""}, qgen.DBTableColumn{"elementType", "varchar", 100, false, false, ""}, qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"actorID", "int", 0, false, false, ""}, + qgen.DBTableColumn{"actorID", "int", 0, false, false, ""}, // TODO: Make this a foreign key qgen.DBTableColumn{"doneAt", "datetime", 0, false, false, ""}, }, []qgen.DBTableKey{}, diff --git a/router_gen/main.go b/router_gen/main.go index 90bdf001..49613ff1 100644 --- a/router_gen/main.go +++ b/router_gen/main.go @@ -302,6 +302,32 @@ func init() { counters.SetReverseOSMapEnum(reverseOSMapEnum) } +type WriterIntercept struct { + w http.ResponseWriter + code int +} + +func NewWriterIntercept(w http.ResponseWriter) *WriterIntercept { + return &WriterIntercept{w:w,code:200} +} + +func (writ *WriterIntercept) Header() http.Header { + return writ.w.Header() +} + +func (writ *WriterIntercept) Write(pieces []byte) (int, error) { + return writ.w.Write(pieces) +} + +func (writ *WriterIntercept) WriteHeader(code int) { + writ.w.WriteHeader(code) + writ.code = code +} + +func (writ *WriterIntercept) GetCode() int { + return writ.code +} + type GenRouter struct { UploadHandler func(http.ResponseWriter, *http.Request) extraRoutes map[string]func(http.ResponseWriter, *http.Request, common.User) common.RouteError @@ -311,7 +337,14 @@ type GenRouter struct { func NewGenRouter(uploads http.Handler) *GenRouter { return &GenRouter{ - UploadHandler: http.StripPrefix("/uploads/",uploads).ServeHTTP, + UploadHandler: func(w http.ResponseWriter, req *http.Request) { + writ := NewWriterIntercept(w) + http.StripPrefix("/uploads/",uploads).ServeHTTP(writ,req) + if writ.GetCode() == 200 { + w.Header().Set("Cache-Control", "max-age=" + strconv.Itoa(common.Day)) + w.Header().Set("Vary", "Accept-Encoding") + } + }, extraRoutes: make(map[string]func(http.ResponseWriter, *http.Request, common.User) common.RouteError), } } diff --git a/router_gen/routes.go b/router_gen/routes.go index f33021ff..c85061a7 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -74,7 +74,7 @@ func buildTopicRoutes() { Action("routes.LockTopicSubmit", "/topic/lock/submit/").LitBefore("req.URL.Path += extraData"), Action("routes.UnlockTopicSubmit", "/topic/unlock/submit/", "extraData"), Action("routes.MoveTopicSubmit", "/topic/move/submit/", "extraData"), - Action("routeLikeTopicSubmit", "/topic/like/submit/", "extraData"), + Action("routeLikeTopicSubmit", "/topic/like/submit/", "extraData").Before("ParseForm"), ) addRouteGroup(topicGroup) } @@ -88,7 +88,7 @@ func buildReplyRoutes() { UploadAction("routes.CreateReplySubmit", "/reply/create/").MaxSizeVar("common.Config.MaxRequestSize"), // TODO: Rename the route so it's /reply/create/submit/ Action("routes.ReplyEditSubmit", "/reply/edit/submit/", "extraData"), Action("routes.ReplyDeleteSubmit", "/reply/delete/submit/", "extraData"), - Action("routeReplyLikeSubmit", "/reply/like/submit/", "extraData"), + Action("routeReplyLikeSubmit", "/reply/like/submit/", "extraData").Before("ParseForm"), ) addRouteGroup(replyGroup) } diff --git a/routes/topic.go b/routes/topic.go index 767eb610..4a980a92 100644 --- a/routes/topic.go +++ b/routes/topic.go @@ -20,7 +20,8 @@ import ( ) type TopicStmts struct { - getReplies *sql.Stmt + getReplies *sql.Stmt + getLikedTopic *sql.Stmt } var topicStmts TopicStmts @@ -29,7 +30,8 @@ var topicStmts TopicStmts func init() { common.DbInits.Add(func(acc *qgen.Accumulator) error { topicStmts = TopicStmts{ - getReplies: acc.SimpleLeftJoin("replies", "users", "replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.avatar, users.name, users.group, users.url_prefix, users.url_name, users.level, replies.ipaddress, replies.likeCount, replies.actionType", "replies.createdBy = users.uid", "replies.tid = ?", "replies.rid ASC", "?,?"), + getReplies: acc.SimpleLeftJoin("replies", "users", "replies.rid, replies.content, replies.createdBy, replies.createdAt, replies.lastEdit, replies.lastEditBy, users.avatar, users.name, users.group, users.url_prefix, users.url_name, users.level, replies.ipaddress, replies.likeCount, replies.actionType", "replies.createdBy = users.uid", "replies.tid = ?", "replies.rid ASC", "?,?"), + getLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy = ? && targetItem = ? && targetType = 'topics'").Prepare(), } return acc.FirstError() }) @@ -107,12 +109,25 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit poll = pPoll.Copy() } + if topic.LikeCount > 0 && user.Liked > 0 { + var disp int // Discard this value + err = topicStmts.getLikedTopic.QueryRow(user.ID, topic.ID).Scan(&disp) + if err == nil { + topic.Liked = true + } else if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + } + // Calculate the offset offset, page, lastPage := common.PageOffset(topic.PostCount, page, common.Config.ItemsPerPage) tpage := common.TopicPage{topic.Title, user, headerVars, []common.ReplyUser{}, topic, poll, page, lastPage} // Get the replies if we have any... if topic.PostCount > 0 { + var likedMap = make(map[int]int) + var likedQueryList = []int{user.ID} + rows, err := topicStmts.getReplies.Query(topic.ID, offset, common.Config.ItemsPerPage) if err == sql.ErrNoRows { return common.LocalError("Bad Page. Some of the posts may have been deleted or you got here by directly typing in the page number.", w, r, user) @@ -171,7 +186,11 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit replyItem.ActionIcon = "" } } - replyItem.Liked = false + + if replyItem.LikeCount > 0 { + likedMap[replyItem.ID] = len(tpage.ItemList) + likedQueryList = append(likedQueryList, replyItem.ID) + } common.RunVhook("topic_reply_row_assign", &tpage, &replyItem) // TODO: Use a pointer instead to make it easier to abstract this loop? What impact would this have on escape analysis? @@ -181,6 +200,28 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, urlBit if err != nil { return common.InternalError(err, w, r) } + + // TODO: Add a config setting to disable the liked query for a burst of extra speed + if user.Liked > 0 && len(likedQueryList) > 1 /*&& user.LastLiked <= time.Now()*/ { + rows, err := qgen.Builder.Accumulator().Select("likes").Columns("targetItem").Where("sentBy = ? AND targetType = 'replies'").In("targetItem", likedQueryList[1:]).Query(user.ID) + if err != nil && err != sql.ErrNoRows { + return common.InternalError(err, w, r) + } + defer rows.Close() + + for rows.Next() { + var likeRid int + err := rows.Scan(&likeRid) + if err != nil { + return common.InternalError(err, w, r) + } + tpage.ItemList[likedMap[likeRid]].Liked = true + } + err = rows.Err() + if err != nil { + return common.InternalError(err, w, r) + } + } } if common.RunPreRenderHook("pre_render_view_topic", w, r, &user, &tpage) { diff --git a/run-linux b/run-linux index fa1e7cfd..6e4cc6e4 100644 --- a/run-linux +++ b/run-linux @@ -1,8 +1,14 @@ echo "Generating the dynamic code" go generate + echo "Building Gosora" go build -o Gosora + echo "Building the templates" ./Gosora -build-templates + +echo "Building Gosora... Again" +go build -o Gosora + echo "Running Gosora" ./Gosora \ No newline at end of file diff --git a/run-linux-nowebsockets b/run-linux-nowebsockets index 32f35fd1..08bd3a2f 100644 --- a/run-linux-nowebsockets +++ b/run-linux-nowebsockets @@ -1,8 +1,14 @@ echo "Generating the dynamic code" go generate + echo "Building Gosora" go build -o Gosora -tags no_ws + echo "Building the templates" ./Gosora -build-templates + +echo "Building Gosora... Again" +go build -o Gosora -tags no_ws + echo "Running Gosora" ./Gosora diff --git a/run-nowebsockets.bat b/run-nowebsockets.bat index 25ffe69f..5699bb4f 100644 --- a/run-nowebsockets.bat +++ b/run-nowebsockets.bat @@ -46,6 +46,13 @@ if %errorlevel% neq 0 ( exit /b %errorlevel% ) +echo Building the executable... again +go build -o gosora.exe -tags no_ws +if %errorlevel% neq 0 ( + pause + exit /b %errorlevel% +) + echo Running Gosora gosora.exe pause \ No newline at end of file diff --git a/run.bat b/run.bat index 0e14741a..6b3da06e 100644 --- a/run.bat +++ b/run.bat @@ -46,6 +46,13 @@ if %errorlevel% neq 0 ( exit /b %errorlevel% ) +echo Building the executable... again +go build -o gosora.exe +if %errorlevel% neq 0 ( + pause + exit /b %errorlevel% +) + echo Running Gosora gosora.exe rem Or you could redirect the output to a file diff --git a/run_mssql.bat b/run_mssql.bat index 6a703423..ff6bb4a3 100644 --- a/run_mssql.bat +++ b/run_mssql.bat @@ -46,6 +46,13 @@ if %errorlevel% neq 0 ( exit /b %errorlevel% ) +echo Building the executable... again +go build -o gosora.exe -tags mssql +if %errorlevel% neq 0 ( + pause + exit /b %errorlevel% +) + echo Running Gosora gosora.exe pause \ No newline at end of file diff --git a/schema/mssql/query_likes.sql b/schema/mssql/query_likes.sql index 9e2733c6..0fed857c 100644 --- a/schema/mssql/query_likes.sql +++ b/schema/mssql/query_likes.sql @@ -3,5 +3,6 @@ CREATE TABLE [likes] ( [targetItem] int not null, [targetType] nvarchar (50) DEFAULT 'replies' not null, [sentBy] int not null, + [createdAt] datetime not null, [recalc] tinyint DEFAULT 0 not null ); \ No newline at end of file diff --git a/schema/mssql/query_users.sql b/schema/mssql/query_users.sql index 0c8b2578..eef6531a 100644 --- a/schema/mssql/query_users.sql +++ b/schema/mssql/query_users.sql @@ -21,6 +21,9 @@ CREATE TABLE [users] ( [bigposts] int DEFAULT 0 not null, [megaposts] int DEFAULT 0 not null, [topics] int DEFAULT 0 not null, + [liked] int DEFAULT 0 not null, + [oldestItemLikedCreatedAt] datetime not null, + [lastLiked] datetime not null, [temp_group] int DEFAULT 0 not null, primary key([uid]), unique([name]) diff --git a/schema/mysql/query_likes.sql b/schema/mysql/query_likes.sql index b73b8cf0..aa070390 100644 --- a/schema/mysql/query_likes.sql +++ b/schema/mysql/query_likes.sql @@ -3,5 +3,6 @@ CREATE TABLE `likes` ( `targetItem` int not null, `targetType` varchar(50) DEFAULT 'replies' not null, `sentBy` int not null, + `createdAt` datetime not null, `recalc` tinyint DEFAULT 0 not null ); \ No newline at end of file diff --git a/schema/mysql/query_users.sql b/schema/mysql/query_users.sql index 676b167b..7425f95a 100644 --- a/schema/mysql/query_users.sql +++ b/schema/mysql/query_users.sql @@ -21,6 +21,9 @@ CREATE TABLE `users` ( `bigposts` int DEFAULT 0 not null, `megaposts` int DEFAULT 0 not null, `topics` int DEFAULT 0 not null, + `liked` int DEFAULT 0 not null, + `oldestItemLikedCreatedAt` datetime not null, + `lastLiked` datetime not null, `temp_group` int DEFAULT 0 not null, primary key(`uid`), unique(`name`) diff --git a/schema/pgsql/query_likes.sql b/schema/pgsql/query_likes.sql index cf55c434..142b396c 100644 --- a/schema/pgsql/query_likes.sql +++ b/schema/pgsql/query_likes.sql @@ -3,5 +3,6 @@ CREATE TABLE `likes` ( `targetItem` int not null, `targetType` varchar (50) DEFAULT 'replies' not null, `sentBy` int not null, + `createdAt` timestamp not null, `recalc` tinyint DEFAULT 0 not null ); \ No newline at end of file diff --git a/schema/pgsql/query_users.sql b/schema/pgsql/query_users.sql index 7b938e68..f57f8acc 100644 --- a/schema/pgsql/query_users.sql +++ b/schema/pgsql/query_users.sql @@ -21,6 +21,9 @@ CREATE TABLE `users` ( `bigposts` int DEFAULT 0 not null, `megaposts` int DEFAULT 0 not null, `topics` int DEFAULT 0 not null, + `liked` int DEFAULT 0 not null, + `oldestItemLikedCreatedAt` timestamp not null, + `lastLiked` timestamp not null, `temp_group` int DEFAULT 0 not null, primary key(`uid`), unique(`name`) diff --git a/schema/schema.json b/schema/schema.json index 68b3d0e2..be6b9ea2 100644 --- a/schema/schema.json +++ b/schema/schema.json @@ -1,3 +1,4 @@ { - "Version":"0" + "DBVersion":"0", + "DynamicFileVersion":"0" } \ No newline at end of file diff --git a/template_forum.go b/template_forum.go index 8f0dee22..261fcd01 100644 --- a/template_forum.go +++ b/template_forum.go @@ -3,9 +3,9 @@ // Code generated by Gosora. More below: /* This file was automatically generated by the software. Please don't edit it as your changes may be overwritten at any moment. */ package main -import "strconv" import "net/http" import "./common" +import "strconv" var forum_tmpl_phrase_id int @@ -307,61 +307,67 @@ w.Write([]byte(item.Link)) w.Write(forum_frags[58]) w.Write([]byte(item.Title)) w.Write(forum_frags[59]) -w.Write([]byte(item.Creator.Link)) +w.Write([]byte(item.Title)) w.Write(forum_frags[60]) -w.Write([]byte(item.Creator.Name)) +w.Write([]byte(item.Creator.Link)) w.Write(forum_frags[61]) -if item.IsClosed { +w.Write([]byte(item.Creator.Name)) w.Write(forum_frags[62]) -w.Write(phrases[44]) +w.Write([]byte(item.Creator.Name)) w.Write(forum_frags[63]) -} -if item.Sticky { +if item.IsClosed { w.Write(forum_frags[64]) -w.Write(phrases[45]) +w.Write(phrases[44]) w.Write(forum_frags[65]) } -w.Write(forum_frags[66]) -w.Write([]byte(strconv.Itoa(item.PostCount))) -w.Write(forum_frags[67]) -w.Write([]byte(strconv.Itoa(item.LikeCount))) -w.Write(forum_frags[68]) if item.Sticky { +w.Write(forum_frags[66]) +w.Write(phrases[45]) +w.Write(forum_frags[67]) +} +w.Write(forum_frags[68]) +w.Write([]byte(strconv.Itoa(item.PostCount))) w.Write(forum_frags[69]) +w.Write([]byte(strconv.Itoa(item.LikeCount))) +w.Write(forum_frags[70]) +if item.Sticky { +w.Write(forum_frags[71]) } else { if item.IsClosed { -w.Write(forum_frags[70]) -} -} -w.Write(forum_frags[71]) -w.Write([]byte(item.LastUser.Link)) w.Write(forum_frags[72]) -w.Write([]byte(item.LastUser.Avatar)) +} +} w.Write(forum_frags[73]) -w.Write([]byte(item.LastUser.Name)) -w.Write(forum_frags[74]) -w.Write([]byte(item.LastUser.Name)) -w.Write(forum_frags[75]) w.Write([]byte(item.LastUser.Link)) +w.Write(forum_frags[74]) +w.Write([]byte(item.LastUser.Avatar)) +w.Write(forum_frags[75]) +w.Write([]byte(item.LastUser.Name)) w.Write(forum_frags[76]) w.Write([]byte(item.LastUser.Name)) w.Write(forum_frags[77]) -w.Write([]byte(item.RelativeLastReplyAt)) +w.Write([]byte(item.LastUser.Link)) w.Write(forum_frags[78]) +w.Write([]byte(item.LastUser.Name)) +w.Write(forum_frags[79]) +w.Write([]byte(item.LastUser.Name)) +w.Write(forum_frags[80]) +w.Write([]byte(item.RelativeLastReplyAt)) +w.Write(forum_frags[81]) } } else { -w.Write(forum_frags[79]) +w.Write(forum_frags[82]) w.Write(phrases[46]) if tmpl_forum_vars.CurrentUser.Perms.CreateTopic { -w.Write(forum_frags[80]) -w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID))) -w.Write(forum_frags[81]) -w.Write(phrases[47]) -w.Write(forum_frags[82]) -} w.Write(forum_frags[83]) -} +w.Write([]byte(strconv.Itoa(tmpl_forum_vars.Forum.ID))) w.Write(forum_frags[84]) +w.Write(phrases[47]) +w.Write(forum_frags[85]) +} +w.Write(forum_frags[86]) +} +w.Write(forum_frags[87]) if tmpl_forum_vars.LastPage > 1 { w.Write(paginator_frags[0]) if tmpl_forum_vars.Page > 1 { @@ -397,7 +403,7 @@ w.Write(paginator_frags[13]) } w.Write(paginator_frags[14]) } -w.Write(forum_frags[85]) +w.Write(forum_frags[88]) w.Write(footer_frags[0]) w.Write([]byte(common.BuildWidget("footer",tmpl_forum_vars.Header))) w.Write(footer_frags[1]) diff --git a/template_list.go b/template_list.go index 677c4b6b..3eba4fb1 100644 --- a/template_list.go +++ b/template_list.go @@ -1,21 +1,21 @@ package main -var ip_search_frags = make([][]byte,18) -var header_frags = make([][]byte,24) -var paginator_frags = make([][]byte,16) var footer_frags = make([][]byte,13) -var register_frags = make([][]byte,9) -var forums_frags = make([][]byte,26) -var topics_frags = make([][]byte,94) -var forum_frags = make([][]byte,87) -var login_frags = make([][]byte,8) +var topic_alt_frags = make([][]byte,200) +var ip_search_frags = make([][]byte,18) var guilds_guild_list_frags = make([][]byte,10) -var topic_alt_frags = make([][]byte,194) +var topics_frags = make([][]byte,98) +var forum_frags = make([][]byte,90) +var header_frags = make([][]byte,24) +var topic_frags = make([][]byte,199) var profile_comments_row_frags = make([][]byte,51) -var profile_frags = make([][]byte,48) -var error_frags = make([][]byte,4) +var profile_frags = make([][]byte,50) +var forums_frags = make([][]byte,26) +var login_frags = make([][]byte,8) +var register_frags = make([][]byte,9) var menu_frags = make([][]byte,30) -var topic_frags = make([][]byte,192) +var paginator_frags = make([][]byte,16) +var error_frags = make([][]byte,4) // nolint func init() { @@ -157,255 +157,266 @@ topic_frags[17] = []byte(`
-
+
-

`) -topic_frags[22] = []byte(`

+topic_frags[20] = []byte(` topic_sticky_head`) +topic_frags[21] = []byte(` topic_closed_head`) +topic_frags[22] = []byte(`"> +

`) +topic_frags[24] = []byte(`

`) -topic_frags[23] = []byte(`🔒︎`) -topic_frags[26] = []byte(` +topic_frags[25] = []byte(`🔒︎`) +topic_frags[28] = []byte(` +topic_frags[29] = []byte(`' type="text" aria-label="`) +topic_frags[30] = []byte(`" /> +topic_frags[31] = []byte(` `) -topic_frags[30] = []byte(` +topic_frags[32] = []byte(`
`) -topic_frags[31] = []byte(` +topic_frags[33] = []byte(`
+topic_frags[34] = []byte(`">
+topic_frags[35] = []byte(`" style="background-image: url(`) +topic_frags[36] = []byte(`), url(/static/`) +topic_frags[37] = []byte(`/post-avatar-bg.jpg);background-position: 0px `) +topic_frags[38] = []byte(`-1`) +topic_frags[39] = []byte(`0px;background-repeat:no-repeat, repeat-y;">
`) -topic_frags[38] = []byte(` +topic_frags[40] = []byte(`
+topic_frags[41] = []byte(`_form" id="poll_option_`) +topic_frags[42] = []byte(`" name="poll_option_input" type="checkbox" value="`) +topic_frags[43] = []byte(`" /> `) -topic_frags[44] = []byte(` +topic_frags[45] = []byte(`" class="poll_option_text">`) +topic_frags[46] = []byte(`
`) -topic_frags[45] = []byte(` +topic_frags[47] = []byte(`
- + +topic_frags[52] = []byte(`
+topic_frags[53] = []byte(`" class="poll_results auto_hide">
`) -topic_frags[52] = []byte(` +topic_frags[54] = []byte(` -
+
+topic_frags[57] = []byte(`" style="background-image: url(`) +topic_frags[58] = []byte(`), url(/static/`) +topic_frags[59] = []byte(`/post-avatar-bg.jpg);background-position: 0px `) +topic_frags[60] = []byte(`-1`) +topic_frags[61] = []byte(`0px;background-repeat:no-repeat, repeat-y;">

`) -topic_frags[59] = []byte(`

+topic_frags[62] = []byte(`

+topic_frags[63] = []byte(` - +    +topic_frags[67] = []byte(`" class="username real_username" rel="author">`) +topic_frags[68] = []byte(`   `) -topic_frags[64] = []byte(` - `) -topic_frags[76] = []byte(``) -topic_frags[80] = []byte(``) -topic_frags[85] = []byte(``) -topic_frags[90] = []byte(``) -topic_frags[95] = []byte(``) -topic_frags[100] = []byte(``) -topic_frags[105] = []byte(``) -topic_frags[109] = []byte(` +topic_frags[69] = []byte(` + `) +topic_frags[82] = []byte(``) +topic_frags[86] = []byte(``) +topic_frags[91] = []byte(``) +topic_frags[96] = []byte(``) +topic_frags[101] = []byte(``) +topic_frags[106] = []byte(``) +topic_frags[111] = []byte(``) +topic_frags[115] = []byte(` +topic_frags[116] = []byte(`?session=`) +topic_frags[117] = []byte(`&type=topic" class="mod_button report_item" style="font-weight:normal;" title="`) +topic_frags[118] = []byte(`" aria-label="`) +topic_frags[119] = []byte(`" rel="nofollow"> + + `) -topic_frags[114] = []byte(``) -topic_frags[118] = []byte(``) -topic_frags[119] = []byte(``) -topic_frags[120] = []byte(``) -topic_frags[122] = []byte(``) -topic_frags[124] = []byte(` +topic_frags[123] = []byte(``) +topic_frags[124] = []byte(``) +topic_frags[125] = []byte(``) +topic_frags[127] = []byte(``) +topic_frags[129] = []byte(`
`) -topic_frags[126] = []byte(` +topic_frags[130] = []byte(`" style="overflow: hidden;">`) +topic_frags[131] = []byte(`
`) -topic_frags[127] = []byte(` +topic_frags[132] = []byte(` `) -topic_frags[128] = []byte(` +topic_frags[133] = []byte(`
`) -topic_frags[129] = []byte(` -
+topic_frags[134] = []byte(` +
`) -topic_frags[135] = []byte(` +topic_frags[141] = []byte(`

`) -topic_frags[136] = []byte(`

+topic_frags[142] = []byte(`

- +    +topic_frags[145] = []byte(`" class="username real_username" rel="author">`) +topic_frags[146] = []byte(`   `) -topic_frags[139] = []byte(``) -topic_frags[144] = []byte(``) -topic_frags[149] = []byte(``) -topic_frags[154] = []byte(``) -topic_frags[159] = []byte(``) -topic_frags[163] = []byte(` +topic_frags[147] = []byte(``) +topic_frags[152] = []byte(``) +topic_frags[157] = []byte(``) +topic_frags[162] = []byte(``) +topic_frags[167] = []byte(``) +topic_frags[171] = []byte(` +topic_frags[172] = []byte(`?session=`) +topic_frags[173] = []byte(`&type=reply" class="mod_button report_item" title="`) +topic_frags[174] = []byte(`" aria-label="`) +topic_frags[175] = []byte(`" rel="nofollow"> + + `) -topic_frags[168] = []byte(``) -topic_frags[171] = []byte(``) -topic_frags[172] = []byte(``) -topic_frags[173] = []byte(``) -topic_frags[175] = []byte(``) -topic_frags[177] = []byte(` +topic_frags[178] = []byte(``) +topic_frags[179] = []byte(``) +topic_frags[180] = []byte(``) +topic_frags[182] = []byte(``) +topic_frags[184] = []byte(`
`) -topic_frags[178] = []byte(`
+topic_frags[185] = []byte(`
`) -topic_frags[179] = []byte(` +topic_frags[186] = []byte(`
+topic_frags[187] = []byte(`">
+topic_frags[188] = []byte(`" method="post"> +topic_frags[189] = []byte(`' type="hidden" />
+topic_frags[190] = []byte(`" required>
@@ -414,28 +425,28 @@ topic_frags[183] = []byte(`" required> +topic_frags[191] = []byte(`" />
+topic_frags[192] = []byte(` +topic_frags[193] = []byte(` `) -topic_frags[187] = []byte(` +topic_frags[194] = []byte(` +topic_frags[195] = []byte(`
`) -topic_frags[189] = []byte(` +topic_frags[196] = []byte(`
`) -topic_frags[190] = []byte(` +topic_frags[197] = []byte(`
@@ -494,288 +505,294 @@ topic_alt_frags[14] = []byte(`
-
+
+topic_alt_frags[17] = []byte(`?session=`) +topic_alt_frags[18] = []byte(`' method="post">
-

`) -topic_alt_frags[21] = []byte(`

+topic_alt_frags[19] = []byte(` topic_sticky_head`) +topic_alt_frags[20] = []byte(` topic_closed_head`) +topic_alt_frags[21] = []byte(`"> +

`) +topic_alt_frags[23] = []byte(`

`) -topic_alt_frags[22] = []byte(`🔒︎`) -topic_alt_frags[25] = []byte(` +topic_alt_frags[24] = []byte(`🔒︎`) +topic_alt_frags[27] = []byte(` +topic_alt_frags[28] = []byte(`' type="text" aria-label="`) +topic_alt_frags[29] = []byte(`" /> +topic_alt_frags[30] = []byte(` `) -topic_alt_frags[29] = []byte(` +topic_alt_frags[31] = []byte(`
`) -topic_alt_frags[30] = []byte(` +topic_alt_frags[32] = []byte(`
+topic_alt_frags[33] = []byte(`_form" action="/poll/vote/`) +topic_alt_frags[34] = []byte(`?session=`) +topic_alt_frags[35] = []byte(`" method="post">
+topic_alt_frags[36] = []byte(`">
 
+topic_alt_frags[37] = []byte(`), url(/static/white-dot.jpg);background-position: 0px -10px;"> 
+topic_alt_frags[38] = []byte(`" class="the_name" rel="author">`) +topic_alt_frags[39] = []byte(` `) -topic_alt_frags[38] = []byte(`
`) -topic_alt_frags[40] = []byte(`
+topic_alt_frags[45] = []byte(`" class="content_container poll_voter">
`) -topic_alt_frags[44] = []byte(` +topic_alt_frags[46] = []byte(`
+topic_alt_frags[47] = []byte(`_form" id="poll_option_`) +topic_alt_frags[48] = []byte(`" name="poll_option_input" type="checkbox" value="`) +topic_alt_frags[49] = []byte(`" /> `) -topic_alt_frags[50] = []byte(` +topic_alt_frags[51] = []byte(`" class="poll_option_text">`) +topic_alt_frags[52] = []byte(`
`) -topic_alt_frags[51] = []byte(` +topic_alt_frags[53] = []byte(`
- + +topic_alt_frags[58] = []byte(`
+topic_alt_frags[59] = []byte(`" class="content_container poll_results auto_hide">
`) -topic_alt_frags[58] = []byte(` -
+topic_alt_frags[60] = []byte(` +
+topic_alt_frags[63] = []byte(`">
 
+topic_alt_frags[64] = []byte(`), url(/static/white-dot.jpg);background-position: 0px -10px;"> 
+topic_alt_frags[65] = []byte(`" class="the_name" rel="author">`) +topic_alt_frags[66] = []byte(` `) -topic_alt_frags[64] = []byte(`
`) -topic_alt_frags[66] = []byte(`
`) -topic_alt_frags[68] = []byte(` +topic_alt_frags[67] = []byte(`
`) +topic_alt_frags[69] = []byte(`
`) +topic_alt_frags[71] = []byte(`
`) -topic_alt_frags[69] = []byte(`
+topic_alt_frags[72] = []byte(`
-
+topic_alt_frags[73] = []byte(` +
`) -topic_alt_frags[71] = []byte(``) -topic_alt_frags[75] = []byte(``) -topic_alt_frags[78] = []byte(``) -topic_alt_frags[82] = []byte(``) -topic_alt_frags[86] = []byte(``) -topic_alt_frags[90] = []byte(``) -topic_alt_frags[94] = []byte(``) -topic_alt_frags[98] = []byte(``) -topic_alt_frags[102] = []byte(` - `) +topic_alt_frags[102] = []byte(``) +topic_alt_frags[106] = []byte(``) +topic_alt_frags[110] = []byte(` + `) -topic_alt_frags[106] = []byte(` -
- `) -topic_alt_frags[109] = []byte(``) -topic_alt_frags[112] = []byte(` +topic_alt_frags[114] = []byte(` +
+ `) -topic_alt_frags[113] = []byte(` +topic_alt_frags[117] = []byte(` `) -topic_alt_frags[114] = []byte(``) -topic_alt_frags[118] = []byte(` +topic_alt_frags[118] = []byte(``) +topic_alt_frags[122] = []byte(`
`) -topic_alt_frags[119] = []byte(` -
+topic_alt_frags[123] = []byte(` +
+topic_alt_frags[127] = []byte(`">
 
+topic_alt_frags[128] = []byte(`), url(/static/white-dot.jpg);background-position: 0px -10px;"> 
+topic_alt_frags[129] = []byte(`" class="the_name" rel="author">`) +topic_alt_frags[130] = []byte(` `) -topic_alt_frags[126] = []byte(`
`) -topic_alt_frags[128] = []byte(`
`) -topic_alt_frags[130] = []byte(` +topic_alt_frags[131] = []byte(`
`) +topic_alt_frags[133] = []byte(`
`) +topic_alt_frags[135] = []byte(`
+topic_alt_frags[136] = []byte(`style="margin-left: 0px;"`) +topic_alt_frags[137] = []byte(`> `) -topic_alt_frags[133] = []byte(` +topic_alt_frags[138] = []byte(` +topic_alt_frags[139] = []byte(` `) -topic_alt_frags[135] = []byte(` +topic_alt_frags[140] = []byte(` `) -topic_alt_frags[136] = []byte(` +topic_alt_frags[141] = []byte(`
`) -topic_alt_frags[137] = []byte(`
-
+topic_alt_frags[142] = []byte(`
+
`) -topic_alt_frags[138] = []byte(``) -topic_alt_frags[142] = []byte(``) -topic_alt_frags[146] = []byte(``) -topic_alt_frags[150] = []byte(``) -topic_alt_frags[154] = []byte(` +topic_alt_frags[145] = []byte(``) +topic_alt_frags[152] = []byte(``) +topic_alt_frags[156] = []byte(``) +topic_alt_frags[160] = []byte(``) +topic_alt_frags[164] = []byte(` +topic_alt_frags[165] = []byte(`?session=`) +topic_alt_frags[166] = []byte(`&type=reply" class="action_button report_item" aria-label="`) +topic_alt_frags[167] = []byte(`" data-action="report"> `) -topic_alt_frags[158] = []byte(` -
- `) -topic_alt_frags[161] = []byte(``) -topic_alt_frags[164] = []byte(` +topic_alt_frags[168] = []byte(` +
+ `) -topic_alt_frags[165] = []byte(` +topic_alt_frags[171] = []byte(` `) -topic_alt_frags[166] = []byte(``) -topic_alt_frags[169] = []byte(` +topic_alt_frags[172] = []byte(``) +topic_alt_frags[175] = []byte(`
`) -topic_alt_frags[170] = []byte(` +topic_alt_frags[176] = []byte(`
`) -topic_alt_frags[171] = []byte(`
+topic_alt_frags[177] = []byte(`
`) -topic_alt_frags[172] = []byte(` +topic_alt_frags[178] = []byte(`
+topic_alt_frags[179] = []byte(`">
 
+topic_alt_frags[180] = []byte(`), url(/static/white-dot.jpg);background-position: 0px -10px;"> 
+topic_alt_frags[181] = []byte(`" class="the_name" rel="author">`) +topic_alt_frags[182] = []byte(` `) -topic_alt_frags[177] = []byte(`
`) -topic_alt_frags[179] = []byte(`
`) -topic_alt_frags[181] = []byte(` +topic_alt_frags[183] = []byte(`
`) +topic_alt_frags[185] = []byte(`
`) +topic_alt_frags[187] = []byte(`
+topic_alt_frags[188] = []byte(`">
+topic_alt_frags[189] = []byte(`" method="post"> +topic_alt_frags[190] = []byte(`' type="hidden" />
+topic_alt_frags[191] = []byte(`" required>
@@ -784,29 +801,29 @@ topic_alt_frags[185] = []byte(`" required> +topic_alt_frags[192] = []byte(`" />
+topic_alt_frags[193] = []byte(` +topic_alt_frags[194] = []byte(` `) -topic_alt_frags[189] = []byte(` +topic_alt_frags[195] = []byte(` +topic_alt_frags[196] = []byte(`
`) -topic_alt_frags[191] = []byte(` +topic_alt_frags[197] = []byte(`
`) -topic_alt_frags[192] = []byte(` +topic_alt_frags[198] = []byte(`
@@ -825,104 +842,106 @@ profile_frags[2] = []byte(`'s Avatar" title="`) profile_frags[3] = []byte(`'s Avatar" />
- `) -profile_frags[4] = []byte(``) -profile_frags[5] = []byte(``) -profile_frags[6] = []byte(``) -profile_frags[7] = []byte(` + `) +profile_frags[5] = []byte(``) +profile_frags[6] = []byte(``) +profile_frags[8] = []byte(``) +profile_frags[9] = []byte(`
`) -profile_frags[8] = []byte(`
- `) -profile_frags[9] = []byte(` -
`) -profile_frags[10] = []byte(` - `) -profile_frags[12] = []byte(`
+profile_frags[14] = []byte(`
`) -profile_frags[13] = []byte(``) -profile_frags[16] = []byte(` +profile_frags[15] = []byte(``) +profile_frags[18] = []byte(` `) -profile_frags[17] = []byte(``) -profile_frags[18] = []byte(``) -profile_frags[19] = []byte(` +profile_frags[19] = []byte(``) +profile_frags[20] = []byte(``) +profile_frags[21] = []byte(`
`) -profile_frags[20] = []byte(` +profile_frags[22] = []byte(`
+profile_frags[23] = []byte(`?session=`) +profile_frags[24] = []byte(`&type=user" class="profile_menu_item report_item" aria-label="`) +profile_frags[25] = []byte(`" title="`) +profile_frags[26] = []byte(`">
`) -profile_frags[25] = []byte(` +profile_frags[27] = []byte(`
`) -profile_frags[26] = []byte(` +profile_frags[28] = []byte(`
`) -profile_frags[37] = []byte(` +profile_frags[39] = []byte(`

`) -profile_frags[38] = []byte(`

+profile_frags[40] = []byte(`
`) profile_comments_row_frags[0] = []byte(` @@ -1013,33 +1032,33 @@ profile_comments_row_frags[49] = []byte(`
`) -profile_frags[39] = []byte(` +profile_frags[41] = []byte(` `) -profile_frags[40] = []byte(` +profile_frags[42] = []byte(`
+profile_frags[43] = []byte(`" method="post"> +profile_frags[44] = []byte(`' type="hidden" />
+profile_frags[45] = []byte(`">
+profile_frags[46] = []byte(`
`) -profile_frags[45] = []byte(` +profile_frags[47] = []byte(` `) -profile_frags[46] = []byte(` +profile_frags[48] = []byte(`