diff --git a/data.sql b/data.sql
index 1cd09579..75db8755 100644
--- a/data.sql
+++ b/data.sql
@@ -6,6 +6,7 @@ CREATE TABLE `users`(
`password` varchar(100) not null,
`salt` varchar(80) DEFAULT '' not null,
`group` int not null,
+ `active` tinyint DEFAULT 0 not null,
`is_super_admin` tinyint(1) not null,
`createdAt` datetime not null,
`lastActiveAt` datetime not null,
@@ -99,7 +100,7 @@ INSERT INTO settings(`name`,`content`,`type`) VALUES ('url_tags','1','bool');
INSERT INTO users(`name`,`group`,`is_super_admin`,`createdAt`,`lastActiveAt`,`message`)
VALUES ('Admin',1,1,NOW(),NOW(),'');
-INSERT INTO users_groups(`name`,`permissions`,`is_mod`,`is_admin`,`tag`) VALUES ('Administrator','{}',1,1,"Admin");
+INSERT INTO users_groups(`name`,`permissions`,`active`,`is_mod`,`is_admin`,`tag`) VALUES ('Administrator','{}',1,1,1,"Admin");
INSERT INTO users_groups(`name`,`permissions`,`is_mod`,`tag`) VALUES ('Moderator','{}',1,"Mod");
INSERT INTO users_groups(`name`,`permissions`) VALUES ('Member','{}');
INSERT INTO users_groups(`name`,`permissions`,`is_banned`) VALUES ('Banned','{}',1);
diff --git a/extend.go b/extend.go
index 0db39266..0d483fd5 100644
--- a/extend.go
+++ b/extend.go
@@ -3,6 +3,7 @@ package main
var plugins map[string]Plugin = make(map[string]Plugin)
var hooks map[string]func(interface{})interface{} = make(map[string]func(interface{})interface{})
+var vhooks map[string]func(...interface{})interface{} = make(map[string]func(...interface{})interface{})
type Plugin struct
{
@@ -14,12 +15,22 @@ type Plugin struct
Active bool
Type string
Init func()
+ Activate func()
+ Deactivate func()
}
func add_hook(name string, handler func(interface{})interface{}) {
hooks[name] = handler
}
+func remove_hook(name string) {
+ delete(hooks, name)
+}
+
func run_hook(name string, data interface{}) interface{} {
return hooks[name](data)
+}
+
+func run_hook_v(name string, data ...interface{}) {
+ vhooks[name](data...)
}
\ No newline at end of file
diff --git a/gosora.exe b/gosora.exe
index c6a56616..d200d06e 100644
Binary files a/gosora.exe and b/gosora.exe differ
diff --git a/images/edit_setting.PNG b/images/edit_setting.PNG
new file mode 100644
index 00000000..b9ffa77d
Binary files /dev/null and b/images/edit_setting.PNG differ
diff --git a/images/new-topic-list.PNG b/images/new-topic-list.PNG
new file mode 100644
index 00000000..2249ae42
Binary files /dev/null and b/images/new-topic-list.PNG differ
diff --git a/images/plugins.PNG b/images/plugins.PNG
new file mode 100644
index 00000000..1ef02107
Binary files /dev/null and b/images/plugins.PNG differ
diff --git a/images/settings.PNG b/images/settings.PNG
new file mode 100644
index 00000000..3559947e
Binary files /dev/null and b/images/settings.PNG differ
diff --git a/images/users.PNG b/images/users.PNG
new file mode 100644
index 00000000..b79f7745
Binary files /dev/null and b/images/users.PNG differ
diff --git a/main.go b/main.go
index 5dc4730b..5434c92d 100644
--- a/main.go
+++ b/main.go
@@ -135,7 +135,8 @@ func main(){
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("/panel/plugins/deactivate/", route_panel_plugins_deactivate)
+ http.HandleFunc("/panel/users/", route_panel_users)
http.HandleFunc("/", default_route)
diff --git a/mod_routes.go b/mod_routes.go
index 38f1d344..2c3e5fe3 100644
--- a/mod_routes.go
+++ b/mod_routes.go
@@ -2,6 +2,7 @@ package main
import "log"
import "fmt"
+import "strings"
import "strconv"
import "net/http"
import "html"
@@ -723,7 +724,6 @@ func route_panel_plugins_activate(w http.ResponseWriter, r *http.Request){
}
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)
@@ -743,6 +743,11 @@ func route_panel_plugins_activate(w http.ResponseWriter, r *http.Request){
LocalError("The plugin is already active",w,r,user)
return
}
+ _, err = update_plugin_stmt.Exec(1, uname)
+ if err != nil {
+ InternalError(err,w,r,user)
+ return
+ }
} else {
_, err := add_plugin_stmt.Exec(uname,1)
if err != nil {
@@ -756,4 +761,108 @@ func route_panel_plugins_activate(w http.ResponseWriter, r *http.Request){
plugins[uname].Init()
http.Redirect(w,r,"/panel/plugins/",http.StatusSeeOther)
+}
+
+func route_panel_plugins_deactivate(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/deactivate/"):]
+ 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 == sql.ErrNoRows {
+ LocalError("The plugin you're trying to deactivate isn't active",w,r,user)
+ return
+ } else if err != nil {
+ InternalError(err,w,r,user)
+ return
+ }
+
+ if !active {
+ LocalError("The plugin you're trying to deactivate isn't active",w,r,user)
+ return
+ }
+ _, err = update_plugin_stmt.Exec(0, uname)
+ if err != nil {
+ InternalError(err,w,r,user)
+ return
+ }
+
+ plugin.Active = false
+ plugins[uname] = plugin
+ plugins[uname].Deactivate()
+
+ http.Redirect(w,r,"/panel/plugins/",http.StatusSeeOther)
+}
+
+func route_panel_users(w http.ResponseWriter, r *http.Request){
+ user := SessionCheck(w,r)
+ if !user.Is_Admin {
+ NoPermissions(w,r,user)
+ return
+ }
+
+ var userList map[int]interface{} = make(map[int]interface{})
+ currentID := 0
+
+ rows, err := db.Query("SELECT `uid`, `name`, `group`, `active`, `is_super_admin`, `avatar` FROM users")
+ if err != nil {
+ InternalError(err,w,r,user)
+ return
+ }
+ defer rows.Close()
+
+ for rows.Next() {
+ puser := User{0,"",0,false,false,false,false,false,false,"",false,"","","","",""}
+ err := rows.Scan(&puser.ID, &puser.Name, &puser.Group, &puser.Active, &puser.Is_Super_Admin, &puser.Avatar)
+ if err != nil {
+ InternalError(err,w,r,user)
+ return
+ }
+
+ puser.Is_Admin = puser.Is_Super_Admin || groups[puser.Group].Is_Admin
+ puser.Is_Super_Mod = puser.Is_Admin || groups[puser.Group].Is_Mod
+ puser.Is_Mod = puser.Is_Super_Mod
+ puser.Is_Banned = groups[puser.Group].Is_Banned
+ if puser.Is_Banned && puser.Is_Super_Mod {
+ puser.Is_Banned = false
+ }
+
+ if puser.Avatar != "" {
+ if puser.Avatar[0] == '.' {
+ puser.Avatar = "/uploads/avatar_" + strconv.Itoa(puser.ID) + puser.Avatar
+ }
+ } else {
+ puser.Avatar = strings.Replace(noavatar,"{id}",strconv.Itoa(puser.ID),1)
+ }
+
+ if groups[puser.Group].Tag != "" {
+ puser.Tag = groups[puser.Group].Tag
+ } else {
+ puser.Tag = ""
+ }
+
+ userList[currentID] = puser
+ currentID++
+ }
+ err = rows.Err()
+ if err != nil {
+ InternalError(err,w,r,user)
+ return
+ }
+
+ pi := Page{"User Manager","panel-users",user,userList,0}
+ err = templates.ExecuteTemplate(w,"panel-users.html", pi)
+ if err != nil {
+ InternalError(err, w, r, user)
+ }
}
\ No newline at end of file
diff --git a/mysql.go b/mysql.go
index a99f9cb1..5b2b2741 100644
--- a/mysql.go
+++ b/mysql.go
@@ -37,6 +37,7 @@ var delete_forum_stmt *sql.Stmt
var update_forum_stmt *sql.Stmt
var update_setting_stmt *sql.Stmt
var add_plugin_stmt *sql.Stmt
+var update_plugin_stmt *sql.Stmt
func init_database(err error) {
if(dbpassword != ""){
@@ -230,6 +231,12 @@ func init_database(err error) {
log.Fatal(err)
}
+ log.Print("Preparing update_plugin statement.")
+ update_plugin_stmt, err = db.Prepare("UPDATE plugins SET active = ? WHERE uname = ?")
+ 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 {
diff --git a/pages.go b/pages.go
index 5c787dc4..571fa61a 100644
--- a/pages.go
+++ b/pages.go
@@ -63,10 +63,15 @@ func shortcode_to_unicode(msg string) string {
msg = strings.Replace(msg,":sleeping:","😴",-1)
msg = strings.Replace(msg,":relieved:","😌",-1)
msg = strings.Replace(msg,":nerd:","🤓",-1)
- return strings.Replace(msg,":stuck_out_tongue:","😛",-1)
+ msg = strings.Replace(msg,":stuck_out_tongue:","😛",-1)
+ return msg
}
func preparse_message(msg string) string {
+ if hooks["preparse_preassign"] != nil {
+ out := run_hook("preparse_preassign", msg)
+ msg = out.(string)
+ }
return shortcode_to_unicode(msg)
}
@@ -74,5 +79,10 @@ func parse_message(msg string) string {
msg = strings.Replace(msg,":)","😀",-1)
msg = strings.Replace(msg,":D","😃",-1)
msg = strings.Replace(msg,":P","😛",-1)
- return strings.Replace(msg,"\n","
",-1)
+ msg = strings.Replace(msg,"\n","
",-1)
+ if hooks["parse_assign"] != nil {
+ out := run_hook("parse_assign", msg)
+ msg = out.(string)
+ }
+ return msg
}
\ No newline at end of file
diff --git a/plugin_helloworld.go b/plugin_helloworld.go
index e18d1ee2..b0e89b15 100644
--- a/plugin_helloworld.go
+++ b/plugin_helloworld.go
@@ -2,7 +2,7 @@ package main
import "html/template"
func init() {
- plugins["helloworld"] = Plugin{"helloworld","Hello World","Azareal","http://github.com/Azareal","",false,"",init_helloworld}
+ plugins["helloworld"] = Plugin{"helloworld","Hello World","Azareal","http://github.com/Azareal","",false,"",init_helloworld,nil,deactivate_helloworld}
}
// init_helloworld is separate from init() as we don't want the plugin to run if the plugin is disabled
@@ -10,10 +10,14 @@ func init_helloworld() {
add_hook("rrow_assign", helloworld_reply)
}
+func deactivate_helloworld() {
+ remove_hook("rrow_assign")
+}
+
func helloworld_reply(data interface{}) interface{} {
reply := data.(Reply)
reply.Content = "Hello World!"
reply.ContentHtml = template.HTML("Hello World!")
- reply.Tag = "Automated"
+ reply.Tag = "Auto"
return reply
}
\ No newline at end of file
diff --git a/plugin_markdown.go b/plugin_markdown.go
new file mode 100644
index 00000000..02ed8cc8
--- /dev/null
+++ b/plugin_markdown.go
@@ -0,0 +1,30 @@
+package main
+
+import "regexp"
+
+var bold_italic *regexp.Regexp
+var bold *regexp.Regexp
+var italic *regexp.Regexp
+
+func init() {
+ plugins["markdown"] = Plugin{"markdown","Markdown","Azareal","http://github.com/Azareal","",false,"",init_markdown,nil,deactivate_markdown}
+}
+
+func init_markdown() {
+ add_hook("parse_assign", markdown_parse)
+ bold_italic = regexp.MustCompile(`\*\*\*(.*)\*\*\*`)
+ bold = regexp.MustCompile(`\*\*(.*)\*\*`)
+ italic = regexp.MustCompile(`\*(.*)\*`)
+}
+
+func deactivate_markdown() {
+ remove_hook("parse_assign")
+}
+
+func markdown_parse(data interface{}) interface{} {
+ msg := data.(string)
+ msg = bold_italic.ReplaceAllString(msg,"$1")
+ msg = bold.ReplaceAllString(msg,"$1")
+ msg = italic.ReplaceAllString(msg,"$1")
+ return msg
+}
\ No newline at end of file
diff --git a/plugin_skeleton.go b/plugin_skeleton.go
index b2f78ced..692aa2aa 100644
--- a/plugin_skeleton.go
+++ b/plugin_skeleton.go
@@ -3,15 +3,28 @@ 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.
+
+ The Activate field is for the handler which is called by the software when the admin hits the Activate button in the control panel. This is separate from the Init handler which is called upon the start of the server and upon activation. Use nil if you don't have a handler for this.
+
+ The Deactivate field is for the handler which is called by the software when the admin hits the Deactivate button in the control panel. You should clean-up any resources you have allocated, remove any hooks, close any statements, etc. within this handler.
*/
- plugins["skeleton"] = Plugin{"skeleton","Skeleton","Azareal","","",false,"",init_test}
+ plugins["skeleton"] = Plugin{"skeleton","Skeleton","Azareal","","",false,"",init_skeleton, activate_skeleton, deactivate_skeleton}
}
-func init_test() {}
+func init_skeleton() {}
+func activate_skeleton() {}
+func deactivate_skeleton() {}
\ No newline at end of file
diff --git a/routes.go b/routes.go
index 2c839734..2d635aee 100644
--- a/routes.go
+++ b/routes.go
@@ -55,10 +55,13 @@ func route_custom_page(w http.ResponseWriter, r *http.Request){
user := SessionCheck(w,r)
name := r.URL.Path[len("/pages/"):]
+ if custom_pages.Lookup(name) == nil {
+ NotFound(w,r,user)
+ }
pi := Page{"Page","page",user,tList,0}
err := custom_pages.ExecuteTemplate(w,name,pi)
if err != nil {
- NotFound(w,r,user)
+ InternalError(err, w, r, user)
}
}
@@ -416,7 +419,7 @@ func route_profile(w http.ResponseWriter, r *http.Request){
replyList = make(map[int]interface{})
currentID = 0
- puser := User{0,"",0,false,false,false,false,false,"",false,"","","",""}
+ puser := User{0,"",0,false,false,false,false,false,false,"",false,"","","","",""}
puser.ID, err = strconv.Atoi(r.URL.Path[len("/user/"):])
if err != nil {
LocalError("The provided TopicID is not a valid number.",w,r,user)
@@ -446,6 +449,12 @@ func route_profile(w http.ResponseWriter, r *http.Request){
}
}
+ if groups[puser.Group].Tag != "" {
+ puser.Tag = groups[puser.Group].Tag
+ } else {
+ puser.Tag = ""
+ }
+
if puser.Avatar != "" {
if puser.Avatar[0] == '.' {
puser.Avatar = "/uploads/avatar_" + strconv.Itoa(puser.ID) + puser.Avatar
@@ -721,11 +730,11 @@ func route_report_submit(w http.ResponseWriter, r *http.Request) {
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)
@@ -758,6 +767,25 @@ func route_report_submit(w http.ResponseWriter, r *http.Request) {
return
}
content = content + "
Original Post: " + title + ""
+ } else if item_type == "user-reply" {
+ err = db.QueryRow("select uid, content from users_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 name from users where uid = ?", tid).Scan(&title)
+ if err == sql.ErrNoRows {
+ LocalError("We were unable to find the profile which the reported post is supposed to be on", 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 {
@@ -769,6 +797,11 @@ func route_report_submit(w http.ResponseWriter, r *http.Request) {
}
content = content + "
Original Post: " + title + ""
} else {
+ if vhooks["report_preassign"] != nil {
+ run_hook_v("report_preassign", &item_id, &item_type)
+ return
+ }
+
// Don't try to guess the type
LocalError("Unknown type", w, r, user)
return
@@ -780,7 +813,6 @@ func route_report_submit(w http.ResponseWriter, r *http.Request) {
InternalError(err,w,r,user)
return
}
-
for rows.Next() {
err = rows.Scan(&count)
if err != nil {
diff --git a/templates/panel-forums.html b/templates/panel-forums.html
index a744e7a2..eb83d538 100644
--- a/templates/panel-forums.html
+++ b/templates/panel-forums.html
@@ -4,6 +4,8 @@