From 3465e4c08f1eb9a30a31c409a8ccd01ad93db3a2 Mon Sep 17 00:00:00 2001 From: Azareal Date: Thu, 27 Dec 2018 15:42:41 +1000 Subject: [PATCH] You can now manage the attachments for an opening post by hitting edit. The update system now uses the database as the source of truth for the last version rather than lastSchema.json Refactored several structs and bits of code, so we can avoid allocations for contexts where we never use a relative time. Clicking on the relative times on the topic list and the forum page should now take you to the post on the last page rather than just the last page. Added the reltime template function. Fixed some obsolete bits of code. Fixed some spelling mistakes. Fixed a bug where MaxBytesReader was capped at the maxFileSize rather than r.ContentLength. All of the client side templates should work again now. Shortened some statement names to save some horizontal space. accUpdateBuilder and SimpleUpdate now use updatePrebuilder behind the scenes to simplify things. Renamed selectItem to builder in AccSelectBuilder. Added a Total() method to accCountBuilder to reduce the amount of boilerplate used for row count queries. The "_builder" strings have been replaced with empty strings to help save memory, to make things slightly faster and to open the door to removing the query name in many contexts down the line. Added the open_edit and close_edit client hooks. Removed many query name checks. Split the attachment logic into separate functions and de-duplicated it between replies and topics. Improved the UI for editing topics in Nox. Used type aliases to reduce the amount of boilerplate in tables.go and patches.go Reduced the amount of boilerplate in the action post logic. Eliminated a map and a slice in the topic page for users who haven't given any likes. E.g. Guests. Fixed some long out-dated parts of the update instructions. Updated the update instructions to remove mention of the obsolete lastSchema.json Fixed a bug in init.js where /api/me was being loaded for guests. Added the MiniTopicGet, GlobalCount and CountInTopic methods to AttachmentStore. Added the MiniAttachment struct. Split the mod floaters out into their own template to reduce duplication. Removed a couple of redundant ParseForms. Added the common.skipUntilIfExistsOrLine function. Added the NotFoundJS and NotFoundJSQ functions. Added the lastReplyID and attachCount columns to the topics table. --- .gitignore | 1 - cmd/query_gen/tables.go | 799 +++++++++++++++--------------- common/attachments.go | 110 +++- common/common.go | 3 +- common/errors.go | 19 +- common/files.go | 56 +-- common/menus.go | 12 + common/profile_reply.go | 21 +- common/reply.go | 76 ++- common/reply_store.go | 2 +- common/routes_common.go | 2 +- common/template_init.go | 23 +- common/templates/templates.go | 11 + common/topic.go | 219 ++++---- common/topic_list.go | 6 +- common/topic_store.go | 10 +- common/utils.go | 3 +- common/ws_hub.go | 2 +- dev-update-linux | 2 - dev-update-travis | 1 - dev-update.bat | 4 - docs/updating.md | 20 +- gen_router.go | 180 ++++--- misc_test.go | 2 +- patcher/main.go | 37 +- patcher/patches.go | 215 ++++---- public/global.js | 208 ++++++-- public/init.js | 4 +- query_gen/acc_builders.go | 103 ++-- query_gen/accumulator.go | 54 +- query_gen/builder.go | 52 +- query_gen/micro_builders.go | 34 +- query_gen/mssql.go | 58 +-- query_gen/mysql.go | 97 ++-- query_gen/pgsql.go | 65 +-- query_gen/querygen.go | 3 +- query_gen/transaction.go | 8 +- quick-update-linux | 8 +- router_gen/routes.go | 6 +- routes.go | 3 + routes/forum.go | 5 +- routes/profile.go | 6 +- routes/reply.go | 78 +-- routes/topic.go | 327 +++++++++--- templates/forum.html | 19 +- templates/topic_alt.html | 23 +- templates/topic_alt_posts.html | 2 +- templates/topics.html | 17 +- templates/topics_mod_floater.html | 16 + templates/topics_topic.html | 2 +- themes/cosora/public/main.css | 5 +- themes/nox/public/main.css | 76 ++- themes/nox/public/misc.js | 4 +- 53 files changed, 1786 insertions(+), 1333 deletions(-) create mode 100644 templates/topics_mod_floater.html 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 @@
- {{.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(() => {