Added the plugin system.

Added the report system.
Rebranded the project.
Improved the installation instructions.
Changed the open / close status CSS.

Added the Hello World plugin.
Added the Skeleton plugin.
This commit is contained in:
Azareal 2016-12-11 16:06:17 +00:00
parent b2e3591997
commit ec2c02d7c9
21 changed files with 396 additions and 15 deletions

View File

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

View File

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

View File

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

25
extend.go Normal file
View File

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

1
extend/filler.txt Normal file
View File

@ -0,0 +1 @@
This file is here so that Git will include this folder in the repository.

View File

@ -1,2 +1,2 @@
go build
./Grosolo
./Gosora

Binary file not shown.

BIN
images/report.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

17
main.go
View File

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

View File

@ -695,3 +695,65 @@ func route_panel_setting_edit(w http.ResponseWriter, r *http.Request) {
}
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)
}

View File

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

19
plugin_helloworld.go Normal file
View File

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

17
plugin_skeleton.go Normal file
View File

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

View File

@ -1,3 +1,4 @@
/* Copyright Azareal 2016 - 2017 */
package main
import "html/template"

135
routes.go
View File

@ -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 + "<br><br>Original Post: <a href='/topic/" + 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 {
NotFound(w,r,user)
return
} else if err != nil {
InternalError(err,w,r,user)
return
}
content = content + "<br><br>Original Post: <a href='/topic/" + strconv.Itoa(item_id) + "'>" + title + "</a>"
} 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 {

View File

@ -1,3 +1,3 @@
go build
grosolo.exe
gosora.exe
pause

View File

@ -3,6 +3,8 @@
<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>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>

View File

@ -0,0 +1,28 @@
{{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>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>Plugins</a></div>
</div>
<div class="colblock_right">
{{range .ItemList}}
<div class="rowitem editable_parent" style="font-weight: normal;text-transform: none;">
<a {{if .URL}}href="{{.URL}}" {{end}}class="editable_block" style="font-size: 20px;position:relative;top: -2px;">{{.Name}}</a><br />
<small style="margin-left: 2px;">Author: {{.Author}}</small>
<span style="float: right;">
{{if .Settings}}<a href="/panel/settings/" class="username">Settings</a>{{end}}
{{if .Active}}<a href="/panel/plugins/deactivate/{{.UName}}?session={{$.CurrentUser.Session}}" class="username">Deactivate</a>
{{else}}<a href="/panel/plugins/activate/{{.UName}}?session={{$.CurrentUser.Session}}" class="username">Activate</a>{{end}}
</span>
</div>
{{end}}
</div>
{{template "footer.html" . }}

View File

@ -3,6 +3,8 @@
<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>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>

View File

@ -3,6 +3,8 @@
<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>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>

View File

@ -3,20 +3,21 @@
<form action='/topic/edit/submit/{{.Something.ID}}' method="post">
<div class="rowitem"{{ if .Something.Sticky }} style="background-color: #FFFFEA;"{{end}}>
<a class='topic_name hide_on_edit'>{{.Something.Title}}</a>
<span class='topic_status topic_status_e topic_status_{{.Something.Status}} hide_on_edit'>{{.Something.Status}}</span>
<span class='username topic_status_e topic_status_{{.Something.Status}} hide_on_edit' style="font-weight:normal;float: right;">{{.Something.Status}}</span>
<span class="username" style="border-right: 0;font-weight: normal;float: right;">Status</span>
{{if .CurrentUser.Is_Mod}}
<a href='/topic/edit/{{.Something.ID}}' class="username hide_on_edit open_edit" style="font-weight: normal;">Edit</a>
<a href='/topic/edit/{{.Something.ID}}' class="username hide_on_edit open_edit" style="font-weight: normal;margin-left: 6px;">Edit</a>
<a href='/topic/delete/submit/{{.Something.ID}}' class="username" style="font-weight: normal;">Delete</a>
{{ if .Something.Sticky }}<a href='/topic/unstick/submit/{{.Something.ID}}' class="username" style="font-weight: normal;">Unpin</a>{{else}}<a href='/topic/stick/submit/{{.Something.ID}}' class="username" style="font-weight: normal;">Pin</a>{{end}}
<input class='show_on_edit topic_name_input' name="topic_name" value='{{.Something.Title}}' type="text" />
<select name="topic_status" class='show_on_edit topic_status_input'>
<select name="topic_status" class='show_on_edit topic_status_input' style='float: right;'>
<option>open</option>
<option>closed</option>
</select>
<button name="topic-button" class="formbutton show_on_edit submit_edit">Update</button>
{{end}}
<a href='/topic/report/submit/{{.Something.ID}}' class="username" style="font-weight: normal;">Report</a>
<a href="/report/submit/{{.Something.ID}}?session={{.CurrentUser.Session}}&type=topic" class="username report_item" style="font-weight: normal;">Report</a>
</div>
</form>
</div>
@ -38,7 +39,7 @@
<a href="/user/{{$element.CreatedBy}}" class="username">{{$element.CreatedByName}}</a>
{{if $.CurrentUser.Is_Mod}}<a href="/reply/edit/submit/{{$element.ID}}"><button class="username edit_item">Edit</button></a>
<a href="/reply/delete/submit/{{$element.ID}}"><button class="username delete_item">Delete</button></a>{{end}}
<a href="/reply/report/submit/{{$element.ID}}"><button class="username report_item">Report</button></a>
<a href="/report/submit/{{$element.ID}}?session={{$.CurrentUser.Session}}&type=reply"><button class="username report_item">Report</button></a>
{{if $element.Tag}}<a class="username" style="float: right;">{{$element.Tag}}</a>{{else if $element.URLName}}<a href="{{$element.URL}}" class="username" style="color: #505050;float: right;" rel="nofollow">{{$element.URLName}}</a>
<a class="username" style="color: #505050;float: right;border-right: 0;">{{$element.URLPrefix}}</a>{{end}}
</div>{{end}}