Added the User Manager.

Added the activation and deactivation handlers to plugins.
Experimenting to see the best way of implementing variadic hooks.
Added the ability to deactivate plugins.
Added the preparse_preassign and parse_assign parser hooks.
Added a simple Markdown plugin.
Added the group tag to the profiles.
Improved the open / close status on /topics/
Added the ability to report profile comments.
Added the report_preassign vhook. This is probably going to be changed.
This commit is contained in:
Azareal 2016-12-13 02:14:14 +00:00
parent ec2c02d7c9
commit 086239f88c
24 changed files with 279 additions and 18 deletions

View File

@ -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);

View File

@ -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...)
}

Binary file not shown.

BIN
images/edit_setting.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
images/new-topic-list.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

BIN
images/plugins.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
images/settings.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
images/users.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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","<br>",-1)
msg = strings.Replace(msg,"\n","<br>",-1)
if hooks["parse_assign"] != nil {
out := run_hook("parse_assign", msg)
msg = out.(string)
}
return msg
}

View File

@ -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
}

30
plugin_markdown.go Normal file
View File

@ -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,"<i><b>$1</b></i>")
msg = bold.ReplaceAllString(msg,"<b>$1</b>")
msg = italic.ReplaceAllString(msg,"<i>$1</i>")
return msg
}

View File

@ -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() {}

View File

@ -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 + "<br><br>Original Post: <a href='/topic/" + strconv.Itoa(tid) + "'>" + title + "</a>"
} 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 + "<br><br>Original Post: <a href='/user/" + strconv.Itoa(tid) + "'>" + title + "</a>"
} 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 + "<br><br>Original Post: <a href='/topic/" + strconv.Itoa(item_id) + "'>" + title + "</a>"
} 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 {

View File

@ -4,6 +4,8 @@
<div class="rowitem passive"><a href="/panel/forums/">Forums</a></div>
<div class="rowitem passive"><a href="/panel/settings/">Settings</a></div>
<div class="rowitem passive"><a href="/panel/plugins/">Plugins</a></div>
<div class="rowitem passive"><a href="/panel/users/">Users</a></div>
<div class="rowitem passive"><a href="/panel/groups/">Groups</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>

View File

@ -4,6 +4,8 @@
<div class="rowitem passive"><a href="/panel/forums/">Forums</a></div>
<div class="rowitem passive"><a href="/panel/settings/">Settings</a></div>
<div class="rowitem passive"><a href="/panel/plugins/">Plugins</a></div>
<div class="rowitem passive"><a href="/panel/users/">Users</a></div>
<div class="rowitem passive"><a href="/panel/groups/">Groups</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>

View File

@ -4,6 +4,8 @@
<div class="rowitem passive"><a href="/panel/forums/">Forums</a></div>
<div class="rowitem passive"><a href="/panel/settings/">Settings</a></div>
<div class="rowitem passive"><a href="/panel/plugins/">Plugins</a></div>
<div class="rowitem passive"><a href="/panel/users/">Users</a></div>
<div class="rowitem passive"><a href="/panel/groups/">Groups</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>

View File

@ -4,6 +4,8 @@
<div class="rowitem passive"><a href="/panel/forums/">Forums</a></div>
<div class="rowitem passive"><a href="/panel/settings/">Settings</a></div>
<div class="rowitem passive"><a href="/panel/plugins/">Plugins</a></div>
<div class="rowitem passive"><a href="/panel/users/">Users</a></div>
<div class="rowitem passive"><a href="/panel/groups/">Groups</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>

View File

@ -0,0 +1,29 @@
{{template "header.html" . }}
<div class="colblock_left">
<div class="rowitem"><a>Control Panel</a></div>
<div class="rowitem passive"><a href="/panel/forums/">Forums</a></div>
<div class="rowitem passive"><a href="/panel/settings/">Settings</a></div>
<div class="rowitem passive"><a href="/panel/plugins/">Plugins</a></div>
<div class="rowitem passive"><a href="/panel/users/">Users</a></div>
<div class="rowitem passive"><a href="/panel/groups/">Groups</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a>Coming Soon</a></div>
<div class="rowitem passive"><a href="/forum/-1">Reports</a></div>
</div>
<div class="colblock_right">
<div class="rowitem"><a>Users</a></div>
</div>
<div class="colblock_right">
{{range .ItemList}}
<div class="rowitem editable_parent" style="font-weight: normal;text-transform: none;">
<a href="/user/{{.ID}}" class="editable_block" style="font-size: 20px;position:relative;top: -2px;">{{.Name}}</a>
{{if .Tag}}<span class="username" style="margin-left 4px;{{if (.Is_Super_Mod) and (.Active)}}float: right;{{end}}">{{.Tag}}</span>{{end}}
<span style="float: right;">
{{if .Is_Banned}}<a href="/users/unban/{{.ID}}?session={{$.CurrentUser.Session}}" class="username">Unban</a>{{else if not .Is_Super_Mod}}<a href="/users/ban/{{.ID}}?session={{$.CurrentUser.Session}}" class="username">Ban</a>{{end}}
{{if not .Active}}<a href="/users/activate/{{.ID}}?session={{$.CurrentUser.Session}}" class="username">Activate</a>{{end}}
</span>
</div>
{{end}}
</div>
{{template "footer.html" . }}

View File

@ -1,13 +1,15 @@
{{template "header.html" . }}
<div class="colblock_left" style="max-width: 220px;">
<div class="rowitem" style="padding: 0;"><img src="{{.Something.Avatar}}" style="max-width: 100%;margin: 0;"/></div>
<div class="rowitem">{{.Something.Name}}</div>
<div class="rowitem" style="text-transform: capitalize;">
<span style="font-size: 18px;">{{.Something.Name}}</span>{{if .Something.Tag}}<span class="username" style="float: right;">{{.Something.Tag}}</span>{{end}}
</div>
<div class="rowitem passive">
<a class="username">Add Friend</a>
{{if (.CurrentUser.Is_Super_Mod) and not (.Something.Is_Super_Mod) }}
{{if .Something.Is_Banned }}<a href="/users/unban/{{.Something.ID}}" class="username">Unban</a>{{else}}<a href="/users/ban/{{.Something.ID}}" class="username">Ban</a>{{end}}
{{end}}
<a class="username">Report</a>
<a href="/report/submit/{{.Something.ID}}?session={{.CurrentUser.Session}}&type=user" class="username report_item">Report</a>
</div>
</div>
<div class="colblock_right">
@ -21,7 +23,7 @@
<a href="/user/{{$element.CreatedBy}}" class="username">{{$element.CreatedByName}}</a>
{{if $.CurrentUser.Is_Mod}}<a href="/profile/reply/edit/submit/{{$element.ID}}"><button class="username edit_item">Edit</button></a>
<a href="/profile/reply/delete/submit/{{$element.ID}}"><button class="username delete_item">Delete</button></a>{{end}}
<a href="/profile/reply/report/submit/{{$element.ID}}"><button class="username report_item">Report</button></a>
<a href="/report/submit/{{$element.ID}}?session={{$.CurrentUser.Session}}&type=user-reply"><button class="username report_item">Report</button></a>
{{ if $element.Tag }}<a class="username" style="float: right;">{{$element.Tag}}</a>{{end}}
</div>{{end}}
</div>

View File

@ -4,7 +4,9 @@
</div>
<div class="rowblock">
{{range .ItemList}}<div class="rowitem passive" style="{{ if .Avatar }}background-image: url({{ .Avatar }});background-position: left;background-repeat: no-repeat;background-size: 64px;padding-left: 72px;{{end}}{{ if .Sticky }}background-color: #FFFFCC;{{end}}">
<a href="/topic/{{.ID}}">{{.Title}}</a> {{if .Is_Closed}}<span class="topic_status topic_status_closed">closed</span>{{else}}<span class="topic_status topic_status_open">open</span>{{end}}
<a href="/topic/{{.ID}}">{{.Title}}</a> {{if .Is_Closed}}<span class="username topic_status_e topic_status_closed" style="float: right;">closed</span>
{{else}}<span class="username topic_status_e topic_status_open" style="float: right;">open</span>{{end}}
<span class="username" style="border-right: 0;float: right;">Status</span>
</div>{{end}}
</div>
{{if .Something}}

View File

@ -12,6 +12,7 @@ type User struct
ID int
Name string
Group int
Active bool
Is_Mod bool
Is_Super_Mod bool
Is_Admin bool
@ -23,6 +24,7 @@ type User struct
Message string
URLPrefix string
URLName string
Tag string
}
func SetPassword(uid int, password string) (error) {
@ -45,7 +47,7 @@ func SetPassword(uid int, password string) (error) {
}
func SessionCheck(w http.ResponseWriter, r *http.Request) (User) {
user := User{0,"",0,false,false,false,false,false,"",false,"","","",""}
user := User{0,"",0,false,false,false,false,false,false,"",false,"","","","",""}
var err error
var cookie *http.Cookie