diff --git a/.gitignore b/.gitignore index 9c39ffe1..d637105f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ out/* *.log .DS_Store .vscode/launch.json -schema/lastSchema.json config/config.go QueryGen RouterGen diff --git a/cmd/query_gen/tables.go b/cmd/query_gen/tables.go index 9d6b5f3c..6fc2908d 100644 --- a/cmd/query_gen/tables.go +++ b/cmd/query_gen/tables.go @@ -2,82 +2,88 @@ package main import "github.com/Azareal/Gosora/query_gen" -func createTables(adapter qgen.Adapter) error { - qgen.Install.CreateTable("users", "utf8mb4", "utf8mb4_general_ci", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"uid", "int", 0, false, true, ""}, - qgen.DBTableColumn{"name", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"password", "varchar", 100, false, false, ""}, +var mysqlPre = "utf8mb4" +var mysqlCol = "utf8mb4_general_ci" - qgen.DBTableColumn{"salt", "varchar", 80, 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, ""}, - qgen.DBTableColumn{"lastActiveAt", "datetime", 0, false, false, ""}, - qgen.DBTableColumn{"session", "varchar", 200, false, false, "''"}, - //qgen.DBTableColumn{"authToken", "varchar", 200, false, false, "''"}, - qgen.DBTableColumn{"last_ip", "varchar", 200, false, false, "0.0.0.0.0"}, - qgen.DBTableColumn{"email", "varchar", 200, false, false, "''"}, - qgen.DBTableColumn{"avatar", "varchar", 100, false, false, "''"}, - qgen.DBTableColumn{"message", "text", 0, false, false, "''"}, - qgen.DBTableColumn{"url_prefix", "varchar", 20, false, false, "''"}, - qgen.DBTableColumn{"url_name", "varchar", 100, false, false, "''"}, - qgen.DBTableColumn{"level", "smallint", 0, false, false, "0"}, - qgen.DBTableColumn{"score", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"posts", "int", 0, false, false, "0"}, - 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"}, +type tblColumn = qgen.DBTableColumn +type tblKey = qgen.DBTableKey + +func createTables(adapter qgen.Adapter) error { + qgen.Install.CreateTable("users", mysqlPre, mysqlCol, + []tblColumn{ + tblColumn{"uid", "int", 0, false, true, ""}, + tblColumn{"name", "varchar", 100, false, false, ""}, + tblColumn{"password", "varchar", 100, false, false, ""}, + + tblColumn{"salt", "varchar", 80, false, false, "''"}, + tblColumn{"group", "int", 0, false, false, ""}, // TODO: Make this a foreign key + tblColumn{"active", "boolean", 0, false, false, "0"}, + tblColumn{"is_super_admin", "boolean", 0, false, false, "0"}, + tblColumn{"createdAt", "createdAt", 0, false, false, ""}, + tblColumn{"lastActiveAt", "datetime", 0, false, false, ""}, + tblColumn{"session", "varchar", 200, false, false, "''"}, + //tblColumn{"authToken", "varchar", 200, false, false, "''"}, + tblColumn{"last_ip", "varchar", 200, false, false, "0.0.0.0.0"}, + tblColumn{"email", "varchar", 200, false, false, "''"}, + tblColumn{"avatar", "varchar", 100, false, false, "''"}, + tblColumn{"message", "text", 0, false, false, "''"}, + tblColumn{"url_prefix", "varchar", 20, false, false, "''"}, + tblColumn{"url_name", "varchar", 100, false, false, "''"}, + tblColumn{"level", "smallint", 0, false, false, "0"}, + tblColumn{"score", "int", 0, false, false, "0"}, + tblColumn{"posts", "int", 0, false, false, "0"}, + tblColumn{"bigposts", "int", 0, false, false, "0"}, + tblColumn{"megaposts", "int", 0, false, false, "0"}, + tblColumn{"topics", "int", 0, false, false, "0"}, + tblColumn{"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 + tblColumn{"oldestItemLikedCreatedAt", "datetime", 0, false, false, ""}, // For internal use only, semantics may change + tblColumn{"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 + //tblColumn{"penalty_count","int",0,false,false,"0"}, + tblColumn{"temp_group", "int", 0, false, false, "0"}, // For temporary groups, set this to zero when a temporary group isn't in effect }, - []qgen.DBTableKey{ - qgen.DBTableKey{"uid", "primary"}, - qgen.DBTableKey{"name", "unique"}, + []tblKey{ + tblKey{"uid", "primary"}, + tblKey{"name", "unique"}, }, ) - qgen.Install.CreateTable("users_groups", "utf8mb4", "utf8mb4_general_ci", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"gid", "int", 0, false, true, ""}, - qgen.DBTableColumn{"name", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"permissions", "text", 0, false, false, ""}, - qgen.DBTableColumn{"plugin_perms", "text", 0, false, false, ""}, - qgen.DBTableColumn{"is_mod", "boolean", 0, false, false, "0"}, - qgen.DBTableColumn{"is_admin", "boolean", 0, false, false, "0"}, - qgen.DBTableColumn{"is_banned", "boolean", 0, false, false, "0"}, - qgen.DBTableColumn{"user_count", "int", 0, false, false, "0"}, // TODO: Implement this + qgen.Install.CreateTable("users_groups", mysqlPre, mysqlCol, + []tblColumn{ + tblColumn{"gid", "int", 0, false, true, ""}, + tblColumn{"name", "varchar", 100, false, false, ""}, + tblColumn{"permissions", "text", 0, false, false, ""}, + tblColumn{"plugin_perms", "text", 0, false, false, ""}, + tblColumn{"is_mod", "boolean", 0, false, false, "0"}, + tblColumn{"is_admin", "boolean", 0, false, false, "0"}, + tblColumn{"is_banned", "boolean", 0, false, false, "0"}, + tblColumn{"user_count", "int", 0, false, false, "0"}, // TODO: Implement this - qgen.DBTableColumn{"tag", "varchar", 50, false, false, "''"}, + tblColumn{"tag", "varchar", 50, false, false, "''"}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"gid", "primary"}, + []tblKey{ + tblKey{"gid", "primary"}, }, ) - qgen.Install.CreateTable("users_2fa_keys", "utf8mb4", "utf8mb4_general_ci", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, - qgen.DBTableColumn{"secret", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"scratch1", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"scratch2", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"scratch3", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"scratch4", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"scratch5", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"scratch6", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"scratch7", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"scratch8", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"createdAt", "createdAt", 0, false, false, ""}, + qgen.Install.CreateTable("users_2fa_keys", mysqlPre, mysqlCol, + []tblColumn{ + tblColumn{"uid", "int", 0, false, false, ""}, + tblColumn{"secret", "varchar", 100, false, false, ""}, + tblColumn{"scratch1", "varchar", 50, false, false, ""}, + tblColumn{"scratch2", "varchar", 50, false, false, ""}, + tblColumn{"scratch3", "varchar", 50, false, false, ""}, + tblColumn{"scratch4", "varchar", 50, false, false, ""}, + tblColumn{"scratch5", "varchar", 50, false, false, ""}, + tblColumn{"scratch6", "varchar", 50, false, false, ""}, + tblColumn{"scratch7", "varchar", 50, false, false, ""}, + tblColumn{"scratch8", "varchar", 50, false, false, ""}, + tblColumn{"createdAt", "createdAt", 0, false, false, ""}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"uid", "primary"}, + []tblKey{ + tblKey{"uid", "primary"}, }, ) @@ -88,533 +94,534 @@ func createTables(adapter qgen.Adapter) error { // TODO: Add a penalty type where a user is stopped from creating plugin_guilds social groups // TODO: Shadow bans. We will probably have a CanShadowBan permission for this, as we *really* don't want people using this lightly. /*qgen.Install.CreateTable("users_penalties","","", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"uid","int",0,false,false,""}, - qgen.DBTableColumn{"element_id","int",0,false,false,""}, - qgen.DBTableColumn{"element_type","varchar",50,false,false,""}, //forum, profile?, and social_group. Leave blank for global. - qgen.DBTableColumn{"overrides","text",0,false,false,"{}"}, + []tblColumn{ + tblColumn{"uid","int",0,false,false,""}, + tblColumn{"element_id","int",0,false,false,""}, + tblColumn{"element_type","varchar",50,false,false,""}, //forum, profile?, and social_group. Leave blank for global. + tblColumn{"overrides","text",0,false,false,"{}"}, - qgen.DBTableColumn{"mod_queue","boolean",0,false,false,"0"}, - qgen.DBTableColumn{"shadow_ban","boolean",0,false,false,"0"}, - qgen.DBTableColumn{"no_avatar","boolean",0,false,false,"0"}, // Coming Soon. Should this be a perm override instead? + tblColumn{"mod_queue","boolean",0,false,false,"0"}, + tblColumn{"shadow_ban","boolean",0,false,false,"0"}, + tblColumn{"no_avatar","boolean",0,false,false,"0"}, // Coming Soon. Should this be a perm override instead? // Do we *really* need rate-limit penalty types? Are we going to be allowing bots or something? - //qgen.DBTableColumn{"posts_per_hour","int",0,false,false,"0"}, - //qgen.DBTableColumn{"topics_per_hour","int",0,false,false,"0"}, - //qgen.DBTableColumn{"posts_count","int",0,false,false,"0"}, - //qgen.DBTableColumn{"topic_count","int",0,false,false,"0"}, - //qgen.DBTableColumn{"last_hour","int",0,false,false,"0"}, // UNIX Time, as we don't need to do anything too fancy here. When an hour has elapsed since that time, reset the hourly penalty counters. + //tblColumn{"posts_per_hour","int",0,false,false,"0"}, + //tblColumn{"topics_per_hour","int",0,false,false,"0"}, + //tblColumn{"posts_count","int",0,false,false,"0"}, + //tblColumn{"topic_count","int",0,false,false,"0"}, + //tblColumn{"last_hour","int",0,false,false,"0"}, // UNIX Time, as we don't need to do anything too fancy here. When an hour has elapsed since that time, reset the hourly penalty counters. - qgen.DBTableColumn{"issued_by","int",0,false,false,""}, - qgen.DBTableColumn{"issued_at","createdAt",0,false,false,""}, - qgen.DBTableColumn{"expires_at","datetime",0,false,false,""}, + tblColumn{"issued_by","int",0,false,false,""}, + tblColumn{"issued_at","createdAt",0,false,false,""}, + tblColumn{"expires_at","datetime",0,false,false,""}, }, - []qgen.DBTableKey{}, + []tblKey{}, )*/ qgen.Install.CreateTable("users_groups_scheduler", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, - qgen.DBTableColumn{"set_group", "int", 0, false, false, ""}, + []tblColumn{ + tblColumn{"uid", "int", 0, false, false, ""}, + tblColumn{"set_group", "int", 0, false, false, ""}, - qgen.DBTableColumn{"issued_by", "int", 0, false, false, ""}, - qgen.DBTableColumn{"issued_at", "createdAt", 0, false, false, ""}, - qgen.DBTableColumn{"revert_at", "datetime", 0, false, false, ""}, - qgen.DBTableColumn{"temporary", "boolean", 0, false, false, ""}, // special case for permanent bans to do the necessary bookkeeping, might be removed in the future + tblColumn{"issued_by", "int", 0, false, false, ""}, + tblColumn{"issued_at", "createdAt", 0, false, false, ""}, + tblColumn{"revert_at", "datetime", 0, false, false, ""}, + tblColumn{"temporary", "boolean", 0, false, false, ""}, // special case for permanent bans to do the necessary bookkeeping, might be removed in the future }, - []qgen.DBTableKey{ - qgen.DBTableKey{"uid", "primary"}, + []tblKey{ + tblKey{"uid", "primary"}, }, ) // TODO: Can we use a piece of software dedicated to persistent queues for this rather than relying on the database for it? qgen.Install.CreateTable("users_avatar_queue", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key + []tblColumn{ + tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key }, - []qgen.DBTableKey{ - qgen.DBTableKey{"uid", "primary"}, + []tblKey{ + tblKey{"uid", "primary"}, }, ) // TODO: Should we add a users prefix to this table to fit the "unofficial convention"? qgen.Install.CreateTable("emails", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"email", "varchar", 200, 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, "''"}, + []tblColumn{ + tblColumn{"email", "varchar", 200, false, false, ""}, + tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key + tblColumn{"validated", "boolean", 0, false, false, "0"}, + tblColumn{"token", "varchar", 200, false, false, "''"}, }, - []qgen.DBTableKey{}, + []tblKey{}, ) // TODO: Allow for patterns in domains, if the bots try to shake things up there? /* qgen.Install.CreateTable("email_domain_blacklist", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"domain", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"gtld", "boolean", 0, false, false, "0"}, + []tblColumn{ + tblColumn{"domain", "varchar", 200, false, false, ""}, + tblColumn{"gtld", "boolean", 0, false, false, "0"}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"domain", "primary"}, + []tblKey{ + tblKey{"domain", "primary"}, }, ) */ - qgen.Install.CreateTable("forums", "utf8mb4", "utf8mb4_general_ci", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"fid", "int", 0, false, true, ""}, - qgen.DBTableColumn{"name", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"desc", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"active", "boolean", 0, false, false, "1"}, - qgen.DBTableColumn{"topicCount", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"preset", "varchar", 100, false, false, "''"}, - qgen.DBTableColumn{"parentID", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"parentType", "varchar", 50, false, false, "''"}, - qgen.DBTableColumn{"lastTopicID", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"lastReplyerID", "int", 0, false, false, "0"}, + qgen.Install.CreateTable("forums", mysqlPre, mysqlCol, + []tblColumn{ + tblColumn{"fid", "int", 0, false, true, ""}, + tblColumn{"name", "varchar", 100, false, false, ""}, + tblColumn{"desc", "varchar", 200, false, false, ""}, + tblColumn{"active", "boolean", 0, false, false, "1"}, + tblColumn{"topicCount", "int", 0, false, false, "0"}, + tblColumn{"preset", "varchar", 100, false, false, "''"}, + tblColumn{"parentID", "int", 0, false, false, "0"}, + tblColumn{"parentType", "varchar", 50, false, false, "''"}, + tblColumn{"lastTopicID", "int", 0, false, false, "0"}, + tblColumn{"lastReplyerID", "int", 0, false, false, "0"}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"fid", "primary"}, + []tblKey{ + tblKey{"fid", "primary"}, }, ) qgen.Install.CreateTable("forums_permissions", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"fid", "int", 0, false, false, ""}, - qgen.DBTableColumn{"gid", "int", 0, false, false, ""}, - qgen.DBTableColumn{"preset", "varchar", 100, false, false, "''"}, - qgen.DBTableColumn{"permissions", "text", 0, false, false, ""}, + []tblColumn{ + tblColumn{"fid", "int", 0, false, false, ""}, + tblColumn{"gid", "int", 0, false, false, ""}, + tblColumn{"preset", "varchar", 100, false, false, "''"}, + tblColumn{"permissions", "text", 0, false, false, ""}, }, - []qgen.DBTableKey{ + []tblKey{ // TODO: Test to see that the compound primary key works - qgen.DBTableKey{"fid,gid", "primary"}, + tblKey{"fid,gid", "primary"}, }, ) - qgen.Install.CreateTable("topics", "utf8mb4", "utf8mb4_general_ci", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"tid", "int", 0, false, true, ""}, - qgen.DBTableColumn{"title", "varchar", 100, false, false, ""}, // TODO: Increase the max length to 200? - qgen.DBTableColumn{"content", "text", 0, false, false, ""}, - qgen.DBTableColumn{"parsed_content", "text", 0, false, false, ""}, - 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, ""}, // TODO: Make this a foreign key - qgen.DBTableColumn{"is_closed", "boolean", 0, false, false, "0"}, - qgen.DBTableColumn{"sticky", "boolean", 0, false, false, "0"}, + qgen.Install.CreateTable("topics", mysqlPre, mysqlCol, + []tblColumn{ + tblColumn{"tid", "int", 0, false, true, ""}, + tblColumn{"title", "varchar", 100, false, false, ""}, // TODO: Increase the max length to 200? + tblColumn{"content", "text", 0, false, false, ""}, + tblColumn{"parsed_content", "text", 0, false, false, ""}, + tblColumn{"createdAt", "createdAt", 0, false, false, ""}, + tblColumn{"lastReplyAt", "datetime", 0, false, false, ""}, + tblColumn{"lastReplyBy", "int", 0, false, false, ""}, + tblColumn{"createdBy", "int", 0, false, false, ""}, // TODO: Make this a foreign key + tblColumn{"is_closed", "boolean", 0, false, false, "0"}, + tblColumn{"sticky", "boolean", 0, false, false, "0"}, // TODO: Add an index for this - qgen.DBTableColumn{"parentID", "int", 0, false, false, "2"}, - qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, - qgen.DBTableColumn{"postCount", "int", 0, false, false, "1"}, - qgen.DBTableColumn{"likeCount", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"words", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"views", "int", 0, false, false, "0"}, - //qgen.DBTableColumn{"dailyViews", "int", 0, false, false, "0"}, - //qgen.DBTableColumn{"weeklyViews", "int", 0, false, false, "0"}, - //qgen.DBTableColumn{"monthlyViews", "int", 0, false, false, "0"}, + tblColumn{"parentID", "int", 0, false, false, "2"}, + tblColumn{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, + tblColumn{"postCount", "int", 0, false, false, "1"}, + tblColumn{"likeCount", "int", 0, false, false, "0"}, + //tblColumn{"attachCount","int",0,false,false,"0"}, + tblColumn{"words", "int", 0, false, false, "0"}, + tblColumn{"views", "int", 0, false, false, "0"}, + //tblColumn{"dailyViews", "int", 0, false, false, "0"}, + //tblColumn{"weeklyViews", "int", 0, false, false, "0"}, + //tblColumn{"monthlyViews", "int", 0, false, false, "0"}, // ? - A little hacky, maybe we could do something less likely to bite us with huge numbers of topics? // TODO: Add an index for this? - //qgen.DBTableColumn{"lastMonth", "datetime", 0, false, false, ""}, - qgen.DBTableColumn{"css_class", "varchar", 100, false, false, "''"}, - qgen.DBTableColumn{"poll", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"data", "varchar", 200, false, false, "''"}, + //tblColumn{"lastMonth", "datetime", 0, false, false, ""}, + tblColumn{"css_class", "varchar", 100, false, false, "''"}, + tblColumn{"poll", "int", 0, false, false, "0"}, + tblColumn{"data", "varchar", 200, false, false, "''"}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"tid", "primary"}, + []tblKey{ + tblKey{"tid", "primary"}, }, ) - qgen.Install.CreateTable("replies", "utf8mb4", "utf8mb4_general_ci", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"rid", "int", 0, false, true, ""}, // TODO: Rename to replyID? - qgen.DBTableColumn{"tid", "int", 0, false, false, ""}, // TODO: Rename to topicID? - 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, ""}, // 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, ""}, - qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, - qgen.DBTableColumn{"likeCount", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"words", "int", 0, false, false, "1"}, // ? - replies has a default of 1 and topics has 0? why? - qgen.DBTableColumn{"actionType", "varchar", 20, false, false, "''"}, - qgen.DBTableColumn{"poll", "int", 0, false, false, "0"}, + qgen.Install.CreateTable("replies", mysqlPre, mysqlCol, + []tblColumn{ + tblColumn{"rid", "int", 0, false, true, ""}, // TODO: Rename to replyID? + tblColumn{"tid", "int", 0, false, false, ""}, // TODO: Rename to topicID? + tblColumn{"content", "text", 0, false, false, ""}, + tblColumn{"parsed_content", "text", 0, false, false, ""}, + tblColumn{"createdAt", "createdAt", 0, false, false, ""}, + tblColumn{"createdBy", "int", 0, false, false, ""}, // TODO: Make this a foreign key + tblColumn{"lastEdit", "int", 0, false, false, "0"}, + tblColumn{"lastEditBy", "int", 0, false, false, "0"}, + tblColumn{"lastUpdated", "datetime", 0, false, false, ""}, + tblColumn{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, + tblColumn{"likeCount", "int", 0, false, false, "0"}, + tblColumn{"words", "int", 0, false, false, "1"}, // ? - replies has a default of 1 and topics has 0? why? + tblColumn{"actionType", "varchar", 20, false, false, "''"}, + tblColumn{"poll", "int", 0, false, false, "0"}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"rid", "primary"}, + []tblKey{ + tblKey{"rid", "primary"}, }, ) - qgen.Install.CreateTable("attachments", "utf8mb4", "utf8mb4_general_ci", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"attachID", "int", 0, false, true, ""}, - qgen.DBTableColumn{"sectionID", "int", 0, false, false, "0"}, - 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, ""}, // TODO; Make this a foreign key - qgen.DBTableColumn{"path", "varchar", 200, false, false, ""}, + qgen.Install.CreateTable("attachments", mysqlPre, mysqlCol, + []tblColumn{ + tblColumn{"attachID", "int", 0, false, true, ""}, + tblColumn{"sectionID", "int", 0, false, false, "0"}, + tblColumn{"sectionTable", "varchar", 200, false, false, "forums"}, + tblColumn{"originID", "int", 0, false, false, ""}, + tblColumn{"originTable", "varchar", 200, false, false, "replies"}, + tblColumn{"uploadedBy", "int", 0, false, false, ""}, // TODO; Make this a foreign key + tblColumn{"path", "varchar", 200, false, false, ""}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"attachID", "primary"}, + []tblKey{ + tblKey{"attachID", "primary"}, }, ) - qgen.Install.CreateTable("revisions", "utf8mb4", "utf8mb4_general_ci", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"reviseID", "int", 0, false, true, ""}, - qgen.DBTableColumn{"content", "text", 0, false, false, ""}, - qgen.DBTableColumn{"contentID", "int", 0, false, false, ""}, - qgen.DBTableColumn{"contentType", "varchar", 100, false, false, "replies"}, - qgen.DBTableColumn{"createdAt", "createdAt", 0, false, false, ""}, + qgen.Install.CreateTable("revisions", mysqlPre, mysqlCol, + []tblColumn{ + tblColumn{"reviseID", "int", 0, false, true, ""}, + tblColumn{"content", "text", 0, false, false, ""}, + tblColumn{"contentID", "int", 0, false, false, ""}, + tblColumn{"contentType", "varchar", 100, false, false, "replies"}, + tblColumn{"createdAt", "createdAt", 0, false, false, ""}, // TODO: Add a createdBy column? }, - []qgen.DBTableKey{ - qgen.DBTableKey{"reviseID", "primary"}, + []tblKey{ + tblKey{"reviseID", "primary"}, }, ) - qgen.Install.CreateTable("polls", "utf8mb4", "utf8mb4_general_ci", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"pollID", "int", 0, false, true, ""}, - qgen.DBTableColumn{"parentID", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"parentTable", "varchar", 100, false, false, "topics"}, // topics, replies - qgen.DBTableColumn{"type", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"options", "json", 0, false, false, ""}, - qgen.DBTableColumn{"votes", "int", 0, false, false, "0"}, + qgen.Install.CreateTable("polls", mysqlPre, mysqlCol, + []tblColumn{ + tblColumn{"pollID", "int", 0, false, true, ""}, + tblColumn{"parentID", "int", 0, false, false, "0"}, + tblColumn{"parentTable", "varchar", 100, false, false, "topics"}, // topics, replies + tblColumn{"type", "int", 0, false, false, "0"}, + tblColumn{"options", "json", 0, false, false, ""}, + tblColumn{"votes", "int", 0, false, false, "0"}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"pollID", "primary"}, + []tblKey{ + tblKey{"pollID", "primary"}, }, ) qgen.Install.CreateTable("polls_options", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"pollID", "int", 0, false, false, ""}, - qgen.DBTableColumn{"option", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"votes", "int", 0, false, false, "0"}, + []tblColumn{ + tblColumn{"pollID", "int", 0, false, false, ""}, + tblColumn{"option", "int", 0, false, false, "0"}, + tblColumn{"votes", "int", 0, false, false, "0"}, }, - []qgen.DBTableKey{}, + []tblKey{}, ) - 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, ""}, // 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"}, + qgen.Install.CreateTable("polls_votes", mysqlPre, mysqlCol, + []tblColumn{ + tblColumn{"pollID", "int", 0, false, false, ""}, + tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key + tblColumn{"option", "int", 0, false, false, "0"}, + tblColumn{"castAt", "createdAt", 0, false, false, ""}, + tblColumn{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, }, - []qgen.DBTableKey{}, + []tblKey{}, ) - 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, ""}, // 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, ""}, // TODO: Make this a foreign key - qgen.DBTableColumn{"lastEdit", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"lastEditBy", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, + qgen.Install.CreateTable("users_replies", mysqlPre, mysqlCol, + []tblColumn{ + tblColumn{"rid", "int", 0, false, true, ""}, + tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key + tblColumn{"content", "text", 0, false, false, ""}, + tblColumn{"parsed_content", "text", 0, false, false, ""}, + tblColumn{"createdAt", "createdAt", 0, false, false, ""}, + tblColumn{"createdBy", "int", 0, false, false, ""}, // TODO: Make this a foreign key + tblColumn{"lastEdit", "int", 0, false, false, "0"}, + tblColumn{"lastEditBy", "int", 0, false, false, "0"}, + tblColumn{"ipaddress", "varchar", 200, false, false, "0.0.0.0.0"}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"rid", "primary"}, + []tblKey{ + tblKey{"rid", "primary"}, }, ) qgen.Install.CreateTable("likes", "", "", - []qgen.DBTableColumn{ - 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, ""}, // TODO: Make this a foreign key - qgen.DBTableColumn{"createdAt", "createdAt", 0, false, false, ""}, - qgen.DBTableColumn{"recalc", "tinyint", 0, false, false, "0"}, + []tblColumn{ + tblColumn{"weight", "tinyint", 0, false, false, "1"}, + tblColumn{"targetItem", "int", 0, false, false, ""}, + tblColumn{"targetType", "varchar", 50, false, false, "replies"}, + tblColumn{"sentBy", "int", 0, false, false, ""}, // TODO: Make this a foreign key + tblColumn{"createdAt", "createdAt", 0, false, false, ""}, + tblColumn{"recalc", "tinyint", 0, false, false, "0"}, }, - []qgen.DBTableKey{}, + []tblKey{}, ) qgen.Install.CreateTable("activity_stream_matches", "", "", - []qgen.DBTableColumn{ - 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 + []tblColumn{ + tblColumn{"watcher", "int", 0, false, false, ""}, // TODO: Make this a foreign key + tblColumn{"asid", "int", 0, false, false, ""}, // TODO: Make this a foreign key }, - []qgen.DBTableKey{}, + []tblKey{}, ) 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 */ // 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 */ - qgen.DBTableColumn{"elementID", "int", 0, false, false, ""}, /* the ID of the element being acted upon */ + []tblColumn{ + tblColumn{"asid", "int", 0, false, true, ""}, + tblColumn{"actor", "int", 0, false, false, ""}, /* the one doing the act */ // TODO: Make this a foreign key + tblColumn{"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 */ + tblColumn{"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 */ + tblColumn{"elementType", "varchar", 50, false, false, ""}, /* topic, post (calling it post here to differentiate it from the 'reply' event), forum, user */ + tblColumn{"elementID", "int", 0, false, false, ""}, /* the ID of the element being acted upon */ }, - []qgen.DBTableKey{ - qgen.DBTableKey{"asid", "primary"}, + []tblKey{ + tblKey{"asid", "primary"}, }, ) qgen.Install.CreateTable("activity_subscriptions", "", "", - []qgen.DBTableColumn{ - 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*/ + []tblColumn{ + tblColumn{"user", "int", 0, false, false, ""}, // TODO: Make this a foreign key + tblColumn{"targetID", "int", 0, false, false, ""}, /* the ID of the element being acted upon */ + tblColumn{"targetType", "varchar", 50, false, false, ""}, /* topic, post (calling it post here to differentiate it from the 'reply' event), forum, user */ + tblColumn{"level", "int", 0, false, false, "0"}, /* 0: Mentions (aka the global default for any post), 1: Replies To You, 2: All Replies*/ }, - []qgen.DBTableKey{}, + []tblKey{}, ) /* Due to MySQL's design, we have to drop the unique keys for table settings, plugins, and themes down from 200 to 180 or it will error */ qgen.Install.CreateTable("settings", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"name", "varchar", 180, false, false, ""}, - qgen.DBTableColumn{"content", "varchar", 250, false, false, ""}, - qgen.DBTableColumn{"type", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"constraints", "varchar", 200, false, false, "''"}, + []tblColumn{ + tblColumn{"name", "varchar", 180, false, false, ""}, + tblColumn{"content", "varchar", 250, false, false, ""}, + tblColumn{"type", "varchar", 50, false, false, ""}, + tblColumn{"constraints", "varchar", 200, false, false, "''"}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"name", "unique"}, + []tblKey{ + tblKey{"name", "unique"}, }, ) qgen.Install.CreateTable("word_filters", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"wfid", "int", 0, false, true, ""}, - qgen.DBTableColumn{"find", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"replacement", "varchar", 200, false, false, ""}, + []tblColumn{ + tblColumn{"wfid", "int", 0, false, true, ""}, + tblColumn{"find", "varchar", 200, false, false, ""}, + tblColumn{"replacement", "varchar", 200, false, false, ""}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"wfid", "primary"}, + []tblKey{ + tblKey{"wfid", "primary"}, }, ) qgen.Install.CreateTable("plugins", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"uname", "varchar", 180, false, false, ""}, - qgen.DBTableColumn{"active", "boolean", 0, false, false, "0"}, - qgen.DBTableColumn{"installed", "boolean", 0, false, false, "0"}, + []tblColumn{ + tblColumn{"uname", "varchar", 180, false, false, ""}, + tblColumn{"active", "boolean", 0, false, false, "0"}, + tblColumn{"installed", "boolean", 0, false, false, "0"}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"uname", "unique"}, + []tblKey{ + tblKey{"uname", "unique"}, }, ) qgen.Install.CreateTable("themes", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"uname", "varchar", 180, false, false, ""}, - qgen.DBTableColumn{"default", "boolean", 0, false, false, "0"}, + []tblColumn{ + tblColumn{"uname", "varchar", 180, false, false, ""}, + tblColumn{"default", "boolean", 0, false, false, "0"}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"uname", "unique"}, + []tblKey{ + tblKey{"uname", "unique"}, }, ) qgen.Install.CreateTable("widgets", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"position", "int", 0, false, false, ""}, - qgen.DBTableColumn{"side", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"type", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"active", "boolean", 0, false, false, "0"}, - qgen.DBTableColumn{"location", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"data", "text", 0, false, false, "''"}, + []tblColumn{ + tblColumn{"position", "int", 0, false, false, ""}, + tblColumn{"side", "varchar", 100, false, false, ""}, + tblColumn{"type", "varchar", 100, false, false, ""}, + tblColumn{"active", "boolean", 0, false, false, "0"}, + tblColumn{"location", "varchar", 100, false, false, ""}, + tblColumn{"data", "text", 0, false, false, "''"}, }, - []qgen.DBTableKey{}, + []tblKey{}, ) qgen.Install.CreateTable("menus", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"mid", "int", 0, false, true, ""}, + []tblColumn{ + tblColumn{"mid", "int", 0, false, true, ""}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"mid", "primary"}, + []tblKey{ + tblKey{"mid", "primary"}, }, ) qgen.Install.CreateTable("menu_items", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"miid", "int", 0, false, true, ""}, - qgen.DBTableColumn{"mid", "int", 0, false, false, ""}, - qgen.DBTableColumn{"name", "varchar", 200, false, false, "''"}, - qgen.DBTableColumn{"htmlID", "varchar", 200, false, false, "''"}, - qgen.DBTableColumn{"cssClass", "varchar", 200, false, false, "''"}, - qgen.DBTableColumn{"position", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"path", "varchar", 200, false, false, "''"}, - qgen.DBTableColumn{"aria", "varchar", 200, false, false, "''"}, - qgen.DBTableColumn{"tooltip", "varchar", 200, false, false, "''"}, - qgen.DBTableColumn{"tmplName", "varchar", 200, false, false, "''"}, - qgen.DBTableColumn{"order", "int", 0, false, false, "0"}, + []tblColumn{ + tblColumn{"miid", "int", 0, false, true, ""}, + tblColumn{"mid", "int", 0, false, false, ""}, + tblColumn{"name", "varchar", 200, false, false, "''"}, + tblColumn{"htmlID", "varchar", 200, false, false, "''"}, + tblColumn{"cssClass", "varchar", 200, false, false, "''"}, + tblColumn{"position", "varchar", 100, false, false, ""}, + tblColumn{"path", "varchar", 200, false, false, "''"}, + tblColumn{"aria", "varchar", 200, false, false, "''"}, + tblColumn{"tooltip", "varchar", 200, false, false, "''"}, + tblColumn{"tmplName", "varchar", 200, false, false, "''"}, + tblColumn{"order", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"guestOnly", "boolean", 0, false, false, "0"}, - qgen.DBTableColumn{"memberOnly", "boolean", 0, false, false, "0"}, - qgen.DBTableColumn{"staffOnly", "boolean", 0, false, false, "0"}, - qgen.DBTableColumn{"adminOnly", "boolean", 0, false, false, "0"}, + tblColumn{"guestOnly", "boolean", 0, false, false, "0"}, + tblColumn{"memberOnly", "boolean", 0, false, false, "0"}, + tblColumn{"staffOnly", "boolean", 0, false, false, "0"}, + tblColumn{"adminOnly", "boolean", 0, false, false, "0"}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"miid", "primary"}, + []tblKey{ + tblKey{"miid", "primary"}, }, ) - qgen.Install.CreateTable("pages", "utf8mb4", "utf8mb4_general_ci", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"pid", "int", 0, false, true, ""}, - //qgen.DBTableColumn{"path", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"name", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"title", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"body", "text", 0, false, false, ""}, + qgen.Install.CreateTable("pages", mysqlPre, mysqlCol, + []tblColumn{ + tblColumn{"pid", "int", 0, false, true, ""}, + //tblColumn{"path", "varchar", 200, false, false, ""}, + tblColumn{"name", "varchar", 200, false, false, ""}, + tblColumn{"title", "varchar", 200, false, false, ""}, + tblColumn{"body", "text", 0, false, false, ""}, // TODO: Make this a table? - qgen.DBTableColumn{"allowedGroups", "text", 0, false, false, ""}, - qgen.DBTableColumn{"menuID", "int", 0, false, false, "-1"}, // simple sidebar menu + tblColumn{"allowedGroups", "text", 0, false, false, ""}, + tblColumn{"menuID", "int", 0, false, false, "-1"}, // simple sidebar menu }, - []qgen.DBTableKey{ - qgen.DBTableKey{"pid", "primary"}, + []tblKey{ + tblKey{"pid", "primary"}, }, ) qgen.Install.CreateTable("registration_logs", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"rlid", "int", 0, false, true, ""}, - qgen.DBTableColumn{"username", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"email", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"failureReason", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed? - qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"doneAt", "createdAt", 0, false, false, ""}, + []tblColumn{ + tblColumn{"rlid", "int", 0, false, true, ""}, + tblColumn{"username", "varchar", 100, false, false, ""}, + tblColumn{"email", "varchar", 100, false, false, ""}, + tblColumn{"failureReason", "varchar", 100, false, false, ""}, + tblColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed? + tblColumn{"ipaddress", "varchar", 200, false, false, ""}, + tblColumn{"doneAt", "createdAt", 0, false, false, ""}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"rlid", "primary"}, + []tblKey{ + tblKey{"rlid", "primary"}, }, ) qgen.Install.CreateTable("login_logs", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"lid", "int", 0, false, true, ""}, - qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, - qgen.DBTableColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed? - qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"doneAt", "createdAt", 0, false, false, ""}, + []tblColumn{ + tblColumn{"lid", "int", 0, false, true, ""}, + tblColumn{"uid", "int", 0, false, false, ""}, + tblColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed? + tblColumn{"ipaddress", "varchar", 200, false, false, ""}, + tblColumn{"doneAt", "createdAt", 0, false, false, ""}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"lid", "primary"}, + []tblKey{ + tblKey{"lid", "primary"}, }, ) qgen.Install.CreateTable("moderation_logs", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"action", "varchar", 100, false, false, ""}, - 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, ""}, // TODO: Make this a foreign key - qgen.DBTableColumn{"doneAt", "datetime", 0, false, false, ""}, + []tblColumn{ + tblColumn{"action", "varchar", 100, false, false, ""}, + tblColumn{"elementID", "int", 0, false, false, ""}, + tblColumn{"elementType", "varchar", 100, false, false, ""}, + tblColumn{"ipaddress", "varchar", 200, false, false, ""}, + tblColumn{"actorID", "int", 0, false, false, ""}, // TODO: Make this a foreign key + tblColumn{"doneAt", "datetime", 0, false, false, ""}, }, - []qgen.DBTableKey{}, + []tblKey{}, ) qgen.Install.CreateTable("administration_logs", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"action", "varchar", 100, false, false, ""}, - 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, ""}, // TODO: Make this a foreign key - qgen.DBTableColumn{"doneAt", "datetime", 0, false, false, ""}, + []tblColumn{ + tblColumn{"action", "varchar", 100, false, false, ""}, + tblColumn{"elementID", "int", 0, false, false, ""}, + tblColumn{"elementType", "varchar", 100, false, false, ""}, + tblColumn{"ipaddress", "varchar", 200, false, false, ""}, + tblColumn{"actorID", "int", 0, false, false, ""}, // TODO: Make this a foreign key + tblColumn{"doneAt", "datetime", 0, false, false, ""}, }, - []qgen.DBTableKey{}, + []tblKey{}, ) qgen.Install.CreateTable("viewchunks", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"count", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"createdAt", "datetime", 0, false, false, ""}, - qgen.DBTableColumn{"route", "varchar", 200, false, false, ""}, + []tblColumn{ + tblColumn{"count", "int", 0, false, false, "0"}, + tblColumn{"createdAt", "datetime", 0, false, false, ""}, + tblColumn{"route", "varchar", 200, false, false, ""}, }, - []qgen.DBTableKey{}, + []tblKey{}, ) qgen.Install.CreateTable("viewchunks_agents", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"count", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"createdAt", "datetime", 0, false, false, ""}, - qgen.DBTableColumn{"browser", "varchar", 200, false, false, ""}, // googlebot, firefox, opera, etc. - //qgen.DBTableColumn{"version","varchar",0,false,false,""}, // the version of the browser or bot + []tblColumn{ + tblColumn{"count", "int", 0, false, false, "0"}, + tblColumn{"createdAt", "datetime", 0, false, false, ""}, + tblColumn{"browser", "varchar", 200, false, false, ""}, // googlebot, firefox, opera, etc. + //tblColumn{"version","varchar",0,false,false,""}, // the version of the browser or bot }, - []qgen.DBTableKey{}, + []tblKey{}, ) qgen.Install.CreateTable("viewchunks_systems", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"count", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"createdAt", "datetime", 0, false, false, ""}, - qgen.DBTableColumn{"system", "varchar", 200, false, false, ""}, // windows, android, unknown, etc. + []tblColumn{ + tblColumn{"count", "int", 0, false, false, "0"}, + tblColumn{"createdAt", "datetime", 0, false, false, ""}, + tblColumn{"system", "varchar", 200, false, false, ""}, // windows, android, unknown, etc. }, - []qgen.DBTableKey{}, + []tblKey{}, ) qgen.Install.CreateTable("viewchunks_langs", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"count", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"createdAt", "datetime", 0, false, false, ""}, - qgen.DBTableColumn{"lang", "varchar", 200, false, false, ""}, // en, ru, etc. + []tblColumn{ + tblColumn{"count", "int", 0, false, false, "0"}, + tblColumn{"createdAt", "datetime", 0, false, false, ""}, + tblColumn{"lang", "varchar", 200, false, false, ""}, // en, ru, etc. }, - []qgen.DBTableKey{}, + []tblKey{}, ) qgen.Install.CreateTable("viewchunks_referrers", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"count", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"createdAt", "datetime", 0, false, false, ""}, - qgen.DBTableColumn{"domain", "varchar", 200, false, false, ""}, + []tblColumn{ + tblColumn{"count", "int", 0, false, false, "0"}, + tblColumn{"createdAt", "datetime", 0, false, false, ""}, + tblColumn{"domain", "varchar", 200, false, false, ""}, }, - []qgen.DBTableKey{}, + []tblKey{}, ) qgen.Install.CreateTable("viewchunks_forums", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"count", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"createdAt", "datetime", 0, false, false, ""}, - qgen.DBTableColumn{"forum", "int", 0, false, false, ""}, + []tblColumn{ + tblColumn{"count", "int", 0, false, false, "0"}, + tblColumn{"createdAt", "datetime", 0, false, false, ""}, + tblColumn{"forum", "int", 0, false, false, ""}, }, - []qgen.DBTableKey{}, + []tblKey{}, ) qgen.Install.CreateTable("topicchunks", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"count", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"createdAt", "datetime", 0, false, false, ""}, + []tblColumn{ + tblColumn{"count", "int", 0, false, false, "0"}, + tblColumn{"createdAt", "datetime", 0, false, false, ""}, // TODO: Add a column for the parent forum? }, - []qgen.DBTableKey{}, + []tblKey{}, ) qgen.Install.CreateTable("postchunks", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"count", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"createdAt", "datetime", 0, false, false, ""}, + []tblColumn{ + tblColumn{"count", "int", 0, false, false, "0"}, + tblColumn{"createdAt", "datetime", 0, false, false, ""}, // TODO: Add a column for the parent topic / profile? }, - []qgen.DBTableKey{}, + []tblKey{}, ) qgen.Install.CreateTable("sync", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"last_update", "datetime", 0, false, false, ""}, + []tblColumn{ + tblColumn{"last_update", "datetime", 0, false, false, ""}, }, - []qgen.DBTableKey{}, + []tblKey{}, ) qgen.Install.CreateTable("updates", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"dbVersion", "int", 0, false, false, "0"}, + []tblColumn{ + tblColumn{"dbVersion", "int", 0, false, false, "0"}, }, - []qgen.DBTableKey{}, + []tblKey{}, ) return nil diff --git a/common/attachments.go b/common/attachments.go index a83446e3..35574e8d 100644 --- a/common/attachments.go +++ b/common/attachments.go @@ -2,28 +2,128 @@ package common import ( "database/sql" + "errors" + "strings" "github.com/Azareal/Gosora/query_gen" ) var Attachments AttachmentStore +type MiniAttachment struct { + ID int + SectionID int + OriginID int + UploadedBy int + Path string + + Image bool + Ext string +} + type AttachmentStore interface { - Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string) error + Get(id int) (*MiniAttachment, error) + MiniTopicGet(id int) (alist []*MiniAttachment, err error) + Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string) (int, error) + GlobalCount() int + CountInTopic(tid int) int + CountInPath(path string) int + Delete(aid int) error } type DefaultAttachmentStore struct { - add *sql.Stmt + get *sql.Stmt + getByTopic *sql.Stmt + add *sql.Stmt + count *sql.Stmt + countInTopic *sql.Stmt + countInPath *sql.Stmt + delete *sql.Stmt } func NewDefaultAttachmentStore() (*DefaultAttachmentStore, error) { acc := qgen.NewAcc() return &DefaultAttachmentStore{ - add: acc.Insert("attachments").Columns("sectionID, sectionTable, originID, originTable, uploadedBy, path").Fields("?,?,?,?,?,?").Prepare(), + get: acc.Select("attachments").Columns("originID, sectionID, uploadedBy, path").Where("attachID = ?").Prepare(), + getByTopic: acc.Select("attachments").Columns("attachID, sectionID, uploadedBy, path").Where("originTable = 'topics' AND originID = ?").Prepare(), + add: acc.Insert("attachments").Columns("sectionID, sectionTable, originID, originTable, uploadedBy, path").Fields("?,?,?,?,?,?").Prepare(), + count: acc.Count("attachments").Prepare(), + countInTopic: acc.Count("attachments").Where("originTable = 'topics' and originID = ?").Prepare(), + countInPath: acc.Count("attachments").Where("path = ?").Prepare(), + delete: acc.Delete("attachments").Where("attachID = ?").Prepare(), }, acc.FirstError() } -func (store *DefaultAttachmentStore) Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string) error { - _, err := store.add.Exec(sectionID, sectionTable, originID, originTable, uploadedBy, path) +// TODO: Make this more generic so we can use it for reply attachments too +func (store *DefaultAttachmentStore) MiniTopicGet(id int) (alist []*MiniAttachment, err error) { + rows, err := store.getByTopic.Query(id) + defer rows.Close() + for rows.Next() { + attach := &MiniAttachment{OriginID: id} + err := rows.Scan(&attach.ID, &attach.SectionID, &attach.UploadedBy, &attach.Path) + if err != nil { + return nil, err + } + extarr := strings.Split(attach.Path, ".") + if len(extarr) < 2 { + return nil, errors.New("corrupt attachment path") + } + attach.Ext = extarr[len(extarr)-1] + attach.Image = ImageFileExts.Contains(attach.Ext) + alist = append(alist, attach) + } + return alist, rows.Err() +} + +func (store *DefaultAttachmentStore) Get(id int) (*MiniAttachment, error) { + attach := &MiniAttachment{ID: id} + err := store.get.QueryRow(id).Scan(&attach.OriginID, &attach.SectionID, &attach.UploadedBy, &attach.Path) + if err != nil { + return nil, err + } + extarr := strings.Split(attach.Path, ".") + if len(extarr) < 2 { + return nil, errors.New("corrupt attachment path") + } + attach.Ext = extarr[len(extarr)-1] + attach.Image = ImageFileExts.Contains(attach.Ext) + return attach, nil +} + +func (store *DefaultAttachmentStore) Add(sectionID int, sectionTable string, originID int, originTable string, uploadedBy int, path string) (int, error) { + res, err := store.add.Exec(sectionID, sectionTable, originID, originTable, uploadedBy, path) + if err != nil { + return 0, err + } + lid, err := res.LastInsertId() + return int(lid), err +} + +func (store *DefaultAttachmentStore) GlobalCount() (count int) { + err := store.count.QueryRow().Scan(&count) + if err != nil { + LogError(err) + } + return count +} + +func (store *DefaultAttachmentStore) CountInTopic(tid int) (count int) { + err := store.countInTopic.QueryRow(tid).Scan(&count) + if err != nil { + LogError(err) + } + return count +} + +func (store *DefaultAttachmentStore) CountInPath(path string) (count int) { + err := store.countInPath.QueryRow(path).Scan(&count) + if err != nil { + LogError(err) + } + return count +} + +func (store *DefaultAttachmentStore) Delete(aid int) error { + _, err := store.delete.Exec(aid) return err } diff --git a/common/common.go b/common/common.go index 00e94349..e4055280 100644 --- a/common/common.go +++ b/common/common.go @@ -11,6 +11,7 @@ import ( "log" "sync/atomic" "time" + "github.com/Azareal/Gosora/query_gen" ) @@ -33,7 +34,7 @@ var TmplPtrMap = make(map[string]interface{}) // Anti-spam token with rotated key var JSTokenBox atomic.Value // TODO: Move this and some of these other globals somewhere else -var SessionSigningKeyBox atomic.Value // For MFA to avoid hitting the database unneccesarily +var SessionSigningKeyBox atomic.Value // For MFA to avoid hitting the database unneccessarily var OldSessionSigningKeyBox atomic.Value // Just in case we've signed with a key that's about to go stale so we don't annoy the user too much var IsDBDown int32 = 0 // 0 = false, 1 = true. this is value which should be manipulated with package atomic for representing whether the database is down so we don't spam the log with lots of redundant errors diff --git a/common/errors.go b/common/errors.go index 3d0be426..07977c6e 100644 --- a/common/errors.go +++ b/common/errors.go @@ -314,12 +314,29 @@ func SecurityError(w http.ResponseWriter, r *http.Request, user User) RouteError } // NotFound is used when the requested page doesn't exist -// ? - Add a JSQ and JS version of this? +// ? - Add a JSQ version of this? // ? - Add a user parameter? func NotFound(w http.ResponseWriter, r *http.Request, header *Header) RouteError { return CustomError(phrases.GetErrorPhrase("not_found_body"), 404, phrases.GetErrorPhrase("not_found_title"), w, r, header, GuestUser) } +// ? - Add a user parameter? +func NotFoundJS(w http.ResponseWriter, r *http.Request) RouteError { + w.WriteHeader(401) + writeJsonError(phrases.GetErrorPhrase("not_found_body"), w) + return HandledRouteError() +} + +func NotFoundJSQ(w http.ResponseWriter, r *http.Request, header *Header, js bool) RouteError { + if js { + return NotFoundJS(w, r) + } + if header == nil { + header = DefaultHeader(w, GuestUser) + } + return NotFound(w, r, header) +} + // CustomError lets us make custom error types which aren't covered by the generic functions above func CustomError(errmsg string, errcode int, errtitle string, w http.ResponseWriter, r *http.Request, header *Header, user User) RouteError { if header == nil { diff --git a/common/files.go b/common/files.go index a348a2fa..e10712fc 100644 --- a/common/files.go +++ b/common/files.go @@ -94,7 +94,7 @@ func (list SFileList) JSTmplInit() error { preLen := len(data) data = replace(data, string(data[spaceIndex:endBrace]), "") - data = replace(data, "))\n", "\n") + data = replace(data, "))\n", " \n") endBrace -= preLen - len(data) // Offset it as we've deleted portions fmt.Println("new endBrace: ", endBrace) fmt.Println("data: ", string(data)) @@ -130,58 +130,38 @@ func (list SFileList) JSTmplInit() error { } } each("strconv.Itoa(", 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 + braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')') if hasEndBrace { data[braceAt] = ' ' // Blank it } }) - each("w.Write([]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 - } - braceAt, hasEndBrace = skipUntilIfExists(data, braceAt, ')') - if hasEndBrace { - 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 + each("[]byte(", func(index int) { + braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')') if hasEndBrace { data[braceAt] = ' ' // Blank it } }) - each("w.Write(StringToBytes(", 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 - } - braceAt, hasEndBrace = skipUntilIfExists(data, braceAt, ')') - if hasEndBrace { - data[braceAt] = ' ' // Blank this one too - } - }) - each(" = StringToBytes(", 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 + each("StringToBytes(", func(index int) { + braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')') 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 + braceAt, hasEndBrace := skipUntilIfExistsOrLine(data, index, ')') if hasEndBrace { data[braceAt] = ' ' // Blank it } }) + each("RelativeTime(", func(index int) { + braceAt, _ := skipUntilIfExistsOrLine(data, index, 10) + if data[braceAt-1] == ' ' { + data[braceAt-1] = ')' // Blank it + } + }) each("if ", func(index int) { //fmt.Println("if index: ", index) - braceAt, hasBrace := skipUntilIfExists(data, index, '{') + braceAt, hasBrace := skipUntilIfExistsOrLine(data, index, '{') if hasBrace { if data[braceAt-1] != ' ' { panic("couldn't find space before brace, found ' " + string(data[braceAt-1]) + "' instead") @@ -210,10 +190,12 @@ func (list SFileList) JSTmplInit() error { data = replace(data, ", 10;", "") 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, " = StringToBytes(", " = ") + data = replace(data, "[]byte(", "") + data = replace(data, "StringToBytes(", "") + // TODO: Format dates properly on the client side + data = replace(data, ".Format(\"2006-01-02 15:04:05\"", "") + data = replace(data, ", 10", "") data = replace(data, "if ", "if(") data = replace(data, "return nil", "return out") data = replace(data, " )", ")") diff --git a/common/menus.go b/common/menus.go index d179fd1d..09735149 100644 --- a/common/menus.go +++ b/common/menus.go @@ -190,6 +190,18 @@ func skipUntilIfExists(tmplData []byte, i int, expects byte) (newI int, hasIt bo return j, false } +func skipUntilIfExistsOrLine(tmplData []byte, i int, expects byte) (newI int, hasIt bool) { + j := i + for ; j < len(tmplData); j++ { + if tmplData[j] == 10 { + return j, false + } else if tmplData[j] == expects { + return j, true + } + } + return j, false +} + func skipUntilCharsExist(tmplData []byte, i int, expects []byte) (newI int, hasIt bool) { j := i expectIndex := 0 diff --git a/common/profile_reply.go b/common/profile_reply.go index 64084483..51b77809 100644 --- a/common/profile_reply.go +++ b/common/profile_reply.go @@ -11,17 +11,16 @@ import ( var profileReplyStmts ProfileReplyStmts type ProfileReply struct { - ID int - ParentID int - Content string - CreatedBy int - Group int - CreatedAt time.Time - RelativeCreatedAt string - LastEdit int - LastEditBy int - ContentLines int - IPAddress string + ID int + ParentID int + Content string + CreatedBy int + Group int + CreatedAt time.Time + LastEdit int + LastEditBy int + ContentLines int + IPAddress string } type ProfileReplyStmts struct { diff --git a/common/reply.go b/common/reply.go index f571e10d..f1c294a0 100644 --- a/common/reply.go +++ b/common/reply.go @@ -16,48 +16,46 @@ import ( ) type ReplyUser struct { - ID int - ParentID int - Content string - ContentHtml string - CreatedBy int - UserLink string - CreatedByName string - Group int - CreatedAt time.Time - RelativeCreatedAt string - LastEdit int - LastEditBy int - Avatar string - MicroAvatar string - ClassName string - ContentLines int - Tag string - URL string - URLPrefix string - URLName string - Level int - IPAddress string - Liked bool - LikeCount int - ActionType string - ActionIcon string + ID int + ParentID int + Content string + ContentHtml string + CreatedBy int + UserLink string + CreatedByName string + Group int + CreatedAt time.Time + LastEdit int + LastEditBy int + Avatar string + MicroAvatar string + ClassName string + ContentLines int + Tag string + URL string + URLPrefix string + URLName string + Level int + IPAddress string + Liked bool + LikeCount int + ActionType string + ActionIcon string } type Reply struct { - ID int - ParentID int - Content string - CreatedBy int - Group int - CreatedAt time.Time - RelativeCreatedAt string - LastEdit int - LastEditBy int - ContentLines int - IPAddress string - Liked bool - LikeCount int + ID int + ParentID int + Content string + CreatedBy int + Group int + CreatedAt time.Time + LastEdit int + LastEditBy int + ContentLines int + IPAddress string + Liked bool + LikeCount int } var ErrAlreadyLiked = errors.New("You already liked this!") diff --git a/common/reply_store.go b/common/reply_store.go index 943809b3..eaa3ba0f 100644 --- a/common/reply_store.go +++ b/common/reply_store.go @@ -40,5 +40,5 @@ func (store *SQLReplyStore) Create(topic *Topic, content string, ipaddress strin if err != nil { return 0, err } - return int(lastID), topic.AddReply(uid) + return int(lastID), topic.AddReply(int(lastID), uid) } diff --git a/common/routes_common.go b/common/routes_common.go index 56cbfba9..52c2aef4 100644 --- a/common/routes_common.go +++ b/common/routes_common.go @@ -341,7 +341,7 @@ func HandleUploadRoute(w http.ResponseWriter, r *http.Request, user User, maxFil size, unit := ConvertByteUnit(float64(maxFileSize)) return CustomError("Your upload is too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, nil, user) } - r.Body = http.MaxBytesReader(w, r.Body, int64(maxFileSize)) + r.Body = http.MaxBytesReader(w, r.Body, r.ContentLength) err := r.ParseMultipartForm(int64(Megabyte)) if err != nil { diff --git a/common/template_init.go b/common/template_init.go index c40c68aa..33893171 100644 --- a/common/template_init.go +++ b/common/template_init.go @@ -226,11 +226,12 @@ func CompileTemplates() error { PollOption{1, "Something"}, }, VoteCount: 7} avatar, microAvatar := BuildAvatar(62, "") - topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, now, RelativeTime(now), now, RelativeTime(now), 0, "", "127.0.0.1", 1, 0, 1, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false} + miniAttach := []*MiniAttachment{&MiniAttachment{Path: "/"}} + topic := TopicUser{1, "blah", "Blah", "Hey there!", 0, false, false, now, now, 1, 1, 0, "", "127.0.0.1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false, miniAttach} var replyList []ReplyUser // TODO: Do we want the UID on this to be 0? avatar, microAvatar = BuildAvatar(0, "") - replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, RelativeTime(now), 0, 0, avatar, microAvatar, "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""}) + replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, 0, 0, avatar, microAvatar, "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""}) var varList = make(map[string]tmpl.VarItem) var compile = func(name string, expects string, expectsInt interface{}) (tmpl string, err error) { @@ -285,7 +286,7 @@ func CompileTemplates() error { } var topicsList []*TopicsRow - topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, "Date", user3.ID, 1, "", "127.0.0.1", 1, 0, 1, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}) + topicsList = append(topicsList, &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "127.0.0.1", 1, 0, 1, 1, 0, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"}) header2.Title = "Topic List" topicListPage := TopicListPage{header, topicsList, forumList, Config.DefaultForum, TopicListSort{"lastupdated", false}, Paginator{[]int{1}, 1, 1}} /*topicListTmpl, err := compile("topics", "common.TopicListPage", topicListPage) @@ -439,7 +440,7 @@ func CompileJSTemplates() error { // TODO: Fix the import loop so we don't have to use this hack anymore c.SetBuildTags("!no_templategen,tmplgentopic") - var topicsRow = &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, "Date", user3.ID, 1, "", "127.0.0.1", 1, 0, 1, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"} + var topicsRow = &TopicsRow{1, "topic-title", "Topic Title", "The topic content.", 1, false, false, now, now, user3.ID, 1, 1, "", "127.0.0.1", 1, 0, 1, 0, 1, "classname", "", &user2, "", 0, &user3, "General", "/forum/general.2"} topicListItemTmpl, err := c.Compile("topics_topic.html", "templates/", "*common.TopicsRow", topicsRow, varList) if err != nil { return err @@ -450,11 +451,12 @@ func CompileJSTemplates() error { PollOption{1, "Something"}, }, VoteCount: 7} avatar, microAvatar := BuildAvatar(62, "") - topic := TopicUser{1, "blah", "Blah", "Hey there!", 62, false, false, now, RelativeTime(now), now, RelativeTime(now), 0, "", "127.0.0.1", 1, 0, 1, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false} + miniAttach := []*MiniAttachment{&MiniAttachment{Path: "/"}} + topic := TopicUser{1, "blah", "Blah", "Hey there!", 62, false, false, now, now, 1, 1, 0, "", "127.0.0.1", 1, 0, 1, 0, "classname", poll.ID, "weird-data", BuildProfileURL("fake-user", 62), "Fake User", Config.DefaultGroup, avatar, microAvatar, 0, "", "", "", "", "", 58, false, miniAttach} var replyList []ReplyUser // TODO: Do we really want the UID here to be zero? avatar, microAvatar = BuildAvatar(0, "") - replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, RelativeTime(now), 0, 0, avatar, microAvatar, "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""}) + replyList = append(replyList, ReplyUser{0, 0, "Yo!", "Yo!", 0, "alice", "Alice", Config.DefaultGroup, now, 0, 0, avatar, microAvatar, "", 0, "", "", "", "", 0, "127.0.0.1", false, 1, "", ""}) varList = make(map[string]tmpl.VarItem) header.Title = "Topic Name" @@ -639,10 +641,17 @@ func InitTemplates() error { if !ok { panic("timeInt is not a time.Time") } - //return time.String() return time.Format("2006-01-02 15:04:05") } + fmap["reltime"] = func(timeInt interface{}) interface{} { + time, ok := timeInt.(time.Time) + if !ok { + panic("timeInt is not a time.Time") + } + return RelativeTime(time) + } + fmap["scope"] = func(name interface{}) interface{} { return "" } diff --git a/common/templates/templates.go b/common/templates/templates.go index 2289caf0..f528801d 100644 --- a/common/templates/templates.go +++ b/common/templates/templates.go @@ -91,6 +91,7 @@ func NewCTemplateSet() *CTemplateSet { //"langf":true, "level": true, "abstime": true, + "reltime": true, "scope": true, "dyntmpl": true, }, @@ -959,6 +960,16 @@ ArgLoop: // TODO: Refactor this litString(leftParam+".Format(\"2006-01-02 15:04:05\")", false) break ArgLoop + case "reltime": + // TODO: Implement level literals + leftOperand := node.Args[pos+1].String() + if len(leftOperand) == 0 { + panic("The leftoperand for function reltime cannot be left blank") + } + leftParam, _ := c.compileIfVarSub(con, leftOperand) + // TODO: Refactor this + litString("common.RelativeTime("+leftParam+")", false) + break ArgLoop case "scope": literal = true break ArgLoop diff --git a/common/topic.go b/common/topic.go index 5363f279..75a29410 100644 --- a/common/topic.go +++ b/common/topic.go @@ -22,51 +22,51 @@ import ( // ? - Add a TopicMeta struct for *Forums? type Topic struct { - ID int - Link string - Title string - Content string - CreatedBy int - IsClosed bool - Sticky bool - CreatedAt time.Time - RelativeCreatedAt string - LastReplyAt time.Time - RelativeLastReplyAt string - //LastReplyBy int - ParentID int - Status string // Deprecated. Marked for removal. - IPAddress string - ViewCount int64 - PostCount int - LikeCount int - ClassName string // CSS Class Name - Poll int - Data string // Used for report metadata + ID int + Link string + Title string + Content string + CreatedBy int + IsClosed bool + Sticky bool + CreatedAt time.Time + LastReplyAt time.Time + LastReplyBy int + LastReplyID int + ParentID int + Status string // Deprecated. Marked for removal. + IPAddress string + ViewCount int64 + PostCount int + LikeCount int + AttachCount int + ClassName string // CSS Class Name + Poll int + Data string // Used for report metadata } type TopicUser struct { - ID int - Link string - Title string - Content string // TODO: Avoid converting this to bytes in templates, particularly if it's long - CreatedBy int - IsClosed bool - Sticky bool - CreatedAt time.Time - RelativeCreatedAt string - LastReplyAt time.Time - RelativeLastReplyAt string - //LastReplyBy int - ParentID int - Status string // Deprecated. Marked for removal. - IPAddress string - ViewCount int64 - PostCount int - LikeCount int - ClassName string - Poll int - Data string // Used for report metadata + ID int + Link string + Title string + Content string // TODO: Avoid converting this to bytes in templates, particularly if it's long + CreatedBy int + IsClosed bool + Sticky bool + CreatedAt time.Time + LastReplyAt time.Time + LastReplyBy int + LastReplyID int + ParentID int + Status string // Deprecated. Marked for removal. + IPAddress string + ViewCount int64 + PostCount int + LikeCount int + AttachCount int + ClassName string + Poll int + Data string // Used for report metadata UserLink string CreatedByName string @@ -81,30 +81,32 @@ type TopicUser struct { URLName string Level int Liked bool + + Attachments []*MiniAttachment } type TopicsRow struct { - ID int - Link string - Title string - Content string - CreatedBy int - IsClosed bool - Sticky bool - CreatedAt time.Time - //RelativeCreatedAt string - LastReplyAt time.Time - RelativeLastReplyAt string - LastReplyBy int - ParentID int - Status string // Deprecated. Marked for removal. -Is there anything we could use it for? - IPAddress string - ViewCount int64 - PostCount int - LikeCount int - LastPage int - ClassName string - Data string // Used for report metadata + ID int + Link string + Title string + Content string + CreatedBy int + IsClosed bool + Sticky bool + CreatedAt time.Time + LastReplyAt time.Time + LastReplyBy int + LastReplyID int + ParentID int + Status string // Deprecated. Marked for removal. -Is there anything we could use it for? + IPAddress string + ViewCount int64 + PostCount int + LikeCount int + AttachCount int + LastPage int + ClassName string + Data string // Used for report metadata Creator *User CSS template.CSS @@ -126,10 +128,12 @@ type WsTopicsRow struct { LastReplyAt time.Time RelativeLastReplyAt string LastReplyBy int + LastReplyID int ParentID int ViewCount int64 PostCount int LikeCount int + AttachCount int ClassName string Creator *WsJSONUser LastUser *WsJSONUser @@ -137,24 +141,26 @@ type WsTopicsRow struct { ForumLink string } +// TODO: Can we get the client side to render the relative times instead? func (row *TopicsRow) WebSockets() *WsTopicsRow { - return &WsTopicsRow{row.ID, row.Link, row.Title, row.CreatedBy, row.IsClosed, row.Sticky, row.CreatedAt, row.LastReplyAt, row.RelativeLastReplyAt, row.LastReplyBy, row.ParentID, row.ViewCount, row.PostCount, row.LikeCount, row.ClassName, row.Creator.WebSockets(), row.LastUser.WebSockets(), row.ForumName, row.ForumLink} + return &WsTopicsRow{row.ID, row.Link, row.Title, row.CreatedBy, row.IsClosed, row.Sticky, row.CreatedAt, row.LastReplyAt, RelativeTime(row.LastReplyAt), row.LastReplyBy, row.LastReplyID, row.ParentID, row.ViewCount, row.PostCount, row.LikeCount, row.AttachCount, row.ClassName, row.Creator.WebSockets(), row.LastUser.WebSockets(), row.ForumName, row.ForumLink} } type TopicStmts struct { - addRepliesToTopic *sql.Stmt - lock *sql.Stmt - unlock *sql.Stmt - moveTo *sql.Stmt - stick *sql.Stmt - unstick *sql.Stmt - hasLikedTopic *sql.Stmt - createLike *sql.Stmt - addLikesToTopic *sql.Stmt - delete *sql.Stmt - edit *sql.Stmt - setPoll *sql.Stmt - createActionReply *sql.Stmt + addReplies *sql.Stmt + updateLastReply *sql.Stmt + lock *sql.Stmt + unlock *sql.Stmt + moveTo *sql.Stmt + stick *sql.Stmt + unstick *sql.Stmt + hasLikedTopic *sql.Stmt + createLike *sql.Stmt + addLikesToTopic *sql.Stmt + delete *sql.Stmt + edit *sql.Stmt + setPoll *sql.Stmt + createAction *sql.Stmt getTopicUser *sql.Stmt // TODO: Can we get rid of this? getByReplyID *sql.Stmt @@ -165,21 +171,22 @@ var topicStmts TopicStmts func init() { DbInits.Add(func(acc *qgen.Accumulator) error { topicStmts = TopicStmts{ - addRepliesToTopic: acc.Update("topics").Set("postCount = postCount + ?, lastReplyBy = ?, lastReplyAt = UTC_TIMESTAMP()").Where("tid = ?").Prepare(), - lock: acc.Update("topics").Set("is_closed = 1").Where("tid = ?").Prepare(), - unlock: acc.Update("topics").Set("is_closed = 0").Where("tid = ?").Prepare(), - moveTo: acc.Update("topics").Set("parentID = ?").Where("tid = ?").Prepare(), - 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, 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? - setPoll: acc.Update("topics").Set("content = '', parsed_content = '', poll = ?").Where("tid = ? AND poll = 0").Prepare(), - createActionReply: acc.Insert("replies").Columns("tid, actionType, ipaddress, createdBy, createdAt, lastUpdated, content, parsed_content").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''").Prepare(), + addReplies: acc.Update("topics").Set("postCount = postCount + ?, lastReplyBy = ?, lastReplyAt = UTC_TIMESTAMP()").Where("tid = ?").Prepare(), + updateLastReply: acc.Update("topics").Set("lastReplyID = ?").Where("lastReplyID > ? AND tid = ?").Prepare(), + lock: acc.Update("topics").Set("is_closed = 1").Where("tid = ?").Prepare(), + unlock: acc.Update("topics").Set("is_closed = 0").Where("tid = ?").Prepare(), + moveTo: acc.Update("topics").Set("parentID = ?").Where("tid = ?").Prepare(), + 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, 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? + setPoll: acc.Update("topics").Set("content = '', parsed_content = '', poll = ?").Where("tid = ? AND poll = 0").Prepare(), + createAction: acc.Insert("replies").Columns("tid, actionType, ipaddress, createdBy, createdAt, lastUpdated, content, parsed_content").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),'',''").Prepare(), - getTopicUser: acc.SimpleLeftJoin("topics", "users", "topics.title, topics.content, topics.createdBy, topics.createdAt, topics.is_closed, topics.sticky, topics.parentID, topics.ipaddress, topics.views, topics.postCount, topics.likeCount, topics.poll, users.name, users.avatar, users.group, users.url_prefix, users.url_name, users.level", "topics.createdBy = users.uid", "tid = ?", "", ""), + getTopicUser: acc.SimpleLeftJoin("topics", "users", "topics.title, topics.content, topics.createdBy, topics.createdAt, topics.lastReplyAt, topics.lastReplyBy, topics.lastReplyID, topics.is_closed, topics.sticky, topics.parentID, topics.ipaddress, topics.views, topics.postCount, topics.likeCount, topics.attachCount,topics.poll, users.name, users.avatar, users.group, users.url_prefix, users.url_name, users.level", "topics.createdBy = users.uid", "tid = ?", "", ""), getByReplyID: acc.SimpleLeftJoin("replies", "topics", "topics.tid, topics.title, topics.content, topics.createdBy, topics.createdAt, topics.is_closed, topics.sticky, topics.parentID, topics.ipaddress, topics.views, topics.postCount, topics.likeCount, topics.poll, topics.data", "replies.tid = topics.tid", "rid = ?", "", ""), } return acc.FirstError() @@ -197,8 +204,12 @@ func (topic *Topic) cacheRemove() { } // TODO: Write a test for this -func (topic *Topic) AddReply(uid int) (err error) { - _, err = topicStmts.addRepliesToTopic.Exec(1, uid, topic.ID) +func (topic *Topic) AddReply(rid int, uid int) (err error) { + _, err = topicStmts.addReplies.Exec(1, uid, topic.ID) + if err != nil { + return err + } + _, err = topicStmts.updateLastReply.Exec(rid, rid, topic.ID) topic.cacheRemove() return err } @@ -314,11 +325,20 @@ func (topic *Topic) SetPoll(pollID int) error { // TODO: Have this go through the ReplyStore? func (topic *Topic) CreateActionReply(action string, ipaddress string, uid int) (err error) { - _, err = topicStmts.createActionReply.Exec(topic.ID, action, ipaddress, uid) + res, err := topicStmts.createAction.Exec(topic.ID, action, ipaddress, uid) if err != nil { return err } - _, err = topicStmts.addRepliesToTopic.Exec(1, uid, topic.ID) + _, err = topicStmts.addReplies.Exec(1, uid, topic.ID) + if err != nil { + return err + } + lid, err := res.LastInsertId() + if err != nil { + return err + } + rid := int(lid) + _, err = topicStmts.updateLastReply.Exec(rid, rid, topic.ID) topic.cacheRemove() // ? - Update the last topic cache for the parent forum? return err @@ -336,7 +356,7 @@ func (topic *Topic) Copy() Topic { return *topic } -// TODO: Load LastReplyAt? +// TODO: Load LastReplyAt and LastReplyID? func TopicByReplyID(rid int) (*Topic, error) { topic := Topic{ID: 0} err := topicStmts.getByReplyID.QueryRow(rid).Scan(&topic.ID, &topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data) @@ -376,14 +396,15 @@ func GetTopicUser(user *User, tid int) (tu TopicUser, err error) { } 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) + // TODO: This misses some important bits... + err = topicStmts.getTopicUser.QueryRow(tid).Scan(&tu.Title, &tu.Content, &tu.CreatedBy, &tu.CreatedAt, &tu.LastReplyAt, &tu.LastReplyBy, &tu.LastReplyID, &tu.IsClosed, &tu.Sticky, &tu.ParentID, &tu.IPAddress, &tu.ViewCount, &tu.PostCount, &tu.LikeCount, &tu.AttachCount, &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) tu.Tag = Groups.DirtyGet(tu.Group).Tag if tcache != nil { - theTopic := Topic{ID: tu.ID, Link: tu.Link, Title: tu.Title, Content: tu.Content, CreatedBy: tu.CreatedBy, IsClosed: tu.IsClosed, Sticky: tu.Sticky, CreatedAt: tu.CreatedAt, LastReplyAt: tu.LastReplyAt, ParentID: tu.ParentID, IPAddress: tu.IPAddress, ViewCount: tu.ViewCount, PostCount: tu.PostCount, LikeCount: tu.LikeCount, Poll: tu.Poll} + theTopic := Topic{ID: tu.ID, Link: tu.Link, Title: tu.Title, Content: tu.Content, CreatedBy: tu.CreatedBy, IsClosed: tu.IsClosed, Sticky: tu.Sticky, CreatedAt: tu.CreatedAt, LastReplyAt: tu.LastReplyAt, LastReplyID: tu.LastReplyID, ParentID: tu.ParentID, IPAddress: tu.IPAddress, ViewCount: tu.ViewCount, PostCount: tu.PostCount, LikeCount: tu.LikeCount, AttachCount: tu.AttachCount, Poll: tu.Poll} //log.Printf("theTopic: %+v\n", theTopic) _ = tcache.Add(&theTopic) } @@ -409,11 +430,13 @@ func copyTopicToTopicUser(topic *Topic, user *User) (tu TopicUser) { tu.Sticky = topic.Sticky tu.CreatedAt = topic.CreatedAt tu.LastReplyAt = topic.LastReplyAt + tu.LastReplyBy = topic.LastReplyBy tu.ParentID = topic.ParentID tu.IPAddress = topic.IPAddress tu.ViewCount = topic.ViewCount tu.PostCount = topic.PostCount tu.LikeCount = topic.LikeCount + tu.AttachCount = topic.AttachCount tu.Poll = topic.Poll tu.Data = topic.Data diff --git a/common/topic_list.go b/common/topic_list.go index 3cd52a2e..f5fe681d 100644 --- a/common/topic_list.go +++ b/common/topic_list.go @@ -211,7 +211,7 @@ func (tList *DefaultTopicList) getList(page int, orderby string, argList []inter } // TODO: Prepare common qlist lengths to speed this up in common cases, prepared statements are prepared lazily anyway, so it probably doesn't matter if we do ten or so - stmt, err := qgen.Builder.SimpleSelect("topics", "tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, parentID, views, postCount, likeCount", "parentID IN("+qlist+")", orderq, "?,?") + stmt, err := qgen.Builder.SimpleSelect("topics", "tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, lastReplyID, parentID, views, postCount, likeCount", "parentID IN("+qlist+")", orderq, "?,?") if err != nil { return nil, Paginator{nil, 1, 1}, err } @@ -230,7 +230,7 @@ func (tList *DefaultTopicList) getList(page int, orderby string, argList []inter for rows.Next() { // TODO: Embed Topic structs in TopicsRow to make it easier for us to reuse this work in the topic cache topicItem := TopicsRow{ID: 0} - err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.ParentID, &topicItem.ViewCount, &topicItem.PostCount, &topicItem.LikeCount) + err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.LastReplyID, &topicItem.ParentID, &topicItem.ViewCount, &topicItem.PostCount, &topicItem.LikeCount) if err != nil { return nil, Paginator{nil, 1, 1}, err } @@ -241,8 +241,6 @@ func (tList *DefaultTopicList) getList(page int, orderby string, argList []inter topicItem.ForumName = forum.Name topicItem.ForumLink = forum.Link - //topicItem.RelativeCreatedAt = RelativeTime(topicItem.CreatedAt) - topicItem.RelativeLastReplyAt = RelativeTime(topicItem.LastReplyAt) // TODO: Create a specialised function with a bit less overhead for getting the last page for a post count _, _, lastPage := PageOffset(topicItem.PostCount, 1, Config.ItemsPerPage) topicItem.LastPage = lastPage diff --git a/common/topic_store.go b/common/topic_store.go index da1610b7..03291e1d 100644 --- a/common/topic_store.go +++ b/common/topic_store.go @@ -57,7 +57,7 @@ func NewDefaultTopicStore(cache TopicCache) (*DefaultTopicStore, error) { } return &DefaultTopicStore{ cache: cache, - get: acc.Select("topics").Columns("title, content, createdBy, createdAt, lastReplyAt, is_closed, sticky, parentID, ipaddress, views, postCount, likeCount, poll, data").Where("tid = ?").Prepare(), + get: acc.Select("topics").Columns("title, content, createdBy, createdAt, lastReplyAt, lastReplyID, is_closed, sticky, parentID, ipaddress, views, postCount, likeCount, attachCount, poll, data").Where("tid = ?").Prepare(), exists: acc.Select("topics").Columns("tid").Where("tid = ?").Prepare(), topicCount: acc.Count("topics").Prepare(), create: acc.Insert("topics").Columns("parentID, title, content, parsed_content, createdAt, lastReplyAt, lastReplyBy, ipaddress, words, createdBy").Fields("?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP(),?,?,?,?").Prepare(), @@ -71,7 +71,7 @@ func (mts *DefaultTopicStore) DirtyGet(id int) *Topic { } topic = &Topic{ID: id} - err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data) + err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.LastReplyID, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.AttachCount, &topic.Poll, &topic.Data) if err == nil { topic.Link = BuildTopicURL(NameToSlug(topic.Title), id) _ = mts.cache.Add(topic) @@ -88,7 +88,7 @@ func (mts *DefaultTopicStore) Get(id int) (topic *Topic, err error) { } topic = &Topic{ID: id} - err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data) + err = mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.LastReplyID, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.AttachCount, &topic.Poll, &topic.Data) if err == nil { topic.Link = BuildTopicURL(NameToSlug(topic.Title), id) _ = mts.cache.Add(topic) @@ -99,14 +99,14 @@ func (mts *DefaultTopicStore) Get(id int) (topic *Topic, err error) { // BypassGet will always bypass the cache and pull the topic directly from the database func (mts *DefaultTopicStore) BypassGet(id int) (*Topic, error) { topic := &Topic{ID: id} - err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data) + err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.LastReplyID, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.AttachCount, &topic.Poll, &topic.Data) topic.Link = BuildTopicURL(NameToSlug(topic.Title), id) return topic, err } func (mts *DefaultTopicStore) Reload(id int) error { topic := &Topic{ID: id} - err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.Poll, &topic.Data) + err := mts.get.QueryRow(id).Scan(&topic.Title, &topic.Content, &topic.CreatedBy, &topic.CreatedAt, &topic.LastReplyAt, &topic.LastReplyID, &topic.IsClosed, &topic.Sticky, &topic.ParentID, &topic.IPAddress, &topic.ViewCount, &topic.PostCount, &topic.LikeCount, &topic.AttachCount, &topic.Poll, &topic.Data) if err == nil { topic.Link = BuildTopicURL(NameToSlug(topic.Title), id) _ = mts.cache.Set(topic) diff --git a/common/utils.go b/common/utils.go index bd233a69..f4e1ccc9 100644 --- a/common/utils.go +++ b/common/utils.go @@ -202,6 +202,7 @@ func ConvertByteInUnit(bytes float64, unit string) (count float64) { } // TODO: Write a test for this +// TODO: Localise this? func FriendlyUnitToBytes(quantity int, unit string) (bytes int, err error) { switch unit { case "PB": @@ -323,7 +324,7 @@ func WeakPassword(password string, username string, email string) error { return errors.New("Your password needs to be at-least eight characters long") } - if strings.Contains(lowPassword, "test") || /*strings.Contains(password,"123456") || */ strings.Contains(password, "123") || strings.Contains(lowPassword, "password") || strings.Contains(lowPassword, "qwerty") || strings.Contains(lowPassword, "fuck") || strings.Contains(lowPassword, "love") { + if strings.Contains(lowPassword, "test") || strings.Contains(password, "123") || strings.Contains(lowPassword, "password") || strings.Contains(lowPassword, "qwerty") || strings.Contains(lowPassword, "fuck") || strings.Contains(lowPassword, "love") { return errors.New("You may not have 'test', '123', 'password', 'qwerty', 'love' or 'fuck' in your password") } diff --git a/common/ws_hub.go b/common/ws_hub.go index 6e2bbc7f..a20c9a2a 100644 --- a/common/ws_hub.go +++ b/common/ws_hub.go @@ -45,7 +45,7 @@ func (hub *WsHubImpl) Start() { AddScheduledSecondTask(hub.Tick) } -// This Tick is seperate from the admin one, as we want to process that in parallel with this due to the blocking calls to gopsutil +// This Tick is separate from the admin one, as we want to process that in parallel with this due to the blocking calls to gopsutil func (hub *WsHubImpl) Tick() error { // Don't waste CPU time if nothing has happened // TODO: Get a topic list method which strips stickies? diff --git a/dev-update-linux b/dev-update-linux index 9ec9939a..812117b2 100644 --- a/dev-update-linux +++ b/dev-update-linux @@ -2,8 +2,6 @@ echo "Updating the dependencies" go get echo "Updating Gosora" -rm ./schema/lastSchema.json -cp ./schema/schema.json ./schema/lastSchema.json git stash git pull origin master git stash apply diff --git a/dev-update-travis b/dev-update-travis index 90818d5f..32205d50 100644 --- a/dev-update-travis +++ b/dev-update-travis @@ -1,4 +1,3 @@ echo "Building the patcher" -cp ./schema/schema.json ./schema/lastSchema.json go generate go build -o Patcher "./patcher" \ No newline at end of file diff --git a/dev-update.bat b/dev-update.bat index 9ff3f59f..fa811d3b 100644 --- a/dev-update.bat +++ b/dev-update.bat @@ -8,10 +8,6 @@ if %errorlevel% neq 0 ( ) echo Updating Gosora -cd schema -del /Q lastSchema.json -copy schema.json lastSchema.json -cd .. git stash if %errorlevel% neq 0 ( pause diff --git a/docs/updating.md b/docs/updating.md index c2e61bbe..8253fd14 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -4,7 +4,7 @@ The update system is currently under development, but you can run `dev-update.ba If you run into any issues doing so, please open an issue: https://github.com/Azareal/Gosora/issues/new -If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first have to create a copy of `./schema/schema.json` named `./schema/lastSchema.json`, and then, you'll overwrite the files with the new ones with `git pull origin master`. +If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first want to save your changes with `git stash`, and then, you'll overwrite the files with the new ones with `git pull origin master`, and then, you can re-apply your custom changes with `git stash apply` After that, you'll need to run `go build ./patcher`. @@ -16,14 +16,9 @@ The update system is currently under development, but you can run `dev-update-li If you run into any issues doing so, please open an issue: https://github.com/Azareal/Gosora/issues/new -If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first have to create a copy of `./schema/schema.json` named `./schema/lastSchema.json`, and then, you'll overwrite the files with the new ones with `git pull origin master`. +If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first want to save your changes with `git stash`, and then, you'll overwrite the files with the new ones with `git pull origin master`, and then, you'll re-apply your changes with `git stash apply`. -After that, you'll need to run the following code block: -``` -cd ./patcher -go build -o Patcher -mv ./Patcher .. -``` +After that, you'll need to run `go build -o Patcher "./patcher"` Once you've done that, you just need to run `./Patcher` to apply the latest patches to the database, etc. @@ -46,14 +41,9 @@ Replace that name and email with whatever you like. This name and email only app If you get an access denied error, then you might need to run `chown -R gosora /home/gosora` and `chgrp -R www-data /home/gosora` to fix the ownership of the files. -If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first have to create a copy of `./schema/schema.json` named `./schema/lastSchema.json`, and then, you'll overwrite the files with the new ones with `git pull origin master`. +If you want to manually patch Gosora rather than relying on the above scripts to do it, you'll first want to save your changes with `git stash`, and then, you'll overwrite the files with the new ones with `git pull origin master`, and then, you'll re-apply your changes with `git stash apply`. -After that, you'll need to run: -``` -cd ./patcher -go build -o Patcher -mv ./Patcher .. -``` +After that, you'll need to run `go build -o Patcher "./patcher"` Once you've done that, you just need to run `./Patcher` to apply the latest patches to the database, etc. diff --git a/gen_router.go b/gen_router.go index bdc7e98c..9555b05b 100644 --- a/gen_router.go +++ b/gen_router.go @@ -128,6 +128,8 @@ var RouteMap = map[string]interface{}{ "routes.UnlockTopicSubmit": routes.UnlockTopicSubmit, "routes.MoveTopicSubmit": routes.MoveTopicSubmit, "routes.LikeTopicSubmit": routes.LikeTopicSubmit, + "routes.AddAttachToTopicSubmit": routes.AddAttachToTopicSubmit, + "routes.RemoveAttachFromTopicSubmit": routes.RemoveAttachFromTopicSubmit, "routes.ViewTopic": routes.ViewTopic, "routes.CreateReplySubmit": routes.CreateReplySubmit, "routes.ReplyEditSubmit": routes.ReplyEditSubmit, @@ -261,29 +263,31 @@ var routeMapEnum = map[string]int{ "routes.UnlockTopicSubmit": 103, "routes.MoveTopicSubmit": 104, "routes.LikeTopicSubmit": 105, - "routes.ViewTopic": 106, - "routes.CreateReplySubmit": 107, - "routes.ReplyEditSubmit": 108, - "routes.ReplyDeleteSubmit": 109, - "routes.ReplyLikeSubmit": 110, - "routes.ProfileReplyCreateSubmit": 111, - "routes.ProfileReplyEditSubmit": 112, - "routes.ProfileReplyDeleteSubmit": 113, - "routes.PollVote": 114, - "routes.PollResults": 115, - "routes.AccountLogin": 116, - "routes.AccountRegister": 117, - "routes.AccountLogout": 118, - "routes.AccountLoginSubmit": 119, - "routes.AccountLoginMFAVerify": 120, - "routes.AccountLoginMFAVerifySubmit": 121, - "routes.AccountRegisterSubmit": 122, - "routes.DynamicRoute": 123, - "routes.UploadedFile": 124, - "routes.StaticFile": 125, - "routes.RobotsTxt": 126, - "routes.SitemapXml": 127, - "routes.BadRoute": 128, + "routes.AddAttachToTopicSubmit": 106, + "routes.RemoveAttachFromTopicSubmit": 107, + "routes.ViewTopic": 108, + "routes.CreateReplySubmit": 109, + "routes.ReplyEditSubmit": 110, + "routes.ReplyDeleteSubmit": 111, + "routes.ReplyLikeSubmit": 112, + "routes.ProfileReplyCreateSubmit": 113, + "routes.ProfileReplyEditSubmit": 114, + "routes.ProfileReplyDeleteSubmit": 115, + "routes.PollVote": 116, + "routes.PollResults": 117, + "routes.AccountLogin": 118, + "routes.AccountRegister": 119, + "routes.AccountLogout": 120, + "routes.AccountLoginSubmit": 121, + "routes.AccountLoginMFAVerify": 122, + "routes.AccountLoginMFAVerifySubmit": 123, + "routes.AccountRegisterSubmit": 124, + "routes.DynamicRoute": 125, + "routes.UploadedFile": 126, + "routes.StaticFile": 127, + "routes.RobotsTxt": 128, + "routes.SitemapXml": 129, + "routes.BadRoute": 130, } var reverseRouteMapEnum = map[int]string{ 0: "routes.Overview", @@ -392,29 +396,31 @@ var reverseRouteMapEnum = map[int]string{ 103: "routes.UnlockTopicSubmit", 104: "routes.MoveTopicSubmit", 105: "routes.LikeTopicSubmit", - 106: "routes.ViewTopic", - 107: "routes.CreateReplySubmit", - 108: "routes.ReplyEditSubmit", - 109: "routes.ReplyDeleteSubmit", - 110: "routes.ReplyLikeSubmit", - 111: "routes.ProfileReplyCreateSubmit", - 112: "routes.ProfileReplyEditSubmit", - 113: "routes.ProfileReplyDeleteSubmit", - 114: "routes.PollVote", - 115: "routes.PollResults", - 116: "routes.AccountLogin", - 117: "routes.AccountRegister", - 118: "routes.AccountLogout", - 119: "routes.AccountLoginSubmit", - 120: "routes.AccountLoginMFAVerify", - 121: "routes.AccountLoginMFAVerifySubmit", - 122: "routes.AccountRegisterSubmit", - 123: "routes.DynamicRoute", - 124: "routes.UploadedFile", - 125: "routes.StaticFile", - 126: "routes.RobotsTxt", - 127: "routes.SitemapXml", - 128: "routes.BadRoute", + 106: "routes.AddAttachToTopicSubmit", + 107: "routes.RemoveAttachFromTopicSubmit", + 108: "routes.ViewTopic", + 109: "routes.CreateReplySubmit", + 110: "routes.ReplyEditSubmit", + 111: "routes.ReplyDeleteSubmit", + 112: "routes.ReplyLikeSubmit", + 113: "routes.ProfileReplyCreateSubmit", + 114: "routes.ProfileReplyEditSubmit", + 115: "routes.ProfileReplyDeleteSubmit", + 116: "routes.PollVote", + 117: "routes.PollResults", + 118: "routes.AccountLogin", + 119: "routes.AccountRegister", + 120: "routes.AccountLogout", + 121: "routes.AccountLoginSubmit", + 122: "routes.AccountLoginMFAVerify", + 123: "routes.AccountLoginMFAVerifySubmit", + 124: "routes.AccountRegisterSubmit", + 125: "routes.DynamicRoute", + 126: "routes.UploadedFile", + 127: "routes.StaticFile", + 128: "routes.RobotsTxt", + 129: "routes.SitemapXml", + 130: "routes.BadRoute", } var osMapEnum = map[string]int{ "unknown": 0, @@ -705,7 +711,7 @@ func (r *GenRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { counters.GlobalViewCounter.Bump() if prefix == "/static" { - counters.RouteViewCounter.Bump(125) + counters.RouteViewCounter.Bump(127) req.URL.Path += extraData routes.StaticFile(w, req) return @@ -1780,15 +1786,40 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - err = common.ParseForm(w,req,user) + counters.RouteViewCounter.Bump(105) + err = routes.LikeTopicSubmit(w,req,user,extraData) + case "/topic/attach/add/submit/": + err = common.MemberOnly(w,req,user) + if err != nil { + return err + } + + err = common.HandleUploadRoute(w,req,user,int(common.Config.MaxRequestSize)) + if err != nil { + return err + } + err = common.NoUploadSessionMismatch(w,req,user) if err != nil { return err } - counters.RouteViewCounter.Bump(105) - err = routes.LikeTopicSubmit(w,req,user,extraData) - default: counters.RouteViewCounter.Bump(106) + err = routes.AddAttachToTopicSubmit(w,req,user,extraData) + case "/topic/attach/remove/submit/": + err = common.NoSessionMismatch(w,req,user) + if err != nil { + return err + } + + err = common.MemberOnly(w,req,user) + if err != nil { + return err + } + + counters.RouteViewCounter.Bump(107) + err = routes.RemoveAttachFromTopicSubmit(w,req,user,extraData) + default: + counters.RouteViewCounter.Bump(108) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1812,7 +1843,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(107) + counters.RouteViewCounter.Bump(109) err = routes.CreateReplySubmit(w,req,user) case "/reply/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1825,7 +1856,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(108) + counters.RouteViewCounter.Bump(110) err = routes.ReplyEditSubmit(w,req,user,extraData) case "/reply/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1838,7 +1869,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(109) + counters.RouteViewCounter.Bump(111) err = routes.ReplyDeleteSubmit(w,req,user,extraData) case "/reply/like/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1851,12 +1882,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - err = common.ParseForm(w,req,user) - if err != nil { - return err - } - - counters.RouteViewCounter.Bump(110) + counters.RouteViewCounter.Bump(112) err = routes.ReplyLikeSubmit(w,req,user,extraData) } case "/profile": @@ -1872,7 +1898,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(111) + counters.RouteViewCounter.Bump(113) err = routes.ProfileReplyCreateSubmit(w,req,user) case "/profile/reply/edit/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1885,7 +1911,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(112) + counters.RouteViewCounter.Bump(114) err = routes.ProfileReplyEditSubmit(w,req,user,extraData) case "/profile/reply/delete/submit/": err = common.NoSessionMismatch(w,req,user) @@ -1898,7 +1924,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(113) + counters.RouteViewCounter.Bump(115) err = routes.ProfileReplyDeleteSubmit(w,req,user,extraData) } case "/poll": @@ -1914,23 +1940,23 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(114) + counters.RouteViewCounter.Bump(116) err = routes.PollVote(w,req,user,extraData) case "/poll/results/": - counters.RouteViewCounter.Bump(115) + counters.RouteViewCounter.Bump(117) err = routes.PollResults(w,req,user,extraData) } case "/accounts": switch(req.URL.Path) { case "/accounts/login/": - counters.RouteViewCounter.Bump(116) + counters.RouteViewCounter.Bump(118) head, err := common.UserCheck(w,req,&user) if err != nil { return err } err = routes.AccountLogin(w,req,user,head) case "/accounts/create/": - counters.RouteViewCounter.Bump(117) + counters.RouteViewCounter.Bump(119) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1947,7 +1973,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(118) + counters.RouteViewCounter.Bump(120) err = routes.AccountLogout(w,req,user) case "/accounts/login/submit/": err = common.ParseForm(w,req,user) @@ -1955,10 +1981,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(119) + counters.RouteViewCounter.Bump(121) err = routes.AccountLoginSubmit(w,req,user) case "/accounts/mfa_verify/": - counters.RouteViewCounter.Bump(120) + counters.RouteViewCounter.Bump(122) head, err := common.UserCheck(w,req,&user) if err != nil { return err @@ -1970,7 +1996,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(121) + counters.RouteViewCounter.Bump(123) err = routes.AccountLoginMFAVerifySubmit(w,req,user) case "/accounts/create/submit/": err = common.ParseForm(w,req,user) @@ -1978,7 +2004,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c return err } - counters.RouteViewCounter.Bump(122) + counters.RouteViewCounter.Bump(124) err = routes.AccountRegisterSubmit(w,req,user) } /*case "/sitemaps": // TODO: Count these views @@ -1994,7 +2020,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c w.Header().Del("Content-Type") w.Header().Del("Content-Encoding") } - counters.RouteViewCounter.Bump(124) + counters.RouteViewCounter.Bump(126) req.URL.Path += extraData // TODO: Find a way to propagate errors up from this? r.UploadHandler(w,req) // TODO: Count these views @@ -2004,10 +2030,10 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c // TODO: Add support for favicons and robots.txt files switch(extraData) { case "robots.txt": - counters.RouteViewCounter.Bump(126) + counters.RouteViewCounter.Bump(128) return routes.RobotsTxt(w,req) /*case "sitemap.xml": - counters.RouteViewCounter.Bump(127) + counters.RouteViewCounter.Bump(129) return routes.SitemapXml(w,req)*/ } return common.NotFound(w,req,nil) @@ -2018,7 +2044,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c r.RUnlock() if ok { - counters.RouteViewCounter.Bump(123) // TODO: Be more specific about *which* dynamic route it is + counters.RouteViewCounter.Bump(125) // TODO: Be more specific about *which* dynamic route it is req.URL.Path += extraData return handle(w,req,user) } @@ -2029,7 +2055,7 @@ func (r *GenRouter) routeSwitch(w http.ResponseWriter, req *http.Request, user c } else { r.DumpRequest(req,"Bad Route") } - counters.RouteViewCounter.Bump(128) + counters.RouteViewCounter.Bump(130) return common.NotFound(w,req,nil) } return err diff --git a/misc_test.go b/misc_test.go index c93150d7..8b180a15 100644 --- a/misc_test.go +++ b/misc_test.go @@ -92,7 +92,7 @@ func userStoreTest(t *testing.T, newUserID int) { expect(t, cond, prefix+" "+midfix+" "+suffix) } - // TODO: Add email checks too? Do them seperately? + // TODO: Add email checks too? Do them separately? var expectUser = func(user *common.User, uid int, name string, group int, super bool, admin bool, mod bool, banned bool) { expect(t, user.ID == uid, fmt.Sprintf("user.ID should be %d. Got '%d' instead.", uid, user.ID)) expect(t, user.Name == name, fmt.Sprintf("user.Name should be '%s', not '%s'", name, user.Name)) diff --git a/patcher/main.go b/patcher/main.go index 46e695c7..08ff1279 100644 --- a/patcher/main.go +++ b/patcher/main.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "database/sql" "encoding/json" "fmt" "io/ioutil" @@ -89,20 +90,29 @@ type SchemaFile struct { MinVersion string // TODO: Minimum version of Gosora to jump to this version, might be tricky as we don't store this in the schema file, maybe store it in the database } -func patcher(scanner *bufio.Scanner) error { +func loadSchema() (schemaFile SchemaFile, err error) { fmt.Println("Loading the schema file") data, err := ioutil.ReadFile("./schema/lastSchema.json") if err != nil { - return err + return schemaFile, err } - - var schemaFile SchemaFile err = json.Unmarshal(data, &schemaFile) - if err != nil { - return err - } - dbVersion, err := strconv.Atoi(schemaFile.DBVersion) - if err != nil { + return schemaFile, err +} + +func patcher(scanner *bufio.Scanner) error { + var dbVersion int + err := qgen.NewAcc().Select("updates").Columns("dbVersion").QueryRow().Scan(&dbVersion) + if err == sql.ErrNoRows { + schemaFile, err := loadSchema() + if err != nil { + return err + } + dbVersion, err = strconv.Atoi(schemaFile.DBVersion) + if err != nil { + return err + } + } else if err != nil { return err } @@ -113,6 +123,7 @@ func patcher(scanner *bufio.Scanner) error { } // Run the queued up patches + var patched int for index, patch := range pslice { if dbVersion > index { continue @@ -121,6 +132,14 @@ func patcher(scanner *bufio.Scanner) error { if err != nil { return err } + patched++ + } + + if patched > 0 { + _, err := qgen.NewAcc().Update("updates").Set("dbVersion = ?").Exec(len(pslice)) + if err != nil { + return err + } } return nil diff --git a/patcher/patches.go b/patcher/patches.go index 03930ac0..54a4f79e 100644 --- a/patcher/patches.go +++ b/patcher/patches.go @@ -7,6 +7,9 @@ import ( "github.com/Azareal/Gosora/query_gen" ) +type tblColumn = qgen.DBTableColumn +type tblKey = qgen.DBTableKey + func init() { addPatch(0, patch0) addPatch(1, patch1) @@ -18,6 +21,7 @@ func init() { addPatch(7, patch7) addPatch(8, patch8) addPatch(9, patch9) + addPatch(10, patch10) } func patch0(scanner *bufio.Scanner) (err error) { @@ -32,11 +36,11 @@ func patch0(scanner *bufio.Scanner) (err error) { } err = execStmt(qgen.Builder.CreateTable("menus", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"mid", "int", 0, false, true, ""}, + []tblColumn{ + tblColumn{"mid", "int", 0, false, true, ""}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"mid", "primary"}, + []tblKey{ + tblKey{"mid", "primary"}, }, )) if err != nil { @@ -44,26 +48,26 @@ func patch0(scanner *bufio.Scanner) (err error) { } err = execStmt(qgen.Builder.CreateTable("menu_items", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"miid", "int", 0, false, true, ""}, - qgen.DBTableColumn{"mid", "int", 0, false, false, ""}, - qgen.DBTableColumn{"name", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"htmlID", "varchar", 200, false, false, "''"}, - qgen.DBTableColumn{"cssClass", "varchar", 200, false, false, "''"}, - qgen.DBTableColumn{"position", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"path", "varchar", 200, false, false, "''"}, - qgen.DBTableColumn{"aria", "varchar", 200, false, false, "''"}, - qgen.DBTableColumn{"tooltip", "varchar", 200, false, false, "''"}, - qgen.DBTableColumn{"tmplName", "varchar", 200, false, false, "''"}, - qgen.DBTableColumn{"order", "int", 0, false, false, "0"}, + []tblColumn{ + tblColumn{"miid", "int", 0, false, true, ""}, + tblColumn{"mid", "int", 0, false, false, ""}, + tblColumn{"name", "varchar", 200, false, false, ""}, + tblColumn{"htmlID", "varchar", 200, false, false, "''"}, + tblColumn{"cssClass", "varchar", 200, false, false, "''"}, + tblColumn{"position", "varchar", 100, false, false, ""}, + tblColumn{"path", "varchar", 200, false, false, "''"}, + tblColumn{"aria", "varchar", 200, false, false, "''"}, + tblColumn{"tooltip", "varchar", 200, false, false, "''"}, + tblColumn{"tmplName", "varchar", 200, false, false, "''"}, + tblColumn{"order", "int", 0, false, false, "0"}, - qgen.DBTableColumn{"guestOnly", "boolean", 0, false, false, "0"}, - qgen.DBTableColumn{"memberOnly", "boolean", 0, false, false, "0"}, - qgen.DBTableColumn{"staffOnly", "boolean", 0, false, false, "0"}, - qgen.DBTableColumn{"adminOnly", "boolean", 0, false, false, "0"}, + tblColumn{"guestOnly", "boolean", 0, false, false, "0"}, + tblColumn{"memberOnly", "boolean", 0, false, false, "0"}, + tblColumn{"staffOnly", "boolean", 0, false, false, "0"}, + tblColumn{"adminOnly", "boolean", 0, false, false, "0"}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"miid", "primary"}, + []tblKey{ + tblKey{"miid", "primary"}, }, )) if err != nil { @@ -159,25 +163,20 @@ func patch2(scanner *bufio.Scanner) error { } func patch3(scanner *bufio.Scanner) error { - err := execStmt(qgen.Builder.CreateTable("registration_logs", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"rlid", "int", 0, false, true, ""}, - qgen.DBTableColumn{"username", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"email", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"failureReason", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed? - qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"doneAt", "createdAt", 0, false, false, ""}, + return execStmt(qgen.Builder.CreateTable("registration_logs", "", "", + []tblColumn{ + tblColumn{"rlid", "int", 0, false, true, ""}, + tblColumn{"username", "varchar", 100, false, false, ""}, + tblColumn{"email", "varchar", 100, false, false, ""}, + tblColumn{"failureReason", "varchar", 100, false, false, ""}, + tblColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed? + tblColumn{"ipaddress", "varchar", 200, false, false, ""}, + tblColumn{"doneAt", "createdAt", 0, false, false, ""}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"rlid", "primary"}, + []tblKey{ + tblKey{"rlid", "primary"}, }, )) - if err != nil { - return err - } - - return nil } func patch4(scanner *bufio.Scanner) error { @@ -229,16 +228,16 @@ func patch4(scanner *bufio.Scanner) error { } err = execStmt(qgen.Builder.CreateTable("pages", "utf8mb4", "utf8mb4_general_ci", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"pid", "int", 0, false, true, ""}, - qgen.DBTableColumn{"name", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"title", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"body", "text", 0, false, false, ""}, - qgen.DBTableColumn{"allowedGroups", "text", 0, false, false, ""}, - qgen.DBTableColumn{"menuID", "int", 0, false, false, "-1"}, + []tblColumn{ + tblColumn{"pid", "int", 0, false, true, ""}, + tblColumn{"name", "varchar", 200, false, false, ""}, + tblColumn{"title", "varchar", 200, false, false, ""}, + tblColumn{"body", "text", 0, false, false, ""}, + tblColumn{"allowedGroups", "text", 0, false, false, ""}, + tblColumn{"menuID", "int", 0, false, false, "-1"}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"pid", "primary"}, + []tblKey{ + tblKey{"pid", "primary"}, }, )) if err != nil { @@ -267,21 +266,21 @@ func patch5(scanner *bufio.Scanner) error { } err = execStmt(qgen.Builder.CreateTable("users_2fa_keys", "utf8mb4", "utf8mb4_general_ci", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, - qgen.DBTableColumn{"secret", "varchar", 100, false, false, ""}, - qgen.DBTableColumn{"scratch1", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"scratch2", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"scratch3", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"scratch4", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"scratch5", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"scratch6", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"scratch7", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"scratch8", "varchar", 50, false, false, ""}, - qgen.DBTableColumn{"createdAt", "createdAt", 0, false, false, ""}, + []tblColumn{ + tblColumn{"uid", "int", 0, false, false, ""}, + tblColumn{"secret", "varchar", 100, false, false, ""}, + tblColumn{"scratch1", "varchar", 50, false, false, ""}, + tblColumn{"scratch2", "varchar", 50, false, false, ""}, + tblColumn{"scratch3", "varchar", 50, false, false, ""}, + tblColumn{"scratch4", "varchar", 50, false, false, ""}, + tblColumn{"scratch5", "varchar", 50, false, false, ""}, + tblColumn{"scratch6", "varchar", 50, false, false, ""}, + tblColumn{"scratch7", "varchar", 50, false, false, ""}, + tblColumn{"scratch8", "varchar", 50, false, false, ""}, + tblColumn{"createdAt", "createdAt", 0, false, false, ""}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"uid", "primary"}, + []tblKey{ + tblKey{"uid", "primary"}, }, )) if err != nil { @@ -292,28 +291,18 @@ func patch5(scanner *bufio.Scanner) error { } func patch6(scanner *bufio.Scanner) error { - err := execStmt(qgen.Builder.SimpleInsert("settings", "name, content, type", "'rapid_loading','1','bool'")) - if err != nil { - return err - } - - return nil + return execStmt(qgen.Builder.SimpleInsert("settings", "name, content, type", "'rapid_loading','1','bool'")) } func patch7(scanner *bufio.Scanner) error { - err := execStmt(qgen.Builder.CreateTable("users_avatar_queue", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key + return execStmt(qgen.Builder.CreateTable("users_avatar_queue", "", "", + []tblColumn{ + tblColumn{"uid", "int", 0, false, false, ""}, // TODO: Make this a foreign key }, - []qgen.DBTableKey{ - qgen.DBTableKey{"uid", "primary"}, + []tblKey{ + tblKey{"uid", "primary"}, }, )) - if err != nil { - return err - } - - return nil } func renameRoutes(routes map[string]string) error { @@ -369,17 +358,12 @@ func patch8(scanner *bufio.Scanner) error { if err != nil { return err } - err = execStmt(qgen.Builder.CreateTable("updates", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"dbVersion", "int", 0, false, false, "0"}, + return execStmt(qgen.Builder.CreateTable("updates", "", "", + []tblColumn{ + tblColumn{"dbVersion", "int", 0, false, false, "0"}, }, - []qgen.DBTableKey{}, + []tblKey{}, )) - if err != nil { - return err - } - - return nil } func patch9(scanner *bufio.Scanner) error { @@ -389,21 +373,60 @@ func patch9(scanner *bufio.Scanner) error { return err } - err = execStmt(qgen.Builder.CreateTable("login_logs", "", "", - []qgen.DBTableColumn{ - qgen.DBTableColumn{"lid", "int", 0, false, true, ""}, - qgen.DBTableColumn{"uid", "int", 0, false, false, ""}, - qgen.DBTableColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed? - qgen.DBTableColumn{"ipaddress", "varchar", 200, false, false, ""}, - qgen.DBTableColumn{"doneAt", "createdAt", 0, false, false, ""}, + return execStmt(qgen.Builder.CreateTable("login_logs", "", "", + []tblColumn{ + tblColumn{"lid", "int", 0, false, true, ""}, + tblColumn{"uid", "int", 0, false, false, ""}, + tblColumn{"success", "bool", 0, false, false, "0"}, // Did this attempt succeed? + tblColumn{"ipaddress", "varchar", 200, false, false, ""}, + tblColumn{"doneAt", "createdAt", 0, false, false, ""}, }, - []qgen.DBTableKey{ - qgen.DBTableKey{"lid", "primary"}, + []tblKey{ + tblKey{"lid", "primary"}, }, )) +} + +var acc = qgen.NewAcc +var itoa = strconv.Itoa + +func patch10(scanner *bufio.Scanner) error { + err := execStmt(qgen.Builder.AddColumn("topics", tblColumn{"attachCount", "int", 0, false, false, "0"})) + if err != nil { + return err + } + err = execStmt(qgen.Builder.AddColumn("topics", tblColumn{"lastReplyID", "int", 0, false, false, "0"})) if err != nil { return err } - return nil + // We could probably do something more efficient, but as there shouldn't be too many sites right now, we can probably cheat a little, otherwise it'll take forever to get things done + err = acc().Select("topics").Cols("tid").EachInt(func(tid int) error { + stid := itoa(tid) + + count, err := acc().Count("attachments").Where("originTable = 'topics' and originID = " + stid).Total() + if err != nil { + return err + } + + var hasReply = false + err = acc().Select("replies").Cols("rid").Where("tid = " + stid).Orderby("rid DESC").Limit("1").EachInt(func(rid int) error { + hasReply = true + _, err := acc().Update("topics").Set("lastReplyID = ?, attachCount = ?").Where("tid = "+stid).Exec(rid, count) + return err + }) + if err != nil { + return err + } + if !hasReply { + _, err = acc().Update("topics").Set("attachCount = ?").Where("tid = " + stid).Exec(count) + } + return err + }) + if err != nil { + return err + } + + _, err = acc().Insert("updates").Columns("dbVersion").Fields("0").Exec() + return err } diff --git a/public/global.js b/public/global.js index 6abe9dd4..b93b380e 100644 --- a/public/global.js +++ b/public/global.js @@ -224,11 +224,8 @@ function runWebSockets() { // TODO: Add support for other alert feeds like PM Alerts var generalAlerts = document.getElementById("general_alerts"); - if(alertList.length < 8) { - loadAlerts(generalAlerts); - } else { - updateAlertList(generalAlerts); - } + if(alertList.length < 8) loadAlerts(generalAlerts); + else updateAlertList(generalAlerts); } }); } @@ -374,6 +371,7 @@ function mainInit(){ event.preventDefault(); $('.hide_on_edit').addClass("edit_opened"); $('.show_on_edit').addClass("edit_opened"); + runHook("open_edit"); }); $(".topic_item .submit_edit").click(function(event){ @@ -388,6 +386,7 @@ function mainInit(){ $('.hide_on_edit').removeClass("edit_opened"); $('.show_on_edit').removeClass("edit_opened"); + runHook("close_edit"); let formAction = this.form.getAttribute("action"); $.ajax({ @@ -566,74 +565,185 @@ function mainInit(){ $(".topic_create_form").hide(); }); - function uploadFileHandler() { - var fileList = this.files; - // Truncate the number of files to 5 + function uploadFileHandler(fileList,maxFiles = 5, step1 = () => {}, step2 = () => {}) { let files = []; for(var i = 0; i < fileList.length && i < 5; i++) { files[i] = fileList[i]; } - // Iterate over the files let totalSize = 0; for(let i = 0; i < files.length; i++) { console.log("files[" + i + "]",files[i]); totalSize += files[i]["size"]; + } + if(totalSize > me.Site.MaxRequestSize) { + throw("You can't upload this much at once, max: " + me.Site.MaxRequestSize); + } + for(let i = 0; i < files.length; i++) { let reader = new FileReader(); - reader.onload = function(e) { - var fileDock = document.getElementById("upload_file_dock"); - var fileItem = document.createElement("label"); - console.log("fileItem",fileItem); - - if(!files[i]["name"].indexOf('.' > -1)) { - // TODO: Surely, there's a prettier and more elegant way of doing this? - alert("This file doesn't have an extension"); - return; - } - - var ext = files[i]["name"].split('.').pop(); - fileItem.innerText = "." + ext; - fileItem.className = "formbutton uploadItem"; - fileItem.style.backgroundImage = "url("+e.target.result+")"; - - fileDock.appendChild(fileItem); + reader.onload = (e) => { + let filename = files[i]["name"]; + step1(e,filename) let reader = new FileReader(); - reader.onload = function(e) { - crypto.subtle.digest('SHA-256',e.target.result) - .then(function(hash) { + reader.onload = (e2) => { + crypto.subtle.digest('SHA-256',e2.target.result) + .then((hash) => { const hashArray = Array.from(new Uint8Array(hash)) return hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('') - }).then(function(hash) { - console.log("hash",hash); - let content = document.getElementById("input_content") - console.log("content.value", content.value); - - let attachItem; - if(content.value == "") attachItem = "//" + window.location.host + "/attachs/" + hash + "." + ext; - else attachItem = "\r\n//" + window.location.host + "/attachs/" + hash + "." + ext; - content.value = content.value + attachItem; - console.log("content.value", content.value); - - // For custom / third party text editors - attachItemCallback(attachItem); - }); + }).then(hash => step2(e,hash,filename)); } reader.readAsArrayBuffer(files[i]); } reader.readAsDataURL(files[i]); } - if(totalSize > me.Site.MaxRequestSize) { + } + + // TODO: Surely, there's a prettier and more elegant way of doing this? + function getExt(filename) { + if(!filename.indexOf('.' > -1)) { + throw("This file doesn't have an extension"); + } + return filename.split('.').pop(); + } + + // Attachment Manager + function uploadAttachHandler2() { + let fileDock = this.closest(".attach_edit_bay"); + try { + uploadFileHandler(this.files, 5, () => {}, + (e, hash, filename) => { + console.log("hash",hash); + + let formData = new FormData(); + formData.append("session",me.User.Session); + for(let i = 0; i < this.files.length; i++) { + formData.append("upload_files",this.files[i]); + } + + let req = new XMLHttpRequest(); + req.addEventListener("load", () => { + let data = JSON.parse(req.responseText); + let fileItem = document.createElement("div"); + let ext = getExt(filename); + // TODO: Check if this is actually an image, maybe push ImageFileExts to the client from the server in some sort of gen.js? + // TODO: Use client templates here + fileItem.className = "attach_item attach_image_holder"; + fileItem.innerHTML = ""+hash+"."+ext+""; + fileDock.insertBefore(fileItem,fileDock.querySelector(".attach_item_buttons")); + + $(".attach_item_select").unbind("click"); + $(".attach_item_copy").unbind("click"); + bindAttachItems() + }); + req.open("POST","//"+window.location.host+"/topic/attach/add/submit/"+fileDock.getAttribute("tid")); + req.send(formData); + }); + } catch(e) { // TODO: Use a notice instead - alert("You can't upload this much data at once, max: " + me.Site.MaxRequestSize); + alert(e); + } + } + + // Quick Topic / Quick Reply + function uploadAttachHandler() { + try { + uploadFileHandler(this.files,5,(e,filename) => { + // TODO: Use client templates here + let fileDock = document.getElementById("upload_file_dock"); + let fileItem = document.createElement("label"); + console.log("fileItem",fileItem); + + let ext = getExt(filename) + fileItem.innerText = "." + ext; + fileItem.className = "formbutton uploadItem"; + // TODO: Check if this is actually an image + fileItem.style.backgroundImage = "url("+e.target.result+")"; + + fileDock.appendChild(fileItem); + },(e,hash, filename) => { + console.log("hash",hash); + let ext = getExt(filename) + let content = document.getElementById("input_content") + console.log("content.value", content.value); + + let attachItem; + if(content.value == "") attachItem = "//" + window.location.host + "/attachs/" + hash + "." + ext; + else attachItem = "\r\n//" + window.location.host + "/attachs/" + hash + "." + ext; + content.value = content.value + attachItem; + console.log("content.value", content.value); + + // For custom / third party text editors + attachItemCallback(attachItem); + }); + } catch(e) { + // TODO: Use a notice instead + alert(e); } } var uploadFiles = document.getElementById("upload_files"); if(uploadFiles != null) { - uploadFiles.addEventListener("change", uploadFileHandler, false); + uploadFiles.addEventListener("change", uploadAttachHandler, false); } + var uploadFilesOp = document.getElementById("upload_files_op"); + if(uploadFilesOp != null) { + uploadFilesOp.addEventListener("change", uploadAttachHandler2, false); + } + + function copyToClipboard(str) { + const el = document.createElement('textarea'); + el.value = str; + el.setAttribute('readonly', ''); + el.style.position = 'absolute'; + el.style.left = '-9999px'; + document.body.appendChild(el); + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); + } + + function bindAttachItems() { + $(".attach_item_select").click(function(){ + let hold = $(this).closest(".attach_item"); + if(hold.hasClass("attach_item_selected")) { + hold.removeClass("attach_item_selected"); + } else { + hold.addClass("attach_item_selected"); + } + }); + $(".attach_item_copy").click(function(){ + let hold = $(this).closest(".attach_item"); + let pathNode = hold.find(".attach_item_path"); + copyToClipboard(pathNode.attr("fullPath")); + }); + } + bindAttachItems(); + + $(".attach_item_delete").click(function(){ + let formData = new URLSearchParams(); + formData.append("session",me.User.Session); + + let aidList = ""; + let elems = document.getElementsByClassName("attach_item_selected"); + if(elems == null) return; + + for(let i = 0; i < elems.length; i++) { + let pathNode = elems[i].querySelector(".attach_item_path"); + console.log("pathNode",pathNode); + aidList += pathNode.getAttribute("aid") + ","; + elems[i].remove(); + } + if(aidList.length > 0) aidList = aidList.slice(0, -1); + console.log("aidList",aidList) + formData.append("aids",aidList); + + let req = new XMLHttpRequest(); + let fileDock = this.closest(".attach_edit_bay"); + req.open("POST","//"+window.location.host+"/topic/attach/remove/submit/"+fileDock.getAttribute("tid"),true); + req.send(formData); + }); $(".moderate_link").click((event) => { event.preventDefault(); @@ -643,10 +753,11 @@ function mainInit(){ $(this).click(function(){ selectedTopics.push(parseInt($(this).attr("data-tid"),10)); if(selectedTopics.length==1) { - $(".mod_floater_head span").html("What do you want to do with this topic?"); + var msg = "What do you want to do with this topic?"; } else { - $(".mod_floater_head span").html("What do you want to do with these "+selectedTopics.length+" topics?"); + var msg = "What do you want to do with these "+selectedTopics.length+" topics?"; } + $(".mod_floater_head span").html(msg); $(this).addClass("topic_selected"); $(".mod_floater").removeClass("auto_hide"); }); @@ -670,7 +781,6 @@ function mainInit(){ let selectNode = this.form.querySelector(".mod_floater_options"); let optionNode = selectNode.options[selectNode.selectedIndex]; let action = optionNode.getAttribute("val"); - //console.log("action", action); // Handle these specially switch(action) { diff --git a/public/init.js b/public/init.js index 756e4879..1f99ffc7 100644 --- a/public/init.js +++ b/public/init.js @@ -12,6 +12,8 @@ var hooks = { "after_phrases":[], "after_add_alert":[], "after_update_alert_list":[], + "open_edit":[], + "close_edit":[], }; var ranInitHooks = {} @@ -130,7 +132,7 @@ function fetchPhrases() { (() => { runInitHook("pre_iife"); let loggedIn = document.head.querySelector("[property='x-loggedin']").content; - if(loggedIn) { + if(loggedIn=="true") { fetch("/api/me/") .then((resp) => resp.json()) .then((data) => { diff --git a/query_gen/acc_builders.go b/query_gen/acc_builders.go index 0431b1bf..083676d2 100644 --- a/query_gen/acc_builders.go +++ b/query_gen/acc_builders.go @@ -40,28 +40,41 @@ func (builder *accDeleteBuilder) Run(args ...interface{}) (int, error) { } type accUpdateBuilder struct { - table string - set string - where string - + up *updatePrebuilder build *Accumulator } func (update *accUpdateBuilder) Set(set string) *accUpdateBuilder { - update.set = set + update.up.set = set return update } func (update *accUpdateBuilder) Where(where string) *accUpdateBuilder { - if update.where != "" { - update.where += " AND " + if update.up.where != "" { + update.up.where += " AND " } - update.where += where + update.up.where += where return update } -func (update *accUpdateBuilder) Prepare() *sql.Stmt { - return update.build.SimpleUpdate(update.table, update.set, update.where) +func (update *accUpdateBuilder) WhereQ(sel *selectPrebuilder) *accUpdateBuilder { + update.up.whereSubQuery = sel + return update +} + +func (builder *accUpdateBuilder) Prepare() *sql.Stmt { + if builder.up.whereSubQuery != nil { + return builder.build.prepare(builder.build.adapter.SimpleUpdateSelect(builder.up)) + } + return builder.build.prepare(builder.build.adapter.SimpleUpdate(builder.up)) +} + +func (builder *accUpdateBuilder) Exec(args ...interface{}) (res sql.Result, err error) { + query, err := builder.build.adapter.SimpleUpdate(builder.up) + if err != nil { + return res, err + } + return builder.build.exec(query, args...) } type AccSelectBuilder struct { @@ -77,17 +90,22 @@ type AccSelectBuilder struct { build *Accumulator } -func (selectItem *AccSelectBuilder) Columns(columns string) *AccSelectBuilder { - selectItem.columns = columns - return selectItem +func (builder *AccSelectBuilder) Columns(columns string) *AccSelectBuilder { + builder.columns = columns + return builder } -func (selectItem *AccSelectBuilder) Where(where string) *AccSelectBuilder { - if selectItem.where != "" { - selectItem.where += " AND " +func (builder *AccSelectBuilder) Cols(columns string) *AccSelectBuilder { + builder.columns = columns + return builder +} + +func (builder *AccSelectBuilder) Where(where string) *AccSelectBuilder { + if builder.where != "" { + builder.where += " AND " } - selectItem.where += where - return selectItem + builder.where += where + return builder } // TODO: Don't implement the SQL at the accumulator level but the adapter level @@ -115,28 +133,28 @@ func (selectItem *AccSelectBuilder) InQ(column string, subBuilder *AccSelectBuil return selectItem } -func (selectItem *AccSelectBuilder) DateCutoff(column string, quantity int, unit string) *AccSelectBuilder { - selectItem.dateCutoff = &dateCutoff{column, quantity, unit} - return selectItem +func (builder *AccSelectBuilder) DateCutoff(column string, quantity int, unit string) *AccSelectBuilder { + builder.dateCutoff = &dateCutoff{column, quantity, unit} + return builder } -func (selectItem *AccSelectBuilder) Orderby(orderby string) *AccSelectBuilder { - selectItem.orderby = orderby - return selectItem +func (builder *AccSelectBuilder) Orderby(orderby string) *AccSelectBuilder { + builder.orderby = orderby + return builder } -func (selectItem *AccSelectBuilder) Limit(limit string) *AccSelectBuilder { - selectItem.limit = limit - return selectItem +func (builder *AccSelectBuilder) Limit(limit string) *AccSelectBuilder { + builder.limit = limit + return builder } -func (selectItem *AccSelectBuilder) Prepare() *sql.Stmt { +func (builder *AccSelectBuilder) Prepare() *sql.Stmt { // TODO: Phase out the procedural API and use the adapter's OO API? The OO API might need a bit more work before we do that and it needs to be rolled out to MSSQL. - if selectItem.dateCutoff != nil || selectItem.inChain != nil { - selectBuilder := selectItem.build.GetAdapter().Builder().Select().FromAcc(selectItem) - return selectItem.build.prepare(selectItem.build.GetAdapter().ComplexSelect(selectBuilder)) + if builder.dateCutoff != nil || builder.inChain != nil { + selectBuilder := builder.build.GetAdapter().Builder().Select().FromAcc(builder) + return builder.build.prepare(builder.build.GetAdapter().ComplexSelect(selectBuilder)) } - return selectItem.build.SimpleSelect(selectItem.table, selectItem.columns, selectItem.where, selectItem.orderby, selectItem.limit) + return builder.build.SimpleSelect(builder.table, builder.columns, builder.where, builder.orderby, builder.limit) } func (builder *AccSelectBuilder) query() (string, error) { @@ -145,15 +163,15 @@ func (builder *AccSelectBuilder) query() (string, error) { selectBuilder := builder.build.GetAdapter().Builder().Select().FromAcc(builder) return builder.build.GetAdapter().ComplexSelect(selectBuilder) } - return builder.build.adapter.SimpleSelect("_builder", builder.table, builder.columns, builder.where, builder.orderby, builder.limit) + return builder.build.adapter.SimpleSelect("", builder.table, builder.columns, builder.where, builder.orderby, builder.limit) } -func (selectItem *AccSelectBuilder) Query(args ...interface{}) (*sql.Rows, error) { - stmt := selectItem.Prepare() +func (builder *AccSelectBuilder) Query(args ...interface{}) (*sql.Rows, error) { + stmt := builder.Prepare() if stmt != nil { return stmt.Query(args...) } - return nil, selectItem.build.FirstError() + return nil, builder.build.FirstError() } type AccRowWrap struct { @@ -245,7 +263,7 @@ func (insert *accInsertBuilder) Prepare() *sql.Stmt { } func (builder *accInsertBuilder) Exec(args ...interface{}) (res sql.Result, err error) { - query, err := builder.build.adapter.SimpleInsert("_builder", builder.table, builder.columns, builder.fields) + query, err := builder.build.adapter.SimpleInsert("", builder.table, builder.columns, builder.fields) if err != nil { return res, err } @@ -253,7 +271,7 @@ func (builder *accInsertBuilder) Exec(args ...interface{}) (res sql.Result, err } func (builder *accInsertBuilder) Run(args ...interface{}) (int, error) { - query, err := builder.build.adapter.SimpleInsert("_builder", builder.table, builder.columns, builder.fields) + query, err := builder.build.adapter.SimpleInsert("", builder.table, builder.columns, builder.fields) if err != nil { return 0, err } @@ -292,4 +310,13 @@ func (count *accCountBuilder) Prepare() *sql.Stmt { return count.build.SimpleCount(count.table, count.where, count.limit) } +func (count *accCountBuilder) Total() (total int, err error) { + stmt := count.Prepare() + if stmt == nil { + return 0, count.build.FirstError() + } + err = stmt.QueryRow().Scan(&total) + return total, err +} + // TODO: Add a Sum builder for summing viewchunks up into one number for the dashboard? diff --git a/query_gen/accumulator.go b/query_gen/accumulator.go index 7752f9bd..84c70488 100644 --- a/query_gen/accumulator.go +++ b/query_gen/accumulator.go @@ -95,52 +95,56 @@ func (build *Accumulator) Tx(handler func(*TransactionBuilder) error) { } func (build *Accumulator) SimpleSelect(table string, columns string, where string, orderby string, limit string) *sql.Stmt { - return build.prepare(build.adapter.SimpleSelect("_builder", table, columns, where, orderby, limit)) + return build.prepare(build.adapter.SimpleSelect("", table, columns, where, orderby, limit)) } func (build *Accumulator) SimpleCount(table string, where string, limit string) *sql.Stmt { - return build.prepare(build.adapter.SimpleCount("_builder", table, where, limit)) + return build.prepare(build.adapter.SimpleCount("", table, where, limit)) } func (build *Accumulator) SimpleLeftJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) *sql.Stmt { - return build.prepare(build.adapter.SimpleLeftJoin("_builder", table1, table2, columns, joiners, where, orderby, limit)) + return build.prepare(build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit)) } func (build *Accumulator) SimpleInnerJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) *sql.Stmt { - return build.prepare(build.adapter.SimpleInnerJoin("_builder", table1, table2, columns, joiners, where, orderby, limit)) + return build.prepare(build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit)) } func (build *Accumulator) CreateTable(table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) *sql.Stmt { - return build.prepare(build.adapter.CreateTable("_builder", table, charset, collation, columns, keys)) + return build.prepare(build.adapter.CreateTable("", table, charset, collation, columns, keys)) } func (build *Accumulator) SimpleInsert(table string, columns string, fields string) *sql.Stmt { - return build.prepare(build.adapter.SimpleInsert("_builder", table, columns, fields)) + return build.prepare(build.adapter.SimpleInsert("", table, columns, fields)) } func (build *Accumulator) SimpleInsertSelect(ins DBInsert, sel DBSelect) *sql.Stmt { - return build.prepare(build.adapter.SimpleInsertSelect("_builder", ins, sel)) + return build.prepare(build.adapter.SimpleInsertSelect("", ins, sel)) } func (build *Accumulator) SimpleInsertLeftJoin(ins DBInsert, sel DBJoin) *sql.Stmt { - return build.prepare(build.adapter.SimpleInsertLeftJoin("_builder", ins, sel)) + return build.prepare(build.adapter.SimpleInsertLeftJoin("", ins, sel)) } func (build *Accumulator) SimpleInsertInnerJoin(ins DBInsert, sel DBJoin) *sql.Stmt { - return build.prepare(build.adapter.SimpleInsertInnerJoin("_builder", ins, sel)) + return build.prepare(build.adapter.SimpleInsertInnerJoin("", ins, sel)) } func (build *Accumulator) SimpleUpdate(table string, set string, where string) *sql.Stmt { - return build.prepare(build.adapter.SimpleUpdate("_builder", table, set, where)) + return build.prepare(build.adapter.SimpleUpdate(qUpdate(table, set, where))) +} + +func (build *Accumulator) SimpleUpdateSelect(table string, set string, where string) *sql.Stmt { + return build.prepare(build.adapter.SimpleUpdateSelect(qUpdate(table, set, where))) } func (build *Accumulator) SimpleDelete(table string, where string) *sql.Stmt { - return build.prepare(build.adapter.SimpleDelete("_builder", table, where)) + return build.prepare(build.adapter.SimpleDelete("", table, where)) } // I don't know why you need this, but here it is x.x func (build *Accumulator) Purge(table string) *sql.Stmt { - return build.prepare(build.adapter.Purge("_builder", table)) + return build.prepare(build.adapter.Purge("", table)) } func (build *Accumulator) prepareTx(tx *sql.Tx, res string, err error) (stmt *sql.Stmt) { @@ -155,63 +159,63 @@ func (build *Accumulator) prepareTx(tx *sql.Tx, res string, err error) (stmt *sq // These ones support transactions func (build *Accumulator) SimpleSelectTx(tx *sql.Tx, table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt) { - res, err := build.adapter.SimpleSelect("_builder", table, columns, where, orderby, limit) + res, err := build.adapter.SimpleSelect("", table, columns, where, orderby, limit) return build.prepareTx(tx, res, err) } func (build *Accumulator) SimpleCountTx(tx *sql.Tx, table string, where string, limit string) (stmt *sql.Stmt) { - res, err := build.adapter.SimpleCount("_builder", table, where, limit) + res, err := build.adapter.SimpleCount("", table, where, limit) return build.prepareTx(tx, res, err) } func (build *Accumulator) SimpleLeftJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt) { - res, err := build.adapter.SimpleLeftJoin("_builder", table1, table2, columns, joiners, where, orderby, limit) + res, err := build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit) return build.prepareTx(tx, res, err) } func (build *Accumulator) SimpleInnerJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt) { - res, err := build.adapter.SimpleInnerJoin("_builder", table1, table2, columns, joiners, where, orderby, limit) + res, err := build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit) return build.prepareTx(tx, res, err) } func (build *Accumulator) CreateTableTx(tx *sql.Tx, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt) { - res, err := build.adapter.CreateTable("_builder", table, charset, collation, columns, keys) + res, err := build.adapter.CreateTable("", table, charset, collation, columns, keys) return build.prepareTx(tx, res, err) } func (build *Accumulator) SimpleInsertTx(tx *sql.Tx, table string, columns string, fields string) (stmt *sql.Stmt) { - res, err := build.adapter.SimpleInsert("_builder", table, columns, fields) + res, err := build.adapter.SimpleInsert("", table, columns, fields) return build.prepareTx(tx, res, err) } func (build *Accumulator) SimpleInsertSelectTx(tx *sql.Tx, ins DBInsert, sel DBSelect) (stmt *sql.Stmt) { - res, err := build.adapter.SimpleInsertSelect("_builder", ins, sel) + res, err := build.adapter.SimpleInsertSelect("", ins, sel) return build.prepareTx(tx, res, err) } func (build *Accumulator) SimpleInsertLeftJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt) { - res, err := build.adapter.SimpleInsertLeftJoin("_builder", ins, sel) + res, err := build.adapter.SimpleInsertLeftJoin("", ins, sel) return build.prepareTx(tx, res, err) } func (build *Accumulator) SimpleInsertInnerJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt) { - res, err := build.adapter.SimpleInsertInnerJoin("_builder", ins, sel) + res, err := build.adapter.SimpleInsertInnerJoin("", ins, sel) return build.prepareTx(tx, res, err) } func (build *Accumulator) SimpleUpdateTx(tx *sql.Tx, table string, set string, where string) (stmt *sql.Stmt) { - res, err := build.adapter.SimpleUpdate("_builder", table, set, where) + res, err := build.adapter.SimpleUpdate(qUpdate(table, set, where)) return build.prepareTx(tx, res, err) } func (build *Accumulator) SimpleDeleteTx(tx *sql.Tx, table string, where string) (stmt *sql.Stmt) { - res, err := build.adapter.SimpleDelete("_builder", table, where) + res, err := build.adapter.SimpleDelete("", table, where) return build.prepareTx(tx, res, err) } // I don't know why you need this, but here it is x.x func (build *Accumulator) PurgeTx(tx *sql.Tx, table string) (stmt *sql.Stmt) { - res, err := build.adapter.Purge("_builder", table) + res, err := build.adapter.Purge("", table) return build.prepareTx(tx, res, err) } @@ -220,7 +224,7 @@ func (build *Accumulator) Delete(table string) *accDeleteBuilder { } func (build *Accumulator) Update(table string) *accUpdateBuilder { - return &accUpdateBuilder{table, "", "", build} + return &accUpdateBuilder{qUpdate(table, "", ""), build} } func (build *Accumulator) Select(table string) *AccSelectBuilder { diff --git a/query_gen/builder.go b/query_gen/builder.go index fc7fb91e..02ab85d1 100644 --- a/query_gen/builder.go +++ b/query_gen/builder.go @@ -85,60 +85,60 @@ func (build *builder) prepare(res string, err error) (*sql.Stmt, error) { } func (build *builder) SimpleSelect(table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.SimpleSelect("_builder", table, columns, where, orderby, limit)) + return build.prepare(build.adapter.SimpleSelect("", table, columns, where, orderby, limit)) } func (build *builder) SimpleCount(table string, where string, limit string) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.SimpleCount("_builder", table, where, limit)) + return build.prepare(build.adapter.SimpleCount("", table, where, limit)) } func (build *builder) SimpleLeftJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.SimpleLeftJoin("_builder", table1, table2, columns, joiners, where, orderby, limit)) + return build.prepare(build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit)) } func (build *builder) SimpleInnerJoin(table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.SimpleInnerJoin("_builder", table1, table2, columns, joiners, where, orderby, limit)) + return build.prepare(build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit)) } func (build *builder) DropTable(table string) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.DropTable("_builder", table)) + return build.prepare(build.adapter.DropTable("", table)) } func (build *builder) CreateTable(table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.CreateTable("_builder", table, charset, collation, columns, keys)) + return build.prepare(build.adapter.CreateTable("", table, charset, collation, columns, keys)) } func (build *builder) AddColumn(table string, column DBTableColumn) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.AddColumn("_builder", table, column)) + return build.prepare(build.adapter.AddColumn("", table, column)) } func (build *builder) SimpleInsert(table string, columns string, fields string) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.SimpleInsert("_builder", table, columns, fields)) + return build.prepare(build.adapter.SimpleInsert("", table, columns, fields)) } func (build *builder) SimpleInsertSelect(ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.SimpleInsertSelect("_builder", ins, sel)) + return build.prepare(build.adapter.SimpleInsertSelect("", ins, sel)) } func (build *builder) SimpleInsertLeftJoin(ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.SimpleInsertLeftJoin("_builder", ins, sel)) + return build.prepare(build.adapter.SimpleInsertLeftJoin("", ins, sel)) } func (build *builder) SimpleInsertInnerJoin(ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.SimpleInsertInnerJoin("_builder", ins, sel)) + return build.prepare(build.adapter.SimpleInsertInnerJoin("", ins, sel)) } func (build *builder) SimpleUpdate(table string, set string, where string) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.SimpleUpdate("_builder", table, set, where)) + return build.prepare(build.adapter.SimpleUpdate(qUpdate(table, set, where))) } func (build *builder) SimpleDelete(table string, where string) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.SimpleDelete("_builder", table, where)) + return build.prepare(build.adapter.SimpleDelete("", table, where)) } // I don't know why you need this, but here it is x.x func (build *builder) Purge(table string) (stmt *sql.Stmt, err error) { - return build.prepare(build.adapter.Purge("_builder", table)) + return build.prepare(build.adapter.Purge("", table)) } func (build *builder) prepareTx(tx *sql.Tx, res string, err error) (*sql.Stmt, error) { @@ -150,62 +150,62 @@ func (build *builder) prepareTx(tx *sql.Tx, res string, err error) (*sql.Stmt, e // These ones support transactions func (build *builder) SimpleSelectTx(tx *sql.Tx, table string, columns string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleSelect("_builder", table, columns, where, orderby, limit) + res, err := build.adapter.SimpleSelect("", table, columns, where, orderby, limit) return build.prepareTx(tx, res, err) } func (build *builder) SimpleCountTx(tx *sql.Tx, table string, where string, limit string) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleCount("_builder", table, where, limit) + res, err := build.adapter.SimpleCount("", table, where, limit) return build.prepareTx(tx, res, err) } func (build *builder) SimpleLeftJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleLeftJoin("_builder", table1, table2, columns, joiners, where, orderby, limit) + res, err := build.adapter.SimpleLeftJoin("", table1, table2, columns, joiners, where, orderby, limit) return build.prepareTx(tx, res, err) } func (build *builder) SimpleInnerJoinTx(tx *sql.Tx, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleInnerJoin("_builder", table1, table2, columns, joiners, where, orderby, limit) + res, err := build.adapter.SimpleInnerJoin("", table1, table2, columns, joiners, where, orderby, limit) return build.prepareTx(tx, res, err) } func (build *builder) CreateTableTx(tx *sql.Tx, table string, charset string, collation string, columns []DBTableColumn, keys []DBTableKey) (stmt *sql.Stmt, err error) { - res, err := build.adapter.CreateTable("_builder", table, charset, collation, columns, keys) + res, err := build.adapter.CreateTable("", table, charset, collation, columns, keys) return build.prepareTx(tx, res, err) } func (build *builder) SimpleInsertTx(tx *sql.Tx, table string, columns string, fields string) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleInsert("_builder", table, columns, fields) + res, err := build.adapter.SimpleInsert("", table, columns, fields) return build.prepareTx(tx, res, err) } func (build *builder) SimpleInsertSelectTx(tx *sql.Tx, ins DBInsert, sel DBSelect) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleInsertSelect("_builder", ins, sel) + res, err := build.adapter.SimpleInsertSelect("", ins, sel) return build.prepareTx(tx, res, err) } func (build *builder) SimpleInsertLeftJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleInsertLeftJoin("_builder", ins, sel) + res, err := build.adapter.SimpleInsertLeftJoin("", ins, sel) return build.prepareTx(tx, res, err) } func (build *builder) SimpleInsertInnerJoinTx(tx *sql.Tx, ins DBInsert, sel DBJoin) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleInsertInnerJoin("_builder", ins, sel) + res, err := build.adapter.SimpleInsertInnerJoin("", ins, sel) return build.prepareTx(tx, res, err) } func (build *builder) SimpleUpdateTx(tx *sql.Tx, table string, set string, where string) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleUpdate("_builder", table, set, where) + res, err := build.adapter.SimpleUpdate(qUpdate(table, set, where)) return build.prepareTx(tx, res, err) } func (build *builder) SimpleDeleteTx(tx *sql.Tx, table string, where string) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleDelete("_builder", table, where) + res, err := build.adapter.SimpleDelete("", table, where) return build.prepareTx(tx, res, err) } // I don't know why you need this, but here it is x.x func (build *builder) PurgeTx(tx *sql.Tx, table string) (stmt *sql.Stmt, err error) { - res, err := build.adapter.Purge("_builder", table) + res, err := build.adapter.Purge("", table) return build.prepareTx(tx, res, err) } diff --git a/query_gen/micro_builders.go b/query_gen/micro_builders.go index 28cc498a..271e8ee1 100644 --- a/query_gen/micro_builders.go +++ b/query_gen/micro_builders.go @@ -11,22 +11,22 @@ type prebuilder struct { } func (build *prebuilder) Select(nlist ...string) *selectPrebuilder { - name := optString(nlist, "_builder") + name := optString(nlist, "") return &selectPrebuilder{name, "", "", "", "", "", nil, nil, "", build.adapter} } func (build *prebuilder) Insert(nlist ...string) *insertPrebuilder { - name := optString(nlist, "_builder") + name := optString(nlist, "") return &insertPrebuilder{name, "", "", "", build.adapter} } func (build *prebuilder) Update(nlist ...string) *updatePrebuilder { - name := optString(nlist, "_builder") - return &updatePrebuilder{name, "", "", "", build.adapter} + name := optString(nlist, "") + return &updatePrebuilder{name, "", "", "", nil, build.adapter} } func (build *prebuilder) Delete(nlist ...string) *deletePrebuilder { - name := optString(nlist, "_builder") + name := optString(nlist, "") return &deletePrebuilder{name, "", "", build.adapter} } @@ -60,14 +60,19 @@ func (delete *deletePrebuilder) Parse() { } type updatePrebuilder struct { - name string - table string - set string - where string + name string + table string + set string + where string + whereSubQuery *selectPrebuilder build Adapter } +func qUpdate(table string, set string, where string) *updatePrebuilder { + return &updatePrebuilder{table: table, set: set, where: where} +} + func (update *updatePrebuilder) Table(table string) *updatePrebuilder { update.table = table return update @@ -86,12 +91,17 @@ func (update *updatePrebuilder) Where(where string) *updatePrebuilder { return update } +func (update *updatePrebuilder) WhereQ(sel *selectPrebuilder) *updatePrebuilder { + update.whereSubQuery = sel + return update +} + func (update *updatePrebuilder) Text() (string, error) { - return update.build.SimpleUpdate(update.name, update.table, update.set, update.where) + return update.build.SimpleUpdate(update) } func (update *updatePrebuilder) Parse() { - update.build.SimpleUpdate(update.name, update.table, update.set, update.where) + update.build.SimpleUpdate(update) } type selectPrebuilder struct { @@ -151,7 +161,7 @@ func (selectItem *selectPrebuilder) FromAcc(accBuilder *AccSelectBuilder) *selec selectItem.dateCutoff = accBuilder.dateCutoff if accBuilder.inChain != nil { - selectItem.inChain = &selectPrebuilder{"__builder", accBuilder.inChain.table, accBuilder.inChain.columns, accBuilder.inChain.where, accBuilder.inChain.orderby, accBuilder.inChain.limit, accBuilder.inChain.dateCutoff, nil, "", selectItem.build} + selectItem.inChain = &selectPrebuilder{"", accBuilder.inChain.table, accBuilder.inChain.columns, accBuilder.inChain.where, accBuilder.inChain.orderby, accBuilder.inChain.limit, accBuilder.inChain.dateCutoff, nil, "", selectItem.build} selectItem.inColumn = accBuilder.inColumn } return selectItem diff --git a/query_gen/mssql.go b/query_gen/mssql.go index cdef288c..f294f642 100644 --- a/query_gen/mssql.go +++ b/query_gen/mssql.go @@ -45,9 +45,6 @@ func (adapter *MssqlAdapter) DbVersion() string { } func (adapter *MssqlAdapter) DropTable(name string, table string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -59,9 +56,6 @@ func (adapter *MssqlAdapter) DropTable(name string, table string) (string, error // 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) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -142,9 +136,6 @@ func (adapter *MssqlAdapter) parseColumn(column DBTableColumn) (col DBTableColum // TODO: Test this, not sure if some things work func (adapter *MssqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -156,9 +147,6 @@ func (adapter *MssqlAdapter) AddColumn(name string, table string, column DBTable } func (adapter *MssqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -237,9 +225,6 @@ func (adapter *MssqlAdapter) SimpleReplace(name string, table string, columns st } func (adapter *MssqlAdapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -332,19 +317,16 @@ func (adapter *MssqlAdapter) SimpleUpsert(name string, table string, columns str return querystr, nil } -func (adapter *MssqlAdapter) SimpleUpdate(name string, table string, set string, where string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } - if table == "" { +func (adapter *MssqlAdapter) SimpleUpdate(up *updatePrebuilder) (string, error) { + if up.table == "" { return "", errors.New("You need a name for this table") } - if set == "" { + if up.set == "" { return "", errors.New("You need to set data in this update statement") } - var querystr = "UPDATE [" + table + "] SET " - for _, item := range processSet(set) { + var querystr = "UPDATE [" + up.table + "] SET " + for _, item := range processSet(up.set) { querystr += "[" + item.Column + "] =" for _, token := range item.Expr { switch token.Type { @@ -370,9 +352,9 @@ func (adapter *MssqlAdapter) SimpleUpdate(name string, table string, set string, querystr = querystr[0 : len(querystr)-1] // Add support for BETWEEN x.x - if len(where) != 0 { + if len(up.where) != 0 { querystr += " WHERE" - for _, loc := range processWhere(where) { + for _, loc := range processWhere(up.where) { for _, token := range loc.Expr { switch token.Type { case "function", "operator", "number", "substitute", "or": @@ -394,14 +376,15 @@ func (adapter *MssqlAdapter) SimpleUpdate(name string, table string, set string, querystr = querystr[0 : len(querystr)-4] } - adapter.pushStatement(name, "update", querystr) + adapter.pushStatement(up.name, "update", querystr) return querystr, nil } +func (adapter *MssqlAdapter) SimpleUpdateSelect(up *updatePrebuilder) (string, error) { + return "", errors.New("not implemented") +} + func (adapter *MssqlAdapter) SimpleDelete(name string, table string, where string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -441,9 +424,6 @@ func (adapter *MssqlAdapter) SimpleDelete(name string, table string, where strin // We don't want to accidentally wipe tables, so we'll have a separate method for purging tables instead func (adapter *MssqlAdapter) Purge(name string, table string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -452,9 +432,6 @@ func (adapter *MssqlAdapter) Purge(name string, table string) (string, error) { } func (adapter *MssqlAdapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -554,9 +531,6 @@ func (adapter *MssqlAdapter) ComplexSelect(preBuilder *selectPrebuilder) (string } func (adapter *MssqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table1 == "" { return "", errors.New("You need a name for the left table") } @@ -683,9 +657,6 @@ func (adapter *MssqlAdapter) SimpleLeftJoin(name string, table1 string, table2 s } func (adapter *MssqlAdapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table1 == "" { return "", errors.New("You need a name for the left table") } @@ -1067,9 +1038,6 @@ func (adapter *MssqlAdapter) SimpleInsertInnerJoin(name string, ins DBInsert, se } func (adapter *MssqlAdapter) SimpleCount(name string, table string, where string, limit string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -1116,7 +1084,7 @@ func (adapter *MssqlAdapter) Builder() *prebuilder { func (adapter *MssqlAdapter) Write() error { var stmts, body string for _, name := range adapter.BufferOrder { - if name[0] == '_' { + if name == "" { continue } stmt := adapter.Buffer[name] diff --git a/query_gen/mysql.go b/query_gen/mysql.go index 68393a12..d7d78990 100644 --- a/query_gen/mysql.go +++ b/query_gen/mysql.go @@ -83,21 +83,16 @@ func (adapter *MysqlAdapter) DbVersion() string { } func (adapter *MysqlAdapter) DropTable(name string, table string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } querystr := "DROP TABLE IF EXISTS `" + table + "`;" + // TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator adapter.pushStatement(name, "drop-table", querystr) return querystr, nil } 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") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -133,6 +128,7 @@ func (adapter *MysqlAdapter) CreateTable(name string, table string, charset stri querystr += " COLLATE " + collation } + // TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator adapter.pushStatement(name, "create-table", querystr+";") return querystr + ";", nil } @@ -178,23 +174,18 @@ func (adapter *MysqlAdapter) parseColumn(column DBTableColumn) (col DBTableColum // TODO: Support AFTER column // TODO: Test to make sure everything works here func (adapter *MysqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } column, size, end := adapter.parseColumn(column) querystr := "ALTER TABLE `" + table + "` ADD COLUMN " + "`" + column.Name + "` " + column.Type + size + end + ";" + // TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator adapter.pushStatement(name, "add-column", querystr) return querystr, nil } func (adapter *MysqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -218,6 +209,7 @@ func (adapter *MysqlAdapter) SimpleInsert(name string, table string, columns str } querystr += ")" + // TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator adapter.pushStatement(name, "insert", querystr) return querystr, nil } @@ -239,9 +231,6 @@ func (adapter *MysqlAdapter) buildColumns(columns string) (querystr string) { // ! DEPRECATED func (adapter *MysqlAdapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -258,14 +247,12 @@ func (adapter *MysqlAdapter) SimpleReplace(name string, table string, columns st } querystr = querystr[0 : len(querystr)-1] + // TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator adapter.pushStatement(name, "replace", querystr+")") return querystr + ")", nil } func (adapter *MysqlAdapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -305,23 +292,21 @@ func (adapter *MysqlAdapter) SimpleUpsert(name string, table string, columns str querystr += insertColumns + setBit + // TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator adapter.pushStatement(name, "upsert", querystr) return querystr, nil } -func (adapter *MysqlAdapter) SimpleUpdate(name string, table string, set string, where string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } - if table == "" { +func (adapter *MysqlAdapter) SimpleUpdate(up *updatePrebuilder) (string, error) { + if up.table == "" { return "", errors.New("You need a name for this table") } - if set == "" { + if up.set == "" { return "", errors.New("You need to set data in this update statement") } - var querystr = "UPDATE `" + table + "` SET " - for _, item := range processSet(set) { + var querystr = "UPDATE `" + up.table + "` SET " + for _, item := range processSet(up.set) { querystr += "`" + item.Column + "` =" for _, token := range item.Expr { switch token.Type { @@ -337,20 +322,18 @@ func (adapter *MysqlAdapter) SimpleUpdate(name string, table string, set string, } querystr = querystr[0 : len(querystr)-1] - whereStr, err := adapter.buildWhere(where) + whereStr, err := adapter.buildWhere(up.where) if err != nil { return querystr, err } querystr += whereStr - adapter.pushStatement(name, "update", querystr) + // TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator + adapter.pushStatement(up.name, "update", querystr) return querystr, nil } func (adapter *MysqlAdapter) SimpleDelete(name string, table string, where string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -378,15 +361,13 @@ func (adapter *MysqlAdapter) SimpleDelete(name string, table string, where strin } querystr = strings.TrimSpace(querystr[0 : len(querystr)-4]) + // TODO: Shunt the table name logic and associated stmt list up to the a higher layer to reduce the amount of unnecessary overhead in the builder / accumulator adapter.pushStatement(name, "delete", querystr) return querystr, nil } // We don't want to accidentally wipe tables, so we'll have a separate method for purging tables instead func (adapter *MysqlAdapter) Purge(name string, table string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -459,9 +440,6 @@ func (adapter *MysqlAdapter) buildOrderby(orderby string) (querystr string) { } func (adapter *MysqlAdapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -490,9 +468,6 @@ func (adapter *MysqlAdapter) SimpleSelect(name string, table string, columns str } func (adapter *MysqlAdapter) ComplexSelect(preBuilder *selectPrebuilder) (out string, err error) { - if preBuilder.name == "" { - return "", errors.New("You need a name for this statement") - } if preBuilder.table == "" { return "", errors.New("You need a name for this table") } @@ -531,9 +506,6 @@ func (adapter *MysqlAdapter) ComplexSelect(preBuilder *selectPrebuilder) (out st } func (adapter *MysqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table1 == "" { return "", errors.New("You need a name for the left table") } @@ -560,9 +532,6 @@ func (adapter *MysqlAdapter) SimpleLeftJoin(name string, table1 string, table2 s } func (adapter *MysqlAdapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table1 == "" { return "", errors.New("You need a name for the left table") } @@ -588,6 +557,37 @@ func (adapter *MysqlAdapter) SimpleInnerJoin(name string, table1 string, table2 return querystr, nil } +func (adapter *MysqlAdapter) SimpleUpdateSelect(up *updatePrebuilder) (string, error) { + sel := up.whereSubQuery + whereStr, err := adapter.buildWhere(sel.where) + if err != nil { + return "", err + } + + var setter string + for _, item := range processSet(up.set) { + setter += "`" + item.Column + "` =" + for _, token := range item.Expr { + switch token.Type { + case "function", "operator", "number", "substitute", "or": + setter += " " + token.Contents + case "column": + setter += " `" + token.Contents + "`" + case "string": + setter += " '" + token.Contents + "'" + } + } + setter += "," + } + setter = setter[0 : len(setter)-1] + + var querystr = "UPDATE `" + up.table + "` SET " + setter + " WHERE (SELECT" + adapter.buildJoinColumns(sel.columns) + " FROM `" + sel.table + "`" + whereStr + adapter.buildOrderby(sel.orderby) + adapter.buildLimit(sel.limit) + ")" + + querystr = strings.TrimSpace(querystr) + adapter.pushStatement(up.name, "update", querystr) + return querystr, nil +} + func (adapter *MysqlAdapter) SimpleInsertSelect(name string, ins DBInsert, sel DBSelect) (string, error) { whereStr, err := adapter.buildWhere(sel.Where) if err != nil { @@ -692,9 +692,6 @@ func (adapter *MysqlAdapter) SimpleInsertInnerJoin(name string, ins DBInsert, se } func (adapter *MysqlAdapter) SimpleCount(name string, table string, where string, limit string) (querystr string, err error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -778,7 +775,7 @@ func _gen_mysql() (err error) { // Internal methods, not exposed in the interface func (adapter *MysqlAdapter) pushStatement(name string, stype string, querystr string) { - if name[0] == '_' { + if name == "" { return } adapter.Buffer[name] = DBStmt{querystr, stype} diff --git a/query_gen/pgsql.go b/query_gen/pgsql.go index 206a0cf3..be64bbb2 100644 --- a/query_gen/pgsql.go +++ b/query_gen/pgsql.go @@ -43,9 +43,6 @@ func (adapter *PgsqlAdapter) DbVersion() string { } func (adapter *PgsqlAdapter) DropTable(name string, table string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -57,9 +54,6 @@ func (adapter *PgsqlAdapter) DropTable(name string, table string) (string, error // TODO: Implement this // We may need to change the CreateTable API to better suit PGSQL and the other database drivers which are coming up func (adapter *PgsqlAdapter) 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") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -120,9 +114,6 @@ func (adapter *PgsqlAdapter) CreateTable(name string, table string, charset stri // TODO: Implement this func (adapter *PgsqlAdapter) AddColumn(name string, table string, column DBTableColumn) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -132,9 +123,6 @@ func (adapter *PgsqlAdapter) AddColumn(name string, table string, column DBTable // TODO: Test this // ! We need to get the last ID out of this somehow, maybe add returning to every query? Might require some sort of wrapper over the sql statements func (adapter *PgsqlAdapter) SimpleInsert(name string, table string, columns string, fields string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -179,9 +167,6 @@ func (adapter *PgsqlAdapter) buildColumns(columns string) (querystr string) { // TODO: Implement this func (adapter *PgsqlAdapter) SimpleReplace(name string, table string, columns string, fields string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -196,9 +181,6 @@ func (adapter *PgsqlAdapter) SimpleReplace(name string, table string, columns st // TODO: Implement this func (adapter *PgsqlAdapter) SimpleUpsert(name string, table string, columns string, fields string, where string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -212,19 +194,16 @@ func (adapter *PgsqlAdapter) SimpleUpsert(name string, table string, columns str } // TODO: Implemented, but we need CreateTable and a better installer to *test* it -func (adapter *PgsqlAdapter) SimpleUpdate(name string, table string, set string, where string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } - if table == "" { +func (adapter *PgsqlAdapter) SimpleUpdate(up *updatePrebuilder) (string, error) { + if up.table == "" { return "", errors.New("You need a name for this table") } - if set == "" { + if up.set == "" { return "", errors.New("You need to set data in this update statement") } - var querystr = "UPDATE \"" + table + "\" SET " - for _, item := range processSet(set) { + var querystr = "UPDATE \"" + up.table + "\" SET " + for _, item := range processSet(up.set) { querystr += "`" + item.Column + "` =" for _, token := range item.Expr { switch token.Type { @@ -248,9 +227,9 @@ func (adapter *PgsqlAdapter) SimpleUpdate(name string, table string, set string, querystr = querystr[0 : len(querystr)-1] // Add support for BETWEEN x.x - if len(where) != 0 { + if len(up.where) != 0 { querystr += " WHERE" - for _, loc := range processWhere(where) { + for _, loc := range processWhere(up.where) { for _, token := range loc.Expr { switch token.Type { case "function": @@ -274,15 +253,17 @@ func (adapter *PgsqlAdapter) SimpleUpdate(name string, table string, set string, querystr = querystr[0 : len(querystr)-4] } - adapter.pushStatement(name, "update", querystr) + adapter.pushStatement(up.name, "update", querystr) return querystr, nil } +// TODO: Implement this +func (adapter *PgsqlAdapter) SimpleUpdateSelect(up *updatePrebuilder) (string, error) { + return "", errors.New("not implemented") +} + // TODO: Implement this func (adapter *PgsqlAdapter) SimpleDelete(name string, table string, where string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -295,9 +276,6 @@ func (adapter *PgsqlAdapter) SimpleDelete(name string, table string, where strin // TODO: Implement this // We don't want to accidentally wipe tables, so we'll have a separate method for purging tables instead func (adapter *PgsqlAdapter) Purge(name string, table string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -306,9 +284,6 @@ func (adapter *PgsqlAdapter) Purge(name string, table string) (string, error) { // TODO: Implement this func (adapter *PgsqlAdapter) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -320,9 +295,6 @@ func (adapter *PgsqlAdapter) SimpleSelect(name string, table string, columns str // TODO: Implement this func (adapter *PgsqlAdapter) ComplexSelect(prebuilder *selectPrebuilder) (string, error) { - if prebuilder.name == "" { - return "", errors.New("You need a name for this statement") - } if prebuilder.table == "" { return "", errors.New("You need a name for this table") } @@ -334,9 +306,6 @@ func (adapter *PgsqlAdapter) ComplexSelect(prebuilder *selectPrebuilder) (string // TODO: Implement this func (adapter *PgsqlAdapter) SimpleLeftJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table1 == "" { return "", errors.New("You need a name for the left table") } @@ -354,9 +323,6 @@ func (adapter *PgsqlAdapter) SimpleLeftJoin(name string, table1 string, table2 s // TODO: Implement this func (adapter *PgsqlAdapter) SimpleInnerJoin(name string, table1 string, table2 string, columns string, joiners string, where string, orderby string, limit string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table1 == "" { return "", errors.New("You need a name for the left table") } @@ -389,9 +355,6 @@ func (adapter *PgsqlAdapter) SimpleInsertInnerJoin(name string, ins DBInsert, se // TODO: Implement this func (adapter *PgsqlAdapter) SimpleCount(name string, table string, where string, limit string) (string, error) { - if name == "" { - return "", errors.New("You need a name for this statement") - } if table == "" { return "", errors.New("You need a name for this table") } @@ -454,7 +417,7 @@ func _gen_pgsql() (err error) { // Internal methods, not exposed in the interface func (adapter *PgsqlAdapter) pushStatement(name string, stype string, querystr string) { - if name[0] == '_' { + if name == "" { return } adapter.Buffer[name] = DBStmt{querystr, stype} diff --git a/query_gen/querygen.go b/query_gen/querygen.go index 7455f746..367278ba 100644 --- a/query_gen/querygen.go +++ b/query_gen/querygen.go @@ -110,7 +110,8 @@ type Adapter interface { // TODO: Test this AddColumn(name string, table string, column DBTableColumn) (string, error) SimpleInsert(name string, table string, columns string, fields string) (string, error) - SimpleUpdate(name string, table string, set string, where string) (string, error) + SimpleUpdate(up *updatePrebuilder) (string, error) + SimpleUpdateSelect(up *updatePrebuilder) (string, error) // ! Experimental SimpleDelete(name string, table string, where string) (string, error) Purge(name string, table string) (string, error) SimpleSelect(name string, table string, columns string, where string, orderby string, limit string) (string, error) diff --git a/query_gen/transaction.go b/query_gen/transaction.go index 7f757382..2f6a7bec 100644 --- a/query_gen/transaction.go +++ b/query_gen/transaction.go @@ -25,7 +25,7 @@ type TransactionBuilder struct { } func (build *TransactionBuilder) SimpleDelete(table string, where string) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleDelete("_builder", table, where) + res, err := build.adapter.SimpleDelete("", table, where) if err != nil { return stmt, err } @@ -34,7 +34,7 @@ func (build *TransactionBuilder) SimpleDelete(table string, where string) (stmt // Quick* versions refer to it being quick to type not the performance. For performance critical transactions, you might want to use the Simple* methods or the *Tx methods on the main builder. Alternate suggestions for names are welcome :) func (build *TransactionBuilder) QuickDelete(table string, where string) *transactionStmt { - res, err := build.adapter.SimpleDelete("_builder", table, where) + res, err := build.adapter.SimpleDelete("", table, where) if err != nil { return newTransactionStmt(nil, err) } @@ -49,7 +49,7 @@ func (build *TransactionBuilder) QuickDelete(table string, where string) *transa } func (build *TransactionBuilder) SimpleInsert(table string, columns string, fields string) (stmt *sql.Stmt, err error) { - res, err := build.adapter.SimpleInsert("_builder", table, columns, fields) + res, err := build.adapter.SimpleInsert("", table, columns, fields) if err != nil { return stmt, err } @@ -57,7 +57,7 @@ func (build *TransactionBuilder) SimpleInsert(table string, columns string, fiel } func (build *TransactionBuilder) QuickInsert(table string, where string) *transactionStmt { - res, err := build.adapter.SimpleDelete("_builder", table, where) + res, err := build.adapter.SimpleDelete("", table, where) if err != nil { return newTransactionStmt(nil, err) } diff --git a/quick-update-linux b/quick-update-linux index b6ba0db4..f64f2b87 100644 --- a/quick-update-linux +++ b/quick-update-linux @@ -1,14 +1,8 @@ echo "Updating Gosora" -rm ./schema/lastSchema.json -cp ./schema/schema.json ./schema/lastSchema.json git stash git pull origin master git stash apply echo "Patching Gosora" -cd ./patcher -go generate -go build -o Patcher -mv ./Patcher .. -cd .. +go build -o Patcher "./patcher" ./Patcher \ No newline at end of file diff --git a/router_gen/routes.go b/router_gen/routes.go index 1d97b79d..1aeb7d37 100644 --- a/router_gen/routes.go +++ b/router_gen/routes.go @@ -89,7 +89,9 @@ func topicRoutes() *RouteGroup { 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("routes.LikeTopicSubmit", "/topic/like/submit/", "extraData").Before("ParseForm"), + Action("routes.LikeTopicSubmit", "/topic/like/submit/", "extraData"), + UploadAction("routes.AddAttachToTopicSubmit", "/topic/attach/add/submit/", "extraData").MaxSizeVar("int(common.Config.MaxRequestSize)"), + Action("routes.RemoveAttachFromTopicSubmit", "/topic/attach/remove/submit/", "extraData"), ) } @@ -99,7 +101,7 @@ func replyRoutes() *RouteGroup { UploadAction("routes.CreateReplySubmit", "/reply/create/").MaxSizeVar("int(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("routes.ReplyLikeSubmit", "/reply/like/submit/", "extraData").Before("ParseForm"), + Action("routes.ReplyLikeSubmit", "/reply/like/submit/", "extraData"), //MemberView("routes.ReplyEdit","/reply/edit/","extraData"), // No js fallback //MemberView("routes.ReplyDelete","/reply/delete/","extraData"), // No js confirmation page? We could have a confirmation modal for the JS case ) diff --git a/routes.go b/routes.go index 121fd76d..697b1958 100644 --- a/routes.go +++ b/routes.go @@ -30,6 +30,7 @@ var successJSONBytes = []byte(`{"success":"1"}`) var phraseLoginAlerts = []byte(`{"msgs":[{"msg":"Login to see your alerts","path":"/accounts/login"}],"msgCount":0}`) // TODO: Refactor this endpoint +// TODO: Move this into the routes package func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { // TODO: Don't make this too JSON dependent so that we can swap in newer more efficient formats w.Header().Set("Content-Type", "application/json") @@ -44,6 +45,7 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R } switch r.FormValue("module") { + // TODO: Split this into it's own function case "dismiss-alert": asid, err := strconv.Atoi(r.FormValue("asid")) if err != nil { @@ -61,6 +63,7 @@ func routeAPI(w http.ResponseWriter, r *http.Request, user common.User) common.R if common.EnableWebsockets && count > 0 { _ = common.WsHub.PushMessage(user.ID, `{"event":"dismiss-alert","asid":`+strconv.Itoa(asid)+`}`) } + // TODO: Split this into it's own function case "alerts": // A feed of events tailored for a specific user if !user.Loggedin { w.Write(phraseLoginAlerts) diff --git a/routes/forum.go b/routes/forum.go index 5e1e0353..2bacaf3d 100644 --- a/routes/forum.go +++ b/routes/forum.go @@ -21,7 +21,7 @@ var forumStmts ForumStmts func init() { common.DbInits.Add(func(acc *qgen.Accumulator) error { forumStmts = ForumStmts{ - getTopics: acc.Select("topics").Columns("tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, parentID, views, postCount, likeCount").Where("parentID = ?").Orderby("sticky DESC, lastReplyAt DESC, createdBy DESC").Limit("?,?").Prepare(), + getTopics: acc.Select("topics").Columns("tid, title, content, createdBy, is_closed, sticky, createdAt, lastReplyAt, lastReplyBy, lastReplyID, parentID, views, postCount, likeCount").Where("parentID = ?").Orderby("sticky DESC, lastReplyAt DESC, createdBy DESC").Limit("?,?").Prepare(), } return acc.FirstError() }) @@ -68,13 +68,12 @@ func ViewForum(w http.ResponseWriter, r *http.Request, user common.User, header var reqUserList = make(map[int]bool) for rows.Next() { var topicItem = common.TopicsRow{ID: 0} - err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.ParentID, &topicItem.ViewCount, &topicItem.PostCount, &topicItem.LikeCount) + err := rows.Scan(&topicItem.ID, &topicItem.Title, &topicItem.Content, &topicItem.CreatedBy, &topicItem.IsClosed, &topicItem.Sticky, &topicItem.CreatedAt, &topicItem.LastReplyAt, &topicItem.LastReplyBy, &topicItem.LastReplyID, &topicItem.ParentID, &topicItem.ViewCount, &topicItem.PostCount, &topicItem.LikeCount) if err != nil { return common.InternalError(err, w, r) } topicItem.Link = common.BuildTopicURL(common.NameToSlug(topicItem.Title), topicItem.ID) - topicItem.RelativeLastReplyAt = common.RelativeTime(topicItem.LastReplyAt) // TODO: Create a specialised function with a bit less overhead for getting the last page for a post count _, _, lastPage := common.PageOffset(topicItem.PostCount, 1, common.Config.ItemsPerPage) topicItem.LastPage = lastPage diff --git a/routes/profile.go b/routes/profile.go index b98dd796..7073101b 100644 --- a/routes/profile.go +++ b/routes/profile.go @@ -34,7 +34,7 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user common.User, heade var err error var replyCreatedAt time.Time - var replyContent, replyCreatedByName, replyRelativeCreatedAt, replyAvatar, replyMicroAvatar, replyTag, replyClassName string + var replyContent, replyCreatedByName, replyAvatar, replyMicroAvatar, replyTag, replyClassName string var rid, replyCreatedBy, replyLastEdit, replyLastEditBy, replyLines, replyGroup int var replyList []common.ReplyUser @@ -98,11 +98,9 @@ func ViewProfile(w http.ResponseWriter, r *http.Request, user common.User, heade replyLiked := false replyLikeCount := 0 - replyRelativeCreatedAt = common.RelativeTime(replyCreatedAt) - // TODO: Add a hook here - replyList = append(replyList, common.ReplyUser{rid, puser.ID, replyContent, common.ParseMessage(replyContent, 0, ""), replyCreatedBy, common.BuildProfileURL(common.NameToSlug(replyCreatedByName), replyCreatedBy), replyCreatedByName, replyGroup, replyCreatedAt, replyRelativeCreatedAt, replyLastEdit, replyLastEditBy, replyAvatar, replyMicroAvatar, replyClassName, replyLines, replyTag, "", "", "", 0, "", replyLiked, replyLikeCount, "", ""}) + replyList = append(replyList, common.ReplyUser{rid, puser.ID, replyContent, common.ParseMessage(replyContent, 0, ""), replyCreatedBy, common.BuildProfileURL(common.NameToSlug(replyCreatedByName), replyCreatedBy), replyCreatedByName, replyGroup, replyCreatedAt, replyLastEdit, replyLastEditBy, replyAvatar, replyMicroAvatar, replyClassName, replyLines, replyTag, "", "", "", 0, "", replyLiked, replyLikeCount, "", ""}) } err = rows.Err() if err != nil { diff --git a/routes/reply.go b/routes/reply.go index 1a3882f3..b570223a 100644 --- a/routes/reply.go +++ b/routes/reply.go @@ -1,14 +1,8 @@ package routes import ( - "crypto/sha256" "database/sql" - "encoding/hex" - "io" - "log" "net/http" - "os" - "regexp" "strconv" "strings" @@ -16,7 +10,6 @@ import ( "github.com/Azareal/Gosora/common/counters" ) -// TODO: De-duplicate the upload logic func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User) common.RouteError { tid, err := strconv.Atoi(r.PostFormValue("tid")) if err != nil { @@ -45,70 +38,9 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User) // Handle the file attachments // TODO: Stop duplicating this code if user.Perms.UploadFiles { - files, ok := r.MultipartForm.File["upload_files"] - if ok { - if len(files) > 5 { - return common.LocalError("You can't attach more than five files", w, r, user) - } - - for _, file := range files { - if file.Filename == "" { - continue - } - log.Print("file.Filename ", file.Filename) - extarr := strings.Split(file.Filename, ".") - if len(extarr) < 2 { - return common.LocalError("Bad file", w, r, user) - } - ext := extarr[len(extarr)-1] - - // TODO: Can we do this without a regex? - reg, err := regexp.Compile("[^A-Za-z0-9]+") - if err != nil { - return common.LocalError("Bad file extension", w, r, user) - } - ext = strings.ToLower(reg.ReplaceAllString(ext, "")) - if !common.AllowedFileExts.Contains(ext) { - return common.LocalError("You're not allowed to upload files with this extension", w, r, user) - } - - infile, err := file.Open() - if err != nil { - return common.LocalError("Upload failed", w, r, user) - } - defer infile.Close() - - hasher := sha256.New() - _, err = io.Copy(hasher, infile) - if err != nil { - return common.LocalError("Upload failed [Hashing Failed]", w, r, user) - } - infile.Close() - - checksum := hex.EncodeToString(hasher.Sum(nil)) - filename := checksum + "." + ext - outfile, err := os.Create("." + "/attachs/" + filename) - if err != nil { - return common.LocalError("Upload failed [File Creation Failed]", w, r, user) - } - defer outfile.Close() - - infile, err = file.Open() - if err != nil { - return common.LocalError("Upload failed", w, r, user) - } - defer infile.Close() - - _, err = io.Copy(outfile, infile) - if err != nil { - return common.LocalError("Upload failed [Copy Failed]", w, r, user) - } - - err = common.Attachments.Add(topic.ParentID, "forums", tid, "replies", user.ID, filename) - if err != nil { - return common.InternalError(err, w, r) - } - } + _, rerr := uploadAttachment(w, r, user, topic.ParentID, "forums", tid, "replies") + if rerr != nil { + return rerr } } @@ -127,8 +59,8 @@ func CreateReplySubmit(w http.ResponseWriter, r *http.Request, user common.User) var maxPollOptions = 10 var pollInputItems = make(map[int]string) for key, values := range r.Form { - common.DebugDetail("key: ", key) - common.DebugDetailf("values: %+v\n", values) + //common.DebugDetail("key: ", key) + //common.DebugDetailf("values: %+v\n", values) for _, value := range values { if strings.HasPrefix(key, "pollinputitem[") { halves := strings.Split(key, "[") diff --git a/routes/topic.go b/routes/topic.go index a8e12b5a..c352961a 100644 --- a/routes/topic.go +++ b/routes/topic.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/hex" "encoding/json" + "errors" "io" "log" "net/http" @@ -22,6 +23,7 @@ import ( type TopicStmts struct { getReplies *sql.Stmt getLikedTopic *sql.Stmt + updateAttachs *sql.Stmt } var topicStmts TopicStmts @@ -32,6 +34,8 @@ func init() { 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", "?,?"), getLikedTopic: acc.Select("likes").Columns("targetItem").Where("sentBy = ? && targetItem = ? && targetType = 'topics'").Prepare(), + // TODO: Less race-y attachment count updates + updateAttachs: acc.Update("topics").Set("attachCount = ?").Where("tid = ?").Prepare(), } return acc.FirstError() }) @@ -51,7 +55,6 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header } else if err != nil { return common.InternalError(err, w, r) } - topic.ClassName = "" ferr := common.ForumUserCheck(header, w, r, &user, topic.ParentID) if ferr != nil { @@ -64,6 +67,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header header.Zone = "view_topic" header.Path = common.BuildTopicURL(common.NameToSlug(topic.Title), topic.ID) + // TODO: Cache ContentHTML when possible? topic.ContentHTML = common.ParseMessage(topic.Content, topic.ParentID, "forums") topic.ContentLines = strings.Count(topic.Content, "\n") @@ -76,7 +80,6 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header if postGroup.IsMod { topic.ClassName = common.Config.StaffCSS } - topic.RelativeCreatedAt = common.RelativeTime(topic.CreatedAt) forum, err := common.Forums.Get(topic.ParentID) if err != nil { @@ -105,6 +108,15 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header } } + if topic.AttachCount > 0 { + attachs, err := common.Attachments.MiniTopicGet(topic.ID) + if err != nil { + // TODO: We might want to be a little permissive here in-case of a desync? + return common.InternalError(err, w, r) + } + topic.Attachments = attachs + } + // Calculate the offset offset, page, lastPage := common.PageOffset(topic.PostCount, page, common.Config.ItemsPerPage) pageList := common.Paginate(topic.PostCount, common.Config.ItemsPerPage, 5) @@ -150,33 +162,37 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header // TODO: Make a function for this? Build a more sophisticated noavatar handling system? Do bulk user loads and let the common.UserStore initialise this? replyItem.Avatar, replyItem.MicroAvatar = common.BuildAvatar(replyItem.CreatedBy, replyItem.Avatar) replyItem.Tag = postGroup.Tag - replyItem.RelativeCreatedAt = common.RelativeTime(replyItem.CreatedAt) // We really shouldn't have inline HTML, we should do something about this... if replyItem.ActionType != "" { + var action string switch replyItem.ActionType { case "lock": - replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_lock", replyItem.UserLink, replyItem.CreatedByName) + action = "lock" replyItem.ActionIcon = "🔒︎" case "unlock": - replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_unlock", replyItem.UserLink, replyItem.CreatedByName) + action = "unlock" replyItem.ActionIcon = "🔓︎" case "stick": - replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_stick", replyItem.UserLink, replyItem.CreatedByName) + action = "stick" replyItem.ActionIcon = "📌︎" case "unstick": - replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_unstick", replyItem.UserLink, replyItem.CreatedByName) + action = "unstick" replyItem.ActionIcon = "📌︎" case "move": - replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_move", replyItem.UserLink, replyItem.CreatedByName) - // TODO: Only fire this off if a corresponding phrase for the ActionType doesn't exist? Or maybe have some sort of action registry? - default: + action = "move" + replyItem.ActionIcon = "" + } + if action != "" { + replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_"+action, replyItem.UserLink, replyItem.CreatedByName) + } else { + // TODO: Only fire this off if a corresponding phrase for the ActionType doesn't exist? Or maybe have some sort of action registry? replyItem.ActionType = phrases.GetTmplPhrasef("topic.action_topic_default", replyItem.ActionType) replyItem.ActionIcon = "" } } - if replyItem.LikeCount > 0 { + if replyItem.LikeCount > 0 && user.Liked > 0 { likedMap[replyItem.ID] = len(tpage.ItemList) likedQueryList = append(likedQueryList, replyItem.ID) } @@ -192,6 +208,7 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header // 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()*/ { + // TODO: Abstract this rows, err := qgen.NewAcc().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) @@ -219,6 +236,89 @@ func ViewTopic(w http.ResponseWriter, r *http.Request, user common.User, header return rerr } +// TODO: Avoid uploading this again if the attachment already exists? They'll resolve to the same hash either way, but we could save on some IO / bandwidth here +// TODO: Enforce the max request limit on all of this topic's attachments +// TODO: Test this route +func AddAttachToTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError { + tid, err := strconv.Atoi(stid) + if err != nil { + return common.LocalErrorJS(phrases.GetErrorPhrase("id_must_be_integer"), w, r) + } + topic, err := common.Topics.Get(tid) + if err != nil { + return common.NotFoundJS(w, r) + } + + _, ferr := common.SimpleForumUserCheck(w, r, &user, topic.ParentID) + if ferr != nil { + return ferr + } + if !user.Perms.ViewTopic || !user.Perms.EditTopic || !user.Perms.UploadFiles { + return common.NoPermissionsJS(w, r, user) + } + if topic.IsClosed && !user.Perms.CloseTopic { + return common.NoPermissionsJS(w, r, user) + } + + // Handle the file attachments + pathMap, rerr := uploadAttachment(w, r, user, topic.ParentID, "forums", tid, "topics") + if rerr != nil { + // TODO: This needs to be a JS error... + return rerr + } + if len(pathMap) == 0 { + return common.InternalErrorJS(errors.New("no paths for attachment add"), w, r) + } + + var elemStr string + for path, aids := range pathMap { + elemStr += "\"" + path + "\":\"" + aids + "\"," + } + if len(elemStr) > 1 { + elemStr = elemStr[:len(elemStr)-1] + } + + w.Write([]byte(`{"success":"1","elems":[{` + elemStr + `}]}`)) + return nil +} + +func RemoveAttachFromTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError { + tid, err := strconv.Atoi(stid) + if err != nil { + return common.LocalErrorJS(phrases.GetErrorPhrase("id_must_be_integer"), w, r) + } + topic, err := common.Topics.Get(tid) + if err != nil { + return common.NotFoundJS(w, r) + } + + _, ferr := common.SimpleForumUserCheck(w, r, &user, topic.ParentID) + if ferr != nil { + return ferr + } + if !user.Perms.ViewTopic || !user.Perms.EditTopic { + return common.NoPermissionsJS(w, r, user) + } + if topic.IsClosed && !user.Perms.CloseTopic { + return common.NoPermissionsJS(w, r, user) + } + + for _, said := range strings.Split(r.PostFormValue("aids"), ",") { + aid, err := strconv.Atoi(said) + if err != nil { + return common.LocalErrorJS(phrases.GetErrorPhrase("id_must_be_integer"), w, r) + } + rerr := deleteAttachment(w, r, user, aid, true) + if rerr != nil { + // TODO: This needs to be a JS error... + return rerr + } + } + + w.Write(successJSONBytes) + return nil +} + // ? - Should we add a new permission or permission zone (like per-forum permissions) specifically for profile comment creation // ? - Should we allow banned users to make reports? How should we handle report abuse? // TODO: Add a permission to stop certain users from using custom avatars @@ -337,8 +437,6 @@ func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User) var maxPollOptions = 10 var pollInputItems = make(map[int]string) for key, values := range r.Form { - //common.DebugDetail("key: ", key) - //common.DebugDetailf("values: %+v\n", values) for _, value := range values { if strings.HasPrefix(key, "pollinputitem[") { halves := strings.Split(key, "[") @@ -389,72 +487,10 @@ func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User) } // Handle the file attachments - // TODO: Stop duplicating this code if user.Perms.UploadFiles { - files, ok := r.MultipartForm.File["upload_files"] - if ok { - if len(files) > 5 { - return common.LocalError("You can't attach more than five files", w, r, user) - } - - for _, file := range files { - if file.Filename == "" { - continue - } - common.DebugLog("file.Filename ", file.Filename) - extarr := strings.Split(file.Filename, ".") - if len(extarr) < 2 { - return common.LocalError("Bad file", w, r, user) - } - ext := extarr[len(extarr)-1] - - // TODO: Can we do this without a regex? - reg, err := regexp.Compile("[^A-Za-z0-9]+") - if err != nil { - return common.LocalError("Bad file extension", w, r, user) - } - ext = strings.ToLower(reg.ReplaceAllString(ext, "")) - if !common.AllowedFileExts.Contains(ext) { - return common.LocalError("You're not allowed to upload files with this extension", w, r, user) - } - - infile, err := file.Open() - if err != nil { - return common.LocalError("Upload failed", w, r, user) - } - defer infile.Close() - - hasher := sha256.New() - _, err = io.Copy(hasher, infile) - if err != nil { - return common.LocalError("Upload failed [Hashing Failed]", w, r, user) - } - infile.Close() - - checksum := hex.EncodeToString(hasher.Sum(nil)) - filename := checksum + "." + ext - outfile, err := os.Create("." + "/attachs/" + filename) - if err != nil { - return common.LocalError("Upload failed [File Creation Failed]", w, r, user) - } - defer outfile.Close() - - infile, err = file.Open() - if err != nil { - return common.LocalError("Upload failed", w, r, user) - } - defer infile.Close() - - _, err = io.Copy(outfile, infile) - if err != nil { - return common.LocalError("Upload failed [Copy Failed]", w, r, user) - } - - err = common.Attachments.Add(fid, "forums", tid, "topics", user.ID, filename) - if err != nil { - return common.InternalError(err, w, r) - } - } + _, rerr := uploadAttachment(w, r, user, fid, "forums", tid, "topics") + if rerr != nil { + return rerr } } @@ -464,6 +500,141 @@ func CreateTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User) return nil } +func uploadFilesWithHash(w http.ResponseWriter, r *http.Request, user common.User, dir string) (filenames []string, rerr common.RouteError) { + files, ok := r.MultipartForm.File["upload_files"] + if !ok { + return nil, nil + } + if len(files) > 5 { + return nil, common.LocalError("You can't attach more than five files", w, r, user) + } + + for _, file := range files { + if file.Filename == "" { + continue + } + //common.DebugLog("file.Filename ", file.Filename) + + extarr := strings.Split(file.Filename, ".") + if len(extarr) < 2 { + return nil, common.LocalError("Bad file", w, r, user) + } + ext := extarr[len(extarr)-1] + + // TODO: Can we do this without a regex? + reg, err := regexp.Compile("[^A-Za-z0-9]+") + if err != nil { + return nil, common.LocalError("Bad file extension", w, r, user) + } + ext = strings.ToLower(reg.ReplaceAllString(ext, "")) + if !common.AllowedFileExts.Contains(ext) { + return nil, common.LocalError("You're not allowed to upload files with this extension", w, r, user) + } + + infile, err := file.Open() + if err != nil { + return nil, common.LocalError("Upload failed", w, r, user) + } + defer infile.Close() + + hasher := sha256.New() + _, err = io.Copy(hasher, infile) + if err != nil { + return nil, common.LocalError("Upload failed [Hashing Failed]", w, r, user) + } + infile.Close() + + checksum := hex.EncodeToString(hasher.Sum(nil)) + filename := checksum + "." + ext + outfile, err := os.Create(dir + filename) + if err != nil { + return nil, common.LocalError("Upload failed [File Creation Failed]", w, r, user) + } + defer outfile.Close() + + infile, err = file.Open() + if err != nil { + return nil, common.LocalError("Upload failed", w, r, user) + } + defer infile.Close() + + _, err = io.Copy(outfile, infile) + if err != nil { + return nil, common.LocalError("Upload failed [Copy Failed]", w, r, user) + } + + filenames = append(filenames, filename) + } + + return filenames, nil +} + +// TODO: Add a table for the files and lock the file row when performing tasks related to the file +func deleteAttachment(w http.ResponseWriter, r *http.Request, user common.User, aid int, js bool) common.RouteError { + attach, err := common.Attachments.Get(aid) + if err == sql.ErrNoRows { + return common.NotFoundJSQ(w, r, nil, js) + } else if err != nil { + return common.InternalErrorJSQ(err, w, r, js) + } + + err = common.Attachments.Delete(aid) + if err != nil { + return common.InternalErrorJSQ(err, w, r, js) + } + + count := common.Attachments.CountInPath(attach.Path) + if err != nil { + return common.InternalErrorJSQ(err, w, r, js) + } + if count == 0 { + err := os.Remove("./attachs/" + attach.Path) + if err != nil { + return common.InternalErrorJSQ(err, w, r, js) + } + } + + return nil +} + +// TODO: Stop duplicating this code +// TODO: Use a transaction here +func uploadAttachment(w http.ResponseWriter, r *http.Request, user common.User, sid int, sectionTable string, oid int, originTable string) (pathMap map[string]string, rerr common.RouteError) { + pathMap = make(map[string]string) + files, rerr := uploadFilesWithHash(w, r, user, "./attachs/") + if rerr != nil { + return nil, rerr + } + + for _, filename := range files { + aid, err := common.Attachments.Add(sid, sectionTable, oid, originTable, user.ID, filename) + if err != nil { + return nil, common.InternalError(err, w, r) + } + + _, ok := pathMap[filename] + if ok { + pathMap[filename] += "," + strconv.Itoa(aid) + } else { + pathMap[filename] = strconv.Itoa(aid) + } + + switch sectionTable { + case "topics": + _, err = topicStmts.updateAttachs.Exec(common.Attachments.CountInTopic(oid), oid) + if err != nil { + return nil, common.InternalError(err, w, r) + } + err = common.Topics.Reload(oid) + if err != nil { + return nil, common.InternalError(err, w, r) + } + } + } + + return pathMap, nil +} + // TODO: Update the stats after edits so that we don't under or over decrement stats during deletes // TODO: Disable stat updates in posts handled by plugin_guilds func EditTopicSubmit(w http.ResponseWriter, r *http.Request, user common.User, stid string) common.RouteError { diff --git a/templates/forum.html b/templates/forum.html index e70efc12..47809f78 100644 --- a/templates/forum.html +++ b/templates/forum.html @@ -25,21 +25,8 @@ {{end}} {{if .CurrentUser.Loggedin}} -
-
-
- -
-
- - -
-
-
+ {{template "topics_mod_floater.html"}} + {{if .CurrentUser.Perms.CreateTopic}} diff --git a/templates/topic_alt.html b/templates/topic_alt.html index 26fbf66f..3a192f79 100644 --- a/templates/topic_alt.html +++ b/templates/topic_alt.html @@ -77,7 +77,26 @@
{{.Topic.ContentHTML}}
- {{if .CurrentUser.Loggedin}}{{if .CurrentUser.Perms.EditTopic}}{{end}}{{end}} + {{if .CurrentUser.Loggedin}}{{if .CurrentUser.Perms.EditTopic}} + + {{if .Topic.Attachments}}
+ {{range .Topic.Attachments}} +
+ {{if .Image}}{{end}} + {{.Path}} + + +
+ {{end}} +
+ {{if .CurrentUser.Perms.UploadFiles}} + + {{end}} + +
+
{{end}} + + {{end}}{{end}}
{{if .CurrentUser.Loggedin}} @@ -97,7 +116,7 @@
diff --git a/templates/topic_alt_posts.html b/templates/topic_alt_posts.html index 33505cb9..6d77cd7f 100644 --- a/templates/topic_alt_posts.html +++ b/templates/topic_alt_posts.html @@ -28,7 +28,7 @@
{{.LikeCount}} - {{.RelativeCreatedAt}} + {{reltime .CreatedAt}} {{if $.CurrentUser.Loggedin}}{{if $.CurrentUser.Perms.ViewIPs}}{{end}}{{end}}
diff --git a/templates/topics.html b/templates/topics.html index b61bb1e2..da3487a9 100644 --- a/templates/topics.html +++ b/templates/topics.html @@ -32,22 +32,7 @@ {{if .CurrentUser.Loggedin}} -{{/** TODO: Hide these from unauthorised users? **/}} -
-
-
- -
-
- - -
-
-
+{{template "topics_mod_floater.html"}} {{if .ForumList}} {{/** TODO: Have a seperate forum list for moving topics? Maybe an AJAX forum search compatible with plugin_guilds? **/}} diff --git a/templates/topics_mod_floater.html b/templates/topics_mod_floater.html new file mode 100644 index 00000000..7b4d0a9d --- /dev/null +++ b/templates/topics_mod_floater.html @@ -0,0 +1,16 @@ +{{/** TODO: Hide these from unauthorised users? **/}} +
+
+
+ +
+
+ + +
+
+
\ No newline at end of file diff --git a/templates/topics_topic.html b/templates/topics_topic.html index fbdc0a5a..989464dc 100644 --- a/templates/topics_topic.html +++ b/templates/topics_topic.html @@ -27,7 +27,7 @@ {{.LastUser.Name}}'s Avatar {{.LastUser.Name}}
- {{.RelativeLastReplyAt}} + {{reltime .LastReplyAt}}
diff --git a/themes/cosora/public/main.css b/themes/cosora/public/main.css index d7d1f4db..5422d268 100644 --- a/themes/cosora/public/main.css +++ b/themes/cosora/public/main.css @@ -1006,8 +1006,6 @@ textarea { padding-right: 42px; padding-bottom: 18px; height: min-content; - /*overflow: hidden; - text-overflow: ellipsis;*/ } .user_meta { display: flex; @@ -1129,6 +1127,9 @@ textarea { content: "{{lang "topic.report_button_text" .}}"; } +.attach_edit_bay { + display: none; +} .zone_view_topic .pageset { margin-bottom: 14px; } diff --git a/themes/nox/public/main.css b/themes/nox/public/main.css index 3c1912f5..e66b9589 100644 --- a/themes/nox/public/main.css +++ b/themes/nox/public/main.css @@ -293,12 +293,17 @@ h2 { .quick_create_form .topic_meta { display: flex; } +.quick_create_form input, .quick_create_form select { + margin-left: 0px; + margin-bottom: 0px; +} .quick_create_form .topic_meta .topic_name_row { margin-bottom: 8px; width: 100%; + font-size: 14px; } .quick_create_form .topic_meta .topic_name_row:not(:only-child) { - margin-left: 8px; + margin-left: 6px; } .quick_create_form .topic_meta .topic_name_row:only-child input { margin-left: 0px; @@ -623,12 +628,24 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) { .topic_view_count:after { content: "{{lang "topic.view_count_suffix" . }}"; } +.edithead { + margin-left: 0px; + margin-bottom: 10px; +} .topic_name_input { width: 100%; - margin-right: 12px; + margin-right: 10px; + margin-bottom: 0px; + margin-left: 0px; + margin-left: 0px; } .topic_item .submit_edit { - margin-right: 16px; + /*margin-right: 16px;*/ +} +.zone_view_topic button, .zone_view_topic .formbutton { + padding: 5px; + padding-top: 4px; + padding-bottom: 4px; } .postImage { width: 100%; @@ -688,7 +705,7 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) { flex-direction: column; color: #bbbbbb; } -.action_item .content_container, .post_item .user_content { +.action_item .content_container, .post_item .user_content, .post_item .button_container { background-color: #444444; border-radius: 3px; padding: 16px; @@ -698,8 +715,6 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) { margin-top: 8px; margin-bottom: auto; padding: 14px; - background-color: #444444; - border-radius: 3px; } .post_item .action_button { margin-right: 5px; @@ -713,11 +728,7 @@ button, .formbutton, .panel_right_button:not(.has_inner_button) { .post_item .action_button_right { margin-left: auto; } -.post_item .controls:not(.has_likes) .like_count { - display: none; -} - -.action_item .userinfo, .action_item .action_icon { +.post_item .controls:not(.has_likes) .like_count, .action_item .userinfo, .action_item .action_icon { display: none; } .action_item .content_container { @@ -788,6 +799,49 @@ input[type=checkbox]:checked + label .sel { content: "{{lang "topic.like_count_suffix" . }}"; } +/*.attach_edit_bay { + display: flex; + flex-direction: row; +}*/ +.attach_item { + display: flex; + background-color: #444444; + border-radius: 4px; + margin-top: 8px; + padding: 6px; + text-overflow: ellipsis; + overflow: hidden; +} +.attach_item_selected { + background-color: #446644 +} +.attach_item img { + margin-right: 8px; + border-radius: 4px; +} +.attach_image_holder span { + margin-bottom: 4px; +} +.attach_edit_bay button { + margin-top: 8px; + margin-left: 8px; +} + +/* New */ +.attach_item { + padding: 8px; + width: 100%; +} +.attach_image_holder span { + margin-right: auto; + overflow: hidden; + text-overflow: ellipsis; + width: 300px; +} +.attach_item button { + margin-top: -1px; +} + .zone_view_topic .pageset { margin-bottom: 14px; } diff --git a/themes/nox/public/misc.js b/themes/nox/public/misc.js index 7a3765d1..64b546cc 100644 --- a/themes/nox/public/misc.js +++ b/themes/nox/public/misc.js @@ -12,7 +12,9 @@ $(".alerts").html(alertCount + " new alerts"); $(".user_box").addClass("has_alerts"); } - }) + }); + addHook("open_edit", () => $('.topic_block').addClass("edithead")); + addHook("close_edit", () => $('.topic_block').removeClass("edithead")); })(); $(document).ready(() => {