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 }