diff --git a/README.md b/README.md
index c7aadffa..d854f7f3 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,10 @@
-# Grosolo
+# Gosora
A super fast forum software written in Go.
-The initial code-base was forked from one of my side projects, and converted from the web framework it was using.
+The initial code-base was forked from one of my side projects, but has now gone far beyond that.
+
+Discord Server: https://discord.gg/eyYvtTf
# Features
@@ -16,6 +18,8 @@ In-memory static file, forum and group caches.
A profile system including profile comments and moderation tools for the profile owner.
+A plugin system.
+
# Dependencies
@@ -46,10 +50,32 @@ Add -u after go get to update those libraries, if you've already got them instal
# Run the program
-go run errors.go main.go pages.go reply.go routes.go topic.go user.go utils.go forum.go group.go files.go config.go
+*Linux*
+
+cd to the directory / folder the code is in.
+
+go build
+
+./gosora
+
+
+*Windows*
+
+Open up cmd.exe
+
+cd to the directory / folder the code is in. E.g. cd /Users/Blah/Documents/gosora
+
+go build
+
+./gosora.exe
+
Alternatively, you could run the run.bat batch file on Windows.
+We're also looking into ways to distribute ready made executables for Windows. While this is not a complicated endeavour, the configuration settings currently get built with the rest of the program for speed, and we will likely have to change this.
+
+With the introduction of the new settings system, we will begin moving some of the less critical settings out of the configuration file, and will likely have a config.xml or config.ini in the future to store the critical settings in.
+
# TO-DO
@@ -64,12 +90,8 @@ Add emails as a requirement for registration and add a simple anti-spam measure.
Add an alert system.
-Add a report feature.
-
Add a complex permissions system.
-Add a settings system.
-
Add a plugin system.
Tweak the CSS to make it responsive.
@@ -77,3 +99,5 @@ Tweak the CSS to make it responsive.
Nest the moderation routes to possibly speed routing up a little...?
Add a friend system.
+
+Add more administration features.
diff --git a/config.go b/config.go
index d695304f..09c48462 100644
--- a/config.go
+++ b/config.go
@@ -4,7 +4,7 @@ package main
var dbhost = "127.0.0.1"
var dbuser = "root"
var dbpassword = "password"
-var dbname = "grosolo"
+var dbname = "gosora"
var dbport = "3306" // You probably won't need to change this
// Limiters
diff --git a/data.sql b/data.sql
index 6c536d57..1cd09579 100644
--- a/data.sql
+++ b/data.sql
@@ -53,6 +53,7 @@ CREATE TABLE `topics`(
`is_closed` tinyint DEFAULT 0 not null,
`sticky` tinyint DEFAULT 0 not null,
`parentID` int DEFAULT 1 not null,
+ `data` varchar(200) DEFAULT '' not null,
primary key(`tid`)
) CHARSET=utf8mb4 COLLATE utf8mb4_general_ci;
@@ -87,6 +88,12 @@ CREATE TABLE `settings`(
unique(`name`)
);
+CREATE TABLE `plugins`(
+ `uname` varchar(200) not null,
+ `active` tinyint DEFAULT 0 not null,
+ unique(`uname`)
+);
+
INSERT INTO settings(`name`,`content`,`type`) VALUES ('url_tags','1','bool');
INSERT INTO users(`name`,`group`,`is_super_admin`,`createdAt`,`lastActiveAt`,`message`)
diff --git a/extend.go b/extend.go
new file mode 100644
index 00000000..0db39266
--- /dev/null
+++ b/extend.go
@@ -0,0 +1,25 @@
+/* Copyright Azareal 2016 - 2017 */
+package main
+
+var plugins map[string]Plugin = make(map[string]Plugin)
+var hooks map[string]func(interface{})interface{} = make(map[string]func(interface{})interface{})
+
+type Plugin struct
+{
+ UName string
+ Name string
+ Author string
+ URL string
+ Settings string
+ Active bool
+ Type string
+ Init func()
+}
+
+func add_hook(name string, handler func(interface{})interface{}) {
+ hooks[name] = handler
+}
+
+func run_hook(name string, data interface{}) interface{} {
+ return hooks[name](data)
+}
\ No newline at end of file
diff --git a/extend/filler.txt b/extend/filler.txt
new file mode 100644
index 00000000..20e14b1e
--- /dev/null
+++ b/extend/filler.txt
@@ -0,0 +1 @@
+This file is here so that Git will include this folder in the repository.
\ No newline at end of file
diff --git a/grosolo-linux b/gosora-linux
similarity index 50%
rename from grosolo-linux
rename to gosora-linux
index a1cc146d..28ccc10f 100644
--- a/grosolo-linux
+++ b/gosora-linux
@@ -1,2 +1,2 @@
go build
-./Grosolo
\ No newline at end of file
+./Gosora
\ No newline at end of file
diff --git a/grosolo.exe b/gosora.exe
similarity index 60%
rename from grosolo.exe
rename to gosora.exe
index c546ad92..c6a56616 100644
Binary files a/grosolo.exe and b/gosora.exe differ
diff --git a/images/report.PNG b/images/report.PNG
new file mode 100644
index 00000000..02ac5eb4
Binary files /dev/null and b/images/report.PNG differ
diff --git a/main.go b/main.go
index 6dd13d13..5dc4730b 100644
--- a/main.go
+++ b/main.go
@@ -1,8 +1,10 @@
+/* Copyright Azareal 2016 - 2017 */
package main
import (
"net/http"
"log"
+ //"fmt"
"mime"
"strings"
"path/filepath"
@@ -56,6 +58,17 @@ func main(){
}
external_sites["YT"] = "https://www.youtube.com/"
+ hooks["trow_assign"] = nil
+ hooks["rrow_assign"] = nil
+ //fmt.Println(plugins)
+
+ for name, body := range plugins {
+ log.Print("Added plugin " + name)
+ if body.Active {
+ log.Print("Initialised plugin " + name)
+ plugins[name].Init()
+ }
+ }
// In a directory to stop it clashing with the other paths
http.HandleFunc("/static/", route_static)
@@ -77,6 +90,7 @@ func main(){
//http.HandleFunc("/reply/delete/", route_reply_delete)
http.HandleFunc("/reply/edit/submit/", route_reply_edit_submit)
http.HandleFunc("/reply/delete/submit/", route_reply_delete_submit)
+ http.HandleFunc("/report/submit/", route_report_submit)
http.HandleFunc("/topic/edit/submit/", route_edit_topic)
http.HandleFunc("/topic/delete/submit/", route_delete_topic)
http.HandleFunc("/topic/stick/submit/", route_stick_topic)
@@ -119,6 +133,9 @@ func main(){
http.HandleFunc("/panel/settings/", route_panel_settings)
http.HandleFunc("/panel/settings/edit/", route_panel_setting)
http.HandleFunc("/panel/settings/edit/submit/", route_panel_setting_edit)
+ http.HandleFunc("/panel/plugins/", route_panel_plugins)
+ http.HandleFunc("/panel/plugins/activate/", route_panel_plugins_activate)
+ //http.HandleFunc("/panel/plugins/deactivate/", route_panel_plugins_deactivate)
http.HandleFunc("/", default_route)
diff --git a/mod_routes.go b/mod_routes.go
index 56a93eea..38f1d344 100644
--- a/mod_routes.go
+++ b/mod_routes.go
@@ -694,4 +694,66 @@ func route_panel_setting_edit(w http.ResponseWriter, r *http.Request) {
return
}
http.Redirect(w,r,"/panel/settings/",http.StatusSeeOther)
+}
+
+func route_panel_plugins(w http.ResponseWriter, r *http.Request){
+ user := SessionCheck(w,r)
+ if !user.Is_Admin {
+ NoPermissions(w,r,user)
+ return
+ }
+
+ var pluginList map[int]interface{} = make(map[int]interface{})
+ currentID := 0
+
+ for _, plugin := range plugins {
+ pluginList[currentID] = plugin
+ currentID++
+ }
+
+ pi := Page{"Plugin Manager","panel-plugins",user,pluginList,0}
+ templates.ExecuteTemplate(w,"panel-plugins.html", pi)
+}
+
+func route_panel_plugins_activate(w http.ResponseWriter, r *http.Request){
+ user := SessionCheck(w,r)
+ if !user.Is_Admin {
+ NoPermissions(w,r,user)
+ return
+ }
+
+ uname := r.URL.Path[len("/panel/plugins/activate/"):]
+
+ plugin, ok := plugins[uname]
+ if !ok {
+ LocalError("The plugin isn't registered in the system",w,r,user)
+ return
+ }
+
+ var active bool
+ err := db.QueryRow("SELECT active from plugins where uname = ?", uname).Scan(&active)
+ if err != nil && err != sql.ErrNoRows {
+ InternalError(err,w,r,user)
+ return
+ }
+
+ has_plugin := err != sql.ErrNoRows
+ if has_plugin {
+ if active {
+ LocalError("The plugin is already active",w,r,user)
+ return
+ }
+ } else {
+ _, err := add_plugin_stmt.Exec(uname,1)
+ if err != nil {
+ InternalError(err,w,r,user)
+ return
+ }
+ }
+
+ plugin.Active = true
+ plugins[uname] = plugin
+ plugins[uname].Init()
+
+ http.Redirect(w,r,"/panel/plugins/",http.StatusSeeOther)
}
\ No newline at end of file
diff --git a/mysql.go b/mysql.go
index 406668f1..a99f9cb1 100644
--- a/mysql.go
+++ b/mysql.go
@@ -1,3 +1,4 @@
+/* Copyright Azareal 2016 - 2017 */
package main
import "database/sql"
@@ -8,6 +9,7 @@ import "log"
var db *sql.DB
var get_session_stmt *sql.Stmt
var create_topic_stmt *sql.Stmt
+var create_report_stmt *sql.Stmt
var create_reply_stmt *sql.Stmt
var update_forum_cache_stmt *sql.Stmt
var edit_topic_stmt *sql.Stmt
@@ -34,6 +36,7 @@ var create_forum_stmt *sql.Stmt
var delete_forum_stmt *sql.Stmt
var update_forum_stmt *sql.Stmt
var update_setting_stmt *sql.Stmt
+var add_plugin_stmt *sql.Stmt
func init_database(err error) {
if(dbpassword != ""){
@@ -62,6 +65,12 @@ func init_database(err error) {
log.Fatal(err)
}
+ log.Print("Preparing create_report statement.")
+ create_report_stmt, err = db.Prepare("INSERT INTO topics(title,content,parsed_content,createdAt,createdBy,data,parentID) VALUES(?,?,?,NOW(),?,?,-1)")
+ if err != nil {
+ log.Fatal(err)
+ }
+
log.Print("Preparing create_reply statement.")
create_reply_stmt, err = db.Prepare("INSERT INTO replies(tid,content,parsed_content,createdAt,createdBy) VALUES(?,?,?,NOW(),?)")
if err != nil {
@@ -215,6 +224,12 @@ func init_database(err error) {
log.Fatal(err)
}
+ log.Print("Preparing add_plugin statement.")
+ add_plugin_stmt, err = db.Prepare("INSERT INTO plugins(uname,active) VALUES(?,?)")
+ if err != nil {
+ log.Fatal(err)
+ }
+
log.Print("Loading the usergroups.")
rows, err := db.Query("SELECT gid,name,permissions,is_mod,is_admin,is_banned,tag FROM users_groups")
if err != nil {
@@ -295,4 +310,27 @@ func init_database(err error) {
if err != nil {
log.Fatal(err)
}
+
+ log.Print("Loading the plugins.")
+ rows, err = db.Query("SELECT uname, active FROM plugins")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer rows.Close()
+
+ var uname string
+ var active bool
+ for rows.Next() {
+ err := rows.Scan(&uname, &active)
+ if err != nil {
+ log.Fatal(err)
+ }
+ plugin := plugins[uname]
+ plugin.Active = active
+ plugins[uname] = plugin
+ }
+ err = rows.Err()
+ if err != nil {
+ log.Fatal(err)
+ }
}
diff --git a/plugin_helloworld.go b/plugin_helloworld.go
new file mode 100644
index 00000000..e18d1ee2
--- /dev/null
+++ b/plugin_helloworld.go
@@ -0,0 +1,19 @@
+package main
+import "html/template"
+
+func init() {
+ plugins["helloworld"] = Plugin{"helloworld","Hello World","Azareal","http://github.com/Azareal","",false,"",init_helloworld}
+}
+
+// init_helloworld is separate from init() as we don't want the plugin to run if the plugin is disabled
+func init_helloworld() {
+ add_hook("rrow_assign", helloworld_reply)
+}
+
+func helloworld_reply(data interface{}) interface{} {
+ reply := data.(Reply)
+ reply.Content = "Hello World!"
+ reply.ContentHtml = template.HTML("Hello World!")
+ reply.Tag = "Automated"
+ return reply
+}
\ No newline at end of file
diff --git a/plugin_skeleton.go b/plugin_skeleton.go
new file mode 100644
index 00000000..b2f78ced
--- /dev/null
+++ b/plugin_skeleton.go
@@ -0,0 +1,17 @@
+package main
+
+func init() {
+ /*
+ The UName field should match the name in the URL minus plugin_ and the file extension. The same name as the map index. Please choose a unique name which won't clash with any other plugins.
+ The Name field is for the friendly name of the plugin shown to the end-user.
+ The Author field is the author of this plugin. The one who created it.
+ The URL field is for the URL pointing to the location where you can download this plugin.
+ The Settings field points to the route for managing the settings for this plugin. Coming soon.
+ The Active field should always be set to false in the init() function of a plugin. It's used internally by the software to determine whether an admin has enabled a plugin or not and whether to run it. This will be overwritten by the user's preference.
+ The Type field is for the type of the plugin. This gets changed to "go" automatically and we would suggest leaving "".
+ The Init field is for the initialisation handler which is called by the software to run this plugin. This expects a function. You should add your hooks, init logic, initial queries, etc. in said function.
+ */
+ plugins["skeleton"] = Plugin{"skeleton","Skeleton","Azareal","","",false,"",init_test}
+}
+
+func init_test() {}
diff --git a/reply.go b/reply.go
index 82aec635..f7c5c806 100644
--- a/reply.go
+++ b/reply.go
@@ -1,3 +1,4 @@
+/* Copyright Azareal 2016 - 2017 */
package main
import "html/template"
diff --git a/routes.go b/routes.go
index 47f4469f..2c839734 100644
--- a/routes.go
+++ b/routes.go
@@ -1,3 +1,4 @@
+/* Copyright Azareal 2016 - 2017 */
package main
import "errors"
@@ -110,6 +111,10 @@ func route_topics(w http.ResponseWriter, r *http.Request){
}
topicList[currentID] = TopicUser{tid,title,content,createdBy,is_closed,sticky, createdAt,parentID,status,name,avatar,"",0,"","","",""}
+
+ if hooks["trow_assign"] != nil {
+ topicList[currentID] = run_hook("trow_assign", topicList[currentID])
+ }
currentID++
}
err = rows.Err()
@@ -193,6 +198,10 @@ func route_forum(w http.ResponseWriter, r *http.Request){
}
topicList[currentID] = TopicUser{tid,title,content,createdBy,is_closed,sticky,createdAt,parentID,status,name,avatar,"",0,"","","",""}
+
+ if hooks["trow_assign"] != nil {
+ topicList[currentID] = run_hook("trow_assign", topicList[currentID])
+ }
currentID++
}
err = rows.Err()
@@ -364,6 +373,10 @@ func route_topic_id(w http.ResponseWriter, r *http.Request){
}
replyList[currentID] = Reply{rid,topic.ID,replyContent,template.HTML(parse_message(replyContent)),replyCreatedBy,replyCreatedByName,replyCreatedAt,replyLastEdit,replyLastEditBy,replyAvatar,replyCss,replyLines,replyTag,replyURL,replyURLPrefix,replyURLName}
+
+ if hooks["rrow_assign"] != nil {
+ replyList[currentID] = run_hook("rrow_assign", replyList[currentID])
+ }
currentID++
}
err = rows.Err()
@@ -692,6 +705,128 @@ func route_profile_reply_create(w http.ResponseWriter, r *http.Request) {
}
}
+func route_report_submit(w http.ResponseWriter, r *http.Request) {
+ user := SessionCheck(w,r)
+ if !user.Loggedin {
+ LoginRequired(w,r,user)
+ return
+ }
+ if user.Is_Banned {
+ Banned(w,r,user)
+ return
+ }
+
+ err := r.ParseForm()
+ if err != nil {
+ LocalError("Bad Form", w, r, user)
+ return
+ }
+
+ if r.FormValue("session") != user.Session {
+ SecurityError(w,r,user)
+ return
+ }
+ item_id, err := strconv.Atoi(r.URL.Path[len("/report/submit/"):])
+ if err != nil {
+ LocalError("Bad ID", w, r, user)
+ return
+ }
+
+ item_type := r.FormValue("type")
+ success := 1
+
+ var tid int
+ var title string
+ var content string
+ var data string
+ if item_type == "reply" {
+ err = db.QueryRow("select tid, content from replies where rid = ?", item_id).Scan(&tid, &content)
+ if err == sql.ErrNoRows {
+ LocalError("We were unable to find the reported post", w, r, user)
+ return
+ } else if err != nil {
+ InternalError(err,w,r,user)
+ return
+ }
+
+ err = db.QueryRow("select title, data from topics where tid = ?", tid).Scan(&title,&data)
+ if err == sql.ErrNoRows {
+ LocalError("We were unable to find the topic which the reported post is supposed to be in", w, r, user)
+ return
+ } else if err != nil {
+ InternalError(err,w,r,user)
+ return
+ }
+ content = content + "
Original Post: " + title + ""
+ } else if item_type == "topic" {
+ err = db.QueryRow("select title, content from topics where tid = ?", item_id).Scan(&title,&content)
+ if err == sql.ErrNoRows {
+ NotFound(w,r,user)
+ return
+ } else if err != nil {
+ InternalError(err,w,r,user)
+ return
+ }
+ content = content + "
Original Post: " + title + ""
+ } else {
+ // Don't try to guess the type
+ LocalError("Unknown type", w, r, user)
+ return
+ }
+
+ var count int
+ rows, err := db.Query("select count(*) as count from topics where data = ? and data != '' and parentID = -1", item_type + "_" + strconv.Itoa(item_id))
+ if err != nil && err != sql.ErrNoRows {
+ InternalError(err,w,r,user)
+ return
+ }
+
+ for rows.Next() {
+ err = rows.Scan(&count)
+ if err != nil {
+ InternalError(err,w,r,user)
+ return
+ }
+ }
+
+ if count != 0 {
+ LocalError("Someone has already reported this!", w, r, user)
+ return
+ }
+
+ title = "Report: " + title
+ res, err := create_report_stmt.Exec(title,content,content,user.ID,item_type + "_" + strconv.Itoa(item_id))
+ if err != nil {
+ log.Print(err)
+ success = 0
+ }
+
+ lastId, err := res.LastInsertId()
+ if err != nil {
+ log.Print(err)
+ success = 0
+ }
+
+ _, err = update_forum_cache_stmt.Exec(title, lastId, user.Name, user.ID, 1)
+ if err != nil {
+ InternalError(err,w,r,user)
+ return
+ }
+
+ if success != 1 {
+ errmsg := "Unable to create the report"
+ pi := Page{"Error","error",user,tList,errmsg}
+
+ var b bytes.Buffer
+ templates.ExecuteTemplate(&b,"error.html", pi)
+ errpage := b.String()
+ w.WriteHeader(500)
+ fmt.Fprintln(w,errpage)
+ } else {
+ http.Redirect(w, r, "/topic/" + strconv.FormatInt(lastId, 10), http.StatusSeeOther)
+ }
+}
+
func route_account_own_edit_critical(w http.ResponseWriter, r *http.Request) {
user := SessionCheck(w,r)
if !user.Loggedin {
diff --git a/run.bat b/run.bat
index 97bf2c4c..d939fc2a 100644
--- a/run.bat
+++ b/run.bat
@@ -1,3 +1,3 @@
go build
-grosolo.exe
+gosora.exe
pause
\ No newline at end of file
diff --git a/templates/panel-forums.html b/templates/panel-forums.html
index 7e89f3b0..a744e7a2 100644
--- a/templates/panel-forums.html
+++ b/templates/panel-forums.html
@@ -3,6 +3,8 @@