gosora/plugin_socialgroups.go
Azareal 5a43432b80 Replaced most of the uses of fmt with log.
Replaced the io.Writers with http.ResponseWriters.
Refactored the compiled template calls.
Redirect port 443 to port 80.
Catch more errors from templates.
Fixed a few mutexes which are never unlocked.
Eliminated an unnecessary parameter in InternalError()
Temporarily commented out users_penalties so that the installer will succeed.
A couple more template types can be remapped now.
Tweaked the theme.
2017-08-13 12:22:34 +01:00

669 lines
21 KiB
Go

package main
import (
//"fmt"
"bytes"
"strings"
"strconv"
"errors"
"context"
"net/http"
"html"
"html/template"
"database/sql"
"./query_gen/lib"
)
var socialgroups_list_stmt *sql.Stmt
var socialgroups_member_list_stmt *sql.Stmt
var socialgroups_member_list_join_stmt *sql.Stmt
var socialgroups_get_member_stmt *sql.Stmt
var socialgroups_get_group_stmt *sql.Stmt
var socialgroups_create_group_stmt *sql.Stmt
var socialgroups_attach_forum_stmt *sql.Stmt
var socialgroups_unattach_forum_stmt *sql.Stmt
var socialgroups_add_member_stmt *sql.Stmt
// TO-DO: Add a better way of splitting up giant plugins like this
type SocialGroup struct
{
ID int
Link string
Name string
Desc string
Active bool
Privacy int /* 0: Public, 1: Protected, 2: Private */
// Who should be able to accept applications and create invites? Mods+ or just admins? Mods is a good start, we can ponder over whether we should make this more flexible in the future.
Joinable int /* 0: Private, 1: Anyone can join, 2: Applications, 3: Invite-only */
MemberCount int
Owner int
Backdrop string
CreatedAt string
LastUpdateTime string
MainForumID int
MainForum *Forum
Forums []*Forum
ExtData ExtData
}
type SocialGroupPage struct
{
Title string
CurrentUser User
Header HeaderVars
ItemList []*TopicsRow
Forum Forum
SocialGroup SocialGroup
Page int
LastPage int
ExtData ExtData
}
type SocialGroupListPage struct
{
Title string
CurrentUser User
Header HeaderVars
GroupList []SocialGroup
ExtData ExtData
}
type SocialGroupMemberListPage struct
{
Title string
CurrentUser User
Header HeaderVars
ItemList []SocialGroupMember
SocialGroup SocialGroup
Page int
LastPage int
ExtData ExtData
}
type SocialGroupMember struct
{
Link string
Rank int /* 0: Member. 1: Mod. 2: Admin. */
RankString string /* Member, Mod, Admin, Owner */
PostCount int
JoinedAt string
Offline bool // TO-DO: Need to track the online states of members when WebSockets are enabled
User User
}
func init() {
plugins["socialgroups"] = NewPlugin("socialgroups","Social Groups","Azareal","http://github.com/Azareal","","","",init_socialgroups,nil,deactivate_socialgroups,install_socialgroups,nil)
}
func init_socialgroups() (err error) {
plugins["socialgroups"].AddHook("intercept_build_widgets", socialgroups_widgets)
plugins["socialgroups"].AddHook("trow_assign", socialgroups_trow_assign)
plugins["socialgroups"].AddHook("topic_create_pre_loop", socialgroups_topic_create_pre_loop)
plugins["socialgroups"].AddHook("pre_render_view_forum", socialgroups_pre_render_view_forum)
plugins["socialgroups"].AddHook("simple_forum_check_pre_perms", socialgroups_forum_check)
plugins["socialgroups"].AddHook("forum_check_pre_perms", socialgroups_forum_check)
// TO-DO: Auto-grant this perm to admins upon installation?
register_plugin_perm("CreateSocialGroup")
router.HandleFunc("/groups/", socialgroups_group_list)
router.HandleFunc("/group/", socialgroups_view_group)
router.HandleFunc("/group/create/", socialgroups_create_group)
router.HandleFunc("/group/create/submit/", socialgroups_create_group_submit)
router.HandleFunc("/group/members/", socialgroups_member_list)
socialgroups_list_stmt, err = qgen.Builder.SimpleSelect("socialgroups","sgid, name, desc, active, privacy, joinable, owner, memberCount, createdAt, lastUpdateTime","","","")
if err != nil {
return err
}
socialgroups_get_group_stmt, err = qgen.Builder.SimpleSelect("socialgroups","name, desc, active, privacy, joinable, owner, memberCount, mainForum, backdrop, createdAt, lastUpdateTime","sgid = ?","","")
if err != nil {
return err
}
socialgroups_member_list_stmt, err = qgen.Builder.SimpleSelect("socialgroups_members","sgid, uid, rank, posts, joinedAt","","","")
if err != nil {
return err
}
socialgroups_member_list_join_stmt, err = qgen.Builder.SimpleLeftJoin("socialgroups_members","users","users.uid, socialgroups_members.rank, socialgroups_members.posts, socialgroups_members.joinedAt, users.name, users.avatar","socialgroups_members.uid = users.uid","socialgroups_members.sgid = ?","socialgroups_members.rank DESC, socialgroups_members.joinedat ASC","")
if err != nil {
return err
}
socialgroups_get_member_stmt, err = qgen.Builder.SimpleSelect("socialgroups_members","rank, posts, joinedAt","sgid = ? AND uid = ?","","")
if err != nil {
return err
}
socialgroups_create_group_stmt, err = qgen.Builder.SimpleInsert("socialgroups","name, desc, active, privacy, joinable, owner, memberCount, mainForum, backdrop, createdAt, lastUpdateTime","?,?,?,?,1,?,1,?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP()")
if err != nil {
return err
}
socialgroups_attach_forum_stmt, err = qgen.Builder.SimpleUpdate("forums","parentID = ?, parentType = 'socialgroup'","fid = ?")
if err != nil {
return err
}
socialgroups_unattach_forum_stmt, err = qgen.Builder.SimpleUpdate("forums","parentID = 0, parentType = ''","fid = ?")
if err != nil {
return err
}
socialgroups_add_member_stmt, err = qgen.Builder.SimpleInsert("socialgroups_members","sgid, uid, rank, posts, joinedAt","?,?,?,0,UTC_TIMESTAMP()")
if err != nil {
return err
}
return nil
}
func deactivate_socialgroups() {
plugins["socialgroups"].RemoveHook("intercept_build_widgets", socialgroups_widgets)
plugins["socialgroups"].RemoveHook("trow_assign", socialgroups_trow_assign)
plugins["socialgroups"].RemoveHook("topic_create_pre_loop", socialgroups_topic_create_pre_loop)
plugins["socialgroups"].RemoveHook("pre_render_view_forum", socialgroups_pre_render_view_forum)
plugins["socialgroups"].RemoveHook("simple_forum_check_pre_perms", socialgroups_forum_check)
plugins["socialgroups"].RemoveHook("forum_check_pre_perms", socialgroups_forum_check)
deregister_plugin_perm("CreateSocialGroup")
_ = router.RemoveFunc("/groups/")
_ = router.RemoveFunc("/group/")
_ = router.RemoveFunc("/group/create/")
_ = router.RemoveFunc("/group/create/submit/")
_ = socialgroups_list_stmt.Close()
_ = socialgroups_member_list_stmt.Close()
_ = socialgroups_member_list_join_stmt.Close()
_ = socialgroups_get_member_stmt.Close()
_ = socialgroups_get_group_stmt.Close()
_ = socialgroups_create_group_stmt.Close()
_ = socialgroups_attach_forum_stmt.Close()
_ = socialgroups_unattach_forum_stmt.Close()
_ = socialgroups_add_member_stmt.Close()
}
// TO-DO: Stop accessing the query builder directly and add a feature in Gosora which is more easily reversed, if an error comes up during the installation process
func install_socialgroups() error {
sg_table_stmt, err := qgen.Builder.CreateTable("socialgroups","utf8mb4","utf8mb4_general_ci",
[]qgen.DB_Table_Column{
qgen.DB_Table_Column{"sgid","int",0,false,true,""},
qgen.DB_Table_Column{"name","varchar",100,false,false,""},
qgen.DB_Table_Column{"desc","varchar",200,false,false,""},
qgen.DB_Table_Column{"active","boolean",1,false,false,""},
qgen.DB_Table_Column{"privacy","smallint",0,false,false,""},
qgen.DB_Table_Column{"joinable","smallint",0,false,false,"0"},
qgen.DB_Table_Column{"owner","int",0,false,false,""},
qgen.DB_Table_Column{"memberCount","int",0,false,false,""},
qgen.DB_Table_Column{"mainForum","int",0,false,false,"0"}, // The board the user lands on when they click on a group, we'll make it possible for group admins to change what users land on
//qgen.DB_Table_Column{"boards","varchar",255,false,false,""}, // Cap the max number of boards at 8 to avoid overflowing the confines of a 64-bit integer?
qgen.DB_Table_Column{"backdrop","varchar",200,false,false,""}, // File extension for the uploaded file, or an external link
qgen.DB_Table_Column{"createdAt","createdAt",0,false,false,""},
qgen.DB_Table_Column{"lastUpdateTime","datetime",0,false,false,""},
},
[]qgen.DB_Table_Key{
qgen.DB_Table_Key{"sgid","primary"},
},
)
if err != nil {
return err
}
_, err = sg_table_stmt.Exec()
if err != nil {
return err
}
sg_members_table_stmt, err := qgen.Builder.CreateTable("socialgroups_members","","",
[]qgen.DB_Table_Column{
qgen.DB_Table_Column{"sgid","int",0,false,false,""},
qgen.DB_Table_Column{"uid","int",0,false,false,""},
qgen.DB_Table_Column{"rank","int",0,false,false,"0"}, /* 0: Member. 1: Mod. 2: Admin. */
qgen.DB_Table_Column{"posts","int",0,false,false,"0"}, /* Per-Group post count. Should we do some sort of score system? */
qgen.DB_Table_Column{"joinedAt","datetime",0,false,false,""},
},
[]qgen.DB_Table_Key{},
)
if err != nil {
return err
}
_, err = sg_members_table_stmt.Exec()
return err
}
// TO-DO; Implement an uninstallation system into Gosora. And a better installation system.
func uninstall_socialgroups() error {
return nil
}
// TO-DO: Do this properly via the widget system
func socialgroups_common_area_widgets(headerVars *HeaderVars) {
// TO-DO: Hot Groups? Featured Groups? Official Groups?
var b bytes.Buffer
var menu WidgetMenu = WidgetMenu{"Social Groups",[]WidgetMenuItem{
WidgetMenuItem{"Create Group","/group/create/",false},
}}
err := templates.ExecuteTemplate(&b,"widget_menu.html",menu)
if err != nil {
LogError(err)
return
}
if themes[defaultTheme].Sidebars == "left" {
headerVars.Widgets.LeftSidebar = template.HTML(string(b.Bytes()))
} else if themes[defaultTheme].Sidebars == "right" || themes[defaultTheme].Sidebars == "both" {
headerVars.Widgets.RightSidebar = template.HTML(string(b.Bytes()))
}
}
// TO-DO: Do this properly via the widget system
// TO-DO: Make a better more customisable group widget system
func socialgroups_group_widgets(headerVars *HeaderVars, sgItem SocialGroup) (success bool) {
return false // Disabled until the next commit
var b bytes.Buffer
var menu WidgetMenu = WidgetMenu{"Group Options",[]WidgetMenuItem{
WidgetMenuItem{"Join","/group/join/" + strconv.Itoa(sgItem.ID),false},
WidgetMenuItem{"Members","/group/members/" + strconv.Itoa(sgItem.ID),false},
}}
err := templates.ExecuteTemplate(&b,"widget_menu.html",menu)
if err != nil {
LogError(err)
return false
}
if themes[defaultTheme].Sidebars == "left" {
headerVars.Widgets.LeftSidebar = template.HTML(string(b.Bytes()))
} else if themes[defaultTheme].Sidebars == "right" || themes[defaultTheme].Sidebars == "both" {
headerVars.Widgets.RightSidebar = template.HTML(string(b.Bytes()))
} else {
return false
}
return true
}
/*
Custom Pages
*/
func socialgroups_group_list(w http.ResponseWriter, r *http.Request, user User) {
headerVars, ok := SessionCheck(w,r,&user)
if !ok {
return
}
socialgroups_common_area_widgets(&headerVars)
rows, err := socialgroups_list_stmt.Query()
if err != nil && err != ErrNoRows {
InternalError(err,w)
return
}
var sgList []SocialGroup
for rows.Next() {
sgItem := SocialGroup{ID:0}
err := rows.Scan(&sgItem.ID, &sgItem.Name, &sgItem.Desc, &sgItem.Active, &sgItem.Privacy, &sgItem.Joinable, &sgItem.Owner, &sgItem.MemberCount, &sgItem.CreatedAt, &sgItem.LastUpdateTime)
if err != nil {
InternalError(err,w)
return
}
sgItem.Link = socialgroups_build_group_url(name_to_slug(sgItem.Name),sgItem.ID)
sgList = append(sgList,sgItem)
}
err = rows.Err()
if err != nil {
InternalError(err,w)
return
}
rows.Close()
pi := SocialGroupListPage{"Group List",user,headerVars,sgList,extData}
err = templates.ExecuteTemplate(w,"socialgroups_group_list.html", pi)
if err != nil {
InternalError(err,w)
}
}
func socialgroups_get_group(sgid int) (sgItem SocialGroup, err error) {
sgItem = SocialGroup{ID:sgid}
err = socialgroups_get_group_stmt.QueryRow(sgid).Scan(&sgItem.Name, &sgItem.Desc, &sgItem.Active, &sgItem.Privacy, &sgItem.Joinable, &sgItem.Owner, &sgItem.MemberCount, &sgItem.MainForumID, &sgItem.Backdrop, &sgItem.CreatedAt, &sgItem.LastUpdateTime)
return sgItem, err
}
func socialgroups_view_group(w http.ResponseWriter, r *http.Request, user User) {
// SEO URLs...
halves := strings.Split(r.URL.Path[len("/group/"):],".")
if len(halves) < 2 {
halves = append(halves,halves[0])
}
sgid, err := strconv.Atoi(halves[1])
if err != nil {
PreError("Not a valid group ID",w,r)
return
}
sgItem, err := socialgroups_get_group(sgid)
if err != nil {
LocalError("Bad group",w,r,user)
return
}
if !sgItem.Active {
NotFound(w,r)
}
// Re-route the request to route_forums
var ctx context.Context = context.WithValue(r.Context(),"socialgroups_current_group",sgItem)
route_forum(w,r.WithContext(ctx),user,strconv.Itoa(sgItem.MainForumID))
}
func socialgroups_create_group(w http.ResponseWriter, r *http.Request, user User) {
headerVars, ok := SessionCheck(w,r,&user)
if !ok {
return
}
// TO-DO: Add an approval queue mode for group creation
if !user.Loggedin || !user.PluginPerms["CreateSocialGroup"] {
NoPermissions(w,r,user)
return
}
socialgroups_common_area_widgets(&headerVars)
pi := Page{"Create Group",user,headerVars,tList,nil}
err := templates.ExecuteTemplate(w,"socialgroups_create_group.html", pi)
if err != nil {
InternalError(err,w)
}
}
func socialgroups_create_group_submit(w http.ResponseWriter, r *http.Request, user User) {
// TO-DO: Add an approval queue mode for group creation
if !user.Loggedin || !user.PluginPerms["CreateSocialGroup"] {
NoPermissions(w,r,user)
return
}
var group_active bool = true
var group_name string = html.EscapeString(r.PostFormValue("group_name"))
var group_desc string = html.EscapeString(r.PostFormValue("group_desc"))
var gprivacy string = r.PostFormValue("group_privacy")
var group_privacy int
switch(gprivacy) {
case "0": group_privacy = 0 // Public
case "1": group_privacy = 1 // Protected
case "2": group_privacy = 2 // private
default: group_privacy = 0
}
// Create the backing forum
fid, err := fstore.CreateForum(group_name,"",true,"")
if err != nil {
InternalError(err,w)
return
}
res, err := socialgroups_create_group_stmt.Exec(group_name, group_desc, group_active, group_privacy, user.ID, fid)
if err != nil {
InternalError(err,w)
return
}
lastId, err := res.LastInsertId()
if err != nil {
InternalError(err,w)
return
}
// Add the main backing forum to the forum list
err = socialgroups_attach_forum(int(lastId),fid)
if err != nil {
InternalError(err,w)
return
}
_, err = socialgroups_add_member_stmt.Exec(lastId,user.ID,2)
if err != nil {
InternalError(err,w)
return
}
http.Redirect(w,r,socialgroups_build_group_url(name_to_slug(group_name),int(lastId)), http.StatusSeeOther)
}
func socialgroups_member_list(w http.ResponseWriter, r *http.Request, user User) {
headerVars, ok := SessionCheck(w,r,&user)
if !ok {
return
}
// SEO URLs...
halves := strings.Split(r.URL.Path[len("/group/members/"):],".")
if len(halves) < 2 {
halves = append(halves,halves[0])
}
sgid, err := strconv.Atoi(halves[1])
if err != nil {
PreError("Not a valid group ID",w,r)
return
}
var sgItem SocialGroup = SocialGroup{ID:sgid}
var mainForum int // Unused
err = socialgroups_get_group_stmt.QueryRow(sgid).Scan(&sgItem.Name, &sgItem.Desc, &sgItem.Active, &sgItem.Privacy, &sgItem.Joinable, &sgItem.Owner, &sgItem.MemberCount, &mainForum, &sgItem.Backdrop, &sgItem.CreatedAt, &sgItem.LastUpdateTime)
if err != nil {
LocalError("Bad group",w,r,user)
return
}
sgItem.Link = socialgroups_build_group_url(name_to_slug(sgItem.Name),sgItem.ID)
socialgroups_group_widgets(&headerVars, sgItem)
rows, err := socialgroups_member_list_join_stmt.Query(sgid)
if err != nil && err != ErrNoRows {
InternalError(err,w)
return
}
var sgMembers []SocialGroupMember
for rows.Next() {
sgMember := SocialGroupMember{PostCount:0}
err := rows.Scan(&sgMember.User.ID,&sgMember.Rank,&sgMember.PostCount,&sgMember.JoinedAt,&sgMember.User.Name, &sgMember.User.Avatar)
if err != nil {
InternalError(err,w)
return
}
sgMember.Link = build_profile_url(name_to_slug(sgMember.User.Name),sgMember.User.ID)
if sgMember.User.Avatar != "" {
if sgMember.User.Avatar[0] == '.' {
sgMember.User.Avatar = "/uploads/avatar_" + strconv.Itoa(sgMember.User.ID) + sgMember.User.Avatar
}
} else {
sgMember.User.Avatar = strings.Replace(config.Noavatar,"{id}",strconv.Itoa(sgMember.User.ID),1)
}
sgMember.JoinedAt, _ = relative_time(sgMember.JoinedAt)
if sgItem.Owner == sgMember.User.ID {
sgMember.RankString = "Owner"
} else {
switch(sgMember.Rank) {
case 0: sgMember.RankString = "Member"
case 1: sgMember.RankString = "Mod"
case 2: sgMember.RankString = "Admin"
}
}
sgMembers = append(sgMembers,sgMember)
}
err = rows.Err()
if err != nil {
InternalError(err,w)
return
}
rows.Close()
pi := SocialGroupMemberListPage{"Group Member List",user,headerVars,sgMembers,sgItem,0,0,extData}
// A plugin with plugins. Pluginception!
if pre_render_hooks["pre_render_socialgroups_member_list"] != nil {
if run_pre_render_hook("pre_render_socialgroups_member_list", w, r, &user, &pi) {
return
}
}
err = templates.ExecuteTemplate(w,"socialgroups_member_list.html", pi)
if err != nil {
InternalError(err,w)
}
}
func socialgroups_attach_forum(sgid int, fid int) error {
_, err := socialgroups_attach_forum_stmt.Exec(sgid,fid)
return err
}
func socialgroups_unattach_forum(fid int) error {
_, err := socialgroups_attach_forum_stmt.Exec(fid)
return err
}
func socialgroups_build_group_url(slug string, id int) string {
if slug == "" {
return "/group/" + slug + "." + strconv.Itoa(id)
}
return "/group/" + strconv.Itoa(id)
}
/*
Hooks
*/
func socialgroups_pre_render_view_forum(w http.ResponseWriter, r *http.Request, user *User, data interface{}) (halt bool) {
pi := data.(*ForumPage)
if pi.Header.ExtData.items != nil {
if sgData, ok := pi.Header.ExtData.items["socialgroups_current_group"]; ok {
sgItem := sgData.(SocialGroup)
sgpi := SocialGroupPage{pi.Title,pi.CurrentUser,pi.Header,pi.ItemList,pi.Forum,sgItem,pi.Page,pi.LastPage,pi.ExtData}
err := templates.ExecuteTemplate(w,"socialgroups_view_group.html", sgpi)
if err != nil {
LogError(err)
return false
}
return true
}
}
return false
}
func socialgroups_trow_assign(args ...interface{}) interface{} {
var forum *Forum = args[1].(*Forum)
if forum.ParentType == "socialgroup" {
var topicItem *TopicsRow = args[0].(*TopicsRow)
topicItem.ForumLink = "/group/" + strings.TrimPrefix(topicItem.ForumLink,get_forum_url_prefix())
}
return nil
}
// TO-DO: It would be nice, if you could select one of the boards in the group from that drop-down rather than just the one you got linked from
func socialgroups_topic_create_pre_loop(args ...interface{}) interface{} {
var fid int = args[2].(int)
if fstore.DirtyGet(fid).ParentType == "socialgroup" {
var strictmode *bool = args[5].(*bool)
*strictmode = true
}
return nil
}
// TO-DO: Add privacy options
// TO-DO: Add support for multiple boards and add per-board simplified permissions
// TO-DO: Take is_js into account for routes which expect JSON responses
func socialgroups_forum_check(args ...interface{}) (skip interface{}) {
var r = args[1].(*http.Request)
var fid *int = args[3].(*int)
var forum *Forum = fstore.DirtyGet(*fid)
if forum.ParentType == "socialgroup" {
var err error
var w = args[0].(http.ResponseWriter)
var success *bool = args[4].(*bool)
sgItem, ok := r.Context().Value("socialgroups_current_group").(SocialGroup)
if !ok {
sgItem, err = socialgroups_get_group(forum.ParentID)
if err != nil {
InternalError(errors.New("Unable to find the parent group for a forum"),w)
*success = false
return false
}
if !sgItem.Active {
NotFound(w,r)
*success = false
return false
}
r = r.WithContext(context.WithValue(r.Context(),"socialgroups_current_group",sgItem))
}
var user *User = args[2].(*User)
var rank int
var posts int
var joinedAt string
// TO-DO: Group privacy settings. For now, groups are all globally visible
// Clear the default group permissions
// TO-DO: Do this more efficiently, doing it quick and dirty for now to get this out quickly
override_forum_perms(&user.Perms, false)
user.Perms.ViewTopic = true
err = socialgroups_get_member_stmt.QueryRow(sgItem.ID,user.ID).Scan(&rank,&posts,&joinedAt)
if err != nil && err != ErrNoRows {
*success = false
InternalError(err,w)
return false
} else if err != nil {
return true
}
// TO-DO: Implement bans properly by adding the Local Ban API in the next commit
if rank < 0 {
return true
}
// Basic permissions for members, more complicated permissions coming in the next commit!
if sgItem.Owner == user.ID {
override_forum_perms(&user.Perms,true)
} else if rank == 0 {
user.Perms.LikeItem = true
user.Perms.CreateTopic = true
user.Perms.CreateReply = true
} else {
override_forum_perms(&user.Perms,true)
}
return true
}
return false
}
// TO-DO: Override redirects? I don't think this is needed quite yet
func socialgroups_widgets(args ...interface{}) interface{} {
var zone string = args[0].(string)
var headerVars *HeaderVars = args[2].(*HeaderVars)
var request *http.Request = args[3].(*http.Request)
if zone != "view_forum" {
return false
}
var forum *Forum = args[1].(*Forum)
if forum.ParentType == "socialgroup" {
// This is why I hate using contexts, all the daisy chains and interface casts x.x
sgItem, ok := request.Context().Value("socialgroups_current_group").(SocialGroup)
if !ok {
LogError(errors.New("Unable to find a parent group in the context data"))
return false
}
if headerVars.ExtData.items == nil {
headerVars.ExtData.items = make(map[string]interface{})
}
headerVars.ExtData.items["socialgroups_current_group"] = sgItem
return socialgroups_group_widgets(headerVars,sgItem)
}
return false
}