github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/extend/guilds/lib/guilds.go (about) 1 package guilds // import "github.com/Azareal/Gosora/extend/guilds/lib" 2 3 import ( 4 "bytes" 5 "context" 6 "database/sql" 7 "errors" 8 "html/template" 9 "net/http" 10 "strconv" 11 "strings" 12 13 c "github.com/Azareal/Gosora/common" 14 "github.com/Azareal/Gosora/routes" 15 ) 16 17 // A blank list to fill out that parameter in Page for routes which don't use it 18 var tList []interface{} 19 20 var ListStmt *sql.Stmt 21 var MemberListStmt *sql.Stmt 22 var MemberListJoinStmt *sql.Stmt 23 var GetMemberStmt *sql.Stmt 24 var AttachForumStmt *sql.Stmt 25 var UnattachForumStmt *sql.Stmt 26 var AddMemberStmt *sql.Stmt 27 28 // Guild is a struct representing a guild 29 type Guild struct { 30 ID int 31 Link string 32 Name string 33 Desc string 34 Active bool 35 Privacy int /* 0: Public, 1: Protected, 2: Private */ 36 37 // 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. 38 Joinable int /* 0: Private, 1: Anyone can join, 2: Applications, 3: Invite-only */ 39 40 MemberCount int 41 Owner int 42 Backdrop string 43 CreatedAt string 44 LastUpdateTime string 45 46 MainForumID int 47 MainForum *c.Forum 48 Forums []*c.Forum 49 ExtData c.ExtData 50 } 51 52 type Page struct { 53 Title string 54 Header *c.Header 55 ItemList []*c.TopicsRow 56 Forum *c.Forum 57 Guild *Guild 58 Page int 59 LastPage int 60 } 61 62 // ListPage is a page struct for constructing a list of every guild 63 type ListPage struct { 64 Title string 65 Header *c.Header 66 GuildList []*Guild 67 } 68 69 type MemberListPage struct { 70 Title string 71 Header *c.Header 72 ItemList []Member 73 Guild *Guild 74 Page int 75 LastPage int 76 } 77 78 // Member is a struct representing a specific member of a guild, not to be confused with the global User struct. 79 type Member struct { 80 Link string 81 Rank int /* 0: Member. 1: Mod. 2: Admin. */ 82 RankString string /* Member, Mod, Admin, Owner */ 83 PostCount int 84 JoinedAt string 85 Offline bool // TODO: Need to track the online states of members when WebSockets are enabled 86 87 User c.User 88 } 89 90 func PrebuildTmplList(user *c.User, h *c.Header) c.CTmpl { 91 guildList := []*Guild{ 92 &Guild{ 93 ID: 1, 94 Name: "lol", 95 Link: BuildGuildURL(c.NameToSlug("lol"), 1), 96 Desc: "A group for people who like to laugh", 97 Active: true, 98 MemberCount: 1, 99 Owner: 1, 100 CreatedAt: "date", 101 LastUpdateTime: "date", 102 MainForumID: 1, 103 MainForum: c.Forums.DirtyGet(1), 104 Forums: []*c.Forum{c.Forums.DirtyGet(1)}, 105 }, 106 } 107 listPage := ListPage{"Guild List", user, h, guildList} 108 return c.CTmpl{"guilds_guild_list", "guilds_guild_list.html", "templates/", "guilds.ListPage", listPage, []string{"./extend/guilds/lib"}} 109 } 110 111 // TODO: Do this properly via the widget system 112 // TODO: REWRITE THIS 113 func CommonAreaWidgets(header *c.Header) { 114 // TODO: Hot Groups? Featured Groups? Official Groups? 115 var b bytes.Buffer 116 menu := c.WidgetMenu{"Guilds", []c.WidgetMenuItem{ 117 c.WidgetMenuItem{"Create Guild", "/guild/create/", false}, 118 }} 119 120 err := header.Theme.RunTmpl("widget_menu", pi, w) 121 if err != nil { 122 c.LogError(err) 123 return 124 } 125 126 if header.Theme.HasDock("leftSidebar") { 127 header.Widgets.LeftSidebar = template.HTML(string(b.Bytes())) 128 } else if header.Theme.HasDock("rightSidebar") { 129 header.Widgets.RightSidebar = template.HTML(string(b.Bytes())) 130 } 131 } 132 133 // TODO: Do this properly via the widget system 134 // TODO: Make a better more customisable group widget system 135 func GuildWidgets(header *c.Header, guildItem *Guild) (success bool) { 136 return false // Disabled until the next commit 137 138 /*var b bytes.Buffer 139 var menu WidgetMenu = WidgetMenu{"Guild Options", []WidgetMenuItem{ 140 WidgetMenuItem{"Join", "/guild/join/" + strconv.Itoa(guildItem.ID), false}, 141 WidgetMenuItem{"Members", "/guild/members/" + strconv.Itoa(guildItem.ID), false}, 142 }} 143 144 err := templates.ExecuteTemplate(&b, "widget_menu.html", menu) 145 if err != nil { 146 c.LogError(err) 147 return false 148 } 149 150 if themes[header.Theme.Name].Sidebars == "left" { 151 header.Widgets.LeftSidebar = template.HTML(string(b.Bytes())) 152 } else if themes[header.Theme.Name].Sidebars == "right" || themes[header.Theme.Name].Sidebars == "both" { 153 header.Widgets.RightSidebar = template.HTML(string(b.Bytes())) 154 } else { 155 return false 156 } 157 return true*/ 158 } 159 160 /* 161 Custom Pages 162 */ 163 164 func RouteGuildList(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError { 165 h, ferr := c.UserCheck(w, r, user) 166 if ferr != nil { 167 return ferr 168 } 169 CommonAreaWidgets(h) 170 171 rows, err := ListStmt.Query() 172 if err != nil && err != c.ErrNoRows { 173 return c.InternalError(err, w, r) 174 } 175 defer rows.Close() 176 177 var guildList []*Guild 178 for rows.Next() { 179 g := &Guild{ID: 0} 180 err := rows.Scan(&g.ID, &g.Name, &g.Desc, &g.Active, &g.Privacy, &g.Joinable, &g.Owner, &g.MemberCount, &g.CreatedAt, &g.LastUpdateTime) 181 if err != nil { 182 return c.InternalError(err, w, r) 183 } 184 g.Link = BuildGuildURL(c.NameToSlug(g.Name), g.ID) 185 guildList = append(guildList, g) 186 } 187 if err = rows.Err(); err != nil { 188 return c.InternalError(err, w, r) 189 } 190 191 pi := ListPage{"Guild List", user, h, guildList} 192 return routes.RenderTemplate("guilds_guild_list", w, r, h, pi) 193 } 194 195 func MiddleViewGuild(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError { 196 _, guildID, err := routes.ParseSEOURL(r.URL.Path[len("/guild/"):]) 197 if err != nil { 198 return c.PreError("Not a valid guild ID", w, r) 199 } 200 201 guildItem, err := Gstore.Get(guildID) 202 if err != nil { 203 return c.LocalError("Bad guild", w, r, user) 204 } 205 // TODO: Build and pass header 206 if !guildItem.Active { 207 return c.NotFound(w, r, nil) 208 } 209 210 return nil 211 212 // TODO: Re-implement this 213 // Re-route the request to routeForums 214 //var ctx = context.WithValue(r.Context(), "guilds_current_guild", guildItem) 215 //return routeForum(w, r.WithContext(ctx), user, strconv.Itoa(guildItem.MainForumID)) 216 } 217 218 func RouteCreateGuild(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError { 219 h, ferr := c.UserCheck(w, r, user) 220 if ferr != nil { 221 return ferr 222 } 223 h.Title = "Create Guild" 224 // TODO: Add an approval queue mode for group creation 225 if !user.Loggedin || !user.PluginPerms["CreateGuild"] { 226 return c.NoPermissions(w, r, user) 227 } 228 CommonAreaWidgets(h) 229 230 return routes.RenderTemplate("guilds_create_guild", w, r, h, c.Page{h, tList, nil}) 231 } 232 233 func RouteCreateGuildSubmit(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError { 234 // TODO: Add an approval queue mode for group creation 235 if !user.Loggedin || !user.PluginPerms["CreateGuild"] { 236 return c.NoPermissions(w, r, user) 237 } 238 239 guildActive := true 240 guildName := c.SanitiseSingleLine(r.PostFormValue("group_name")) 241 // TODO: Allow Markdown / BBCode / Limited HTML in the description? 242 guildDesc := c.SanitiseBody(r.PostFormValue("group_desc")) 243 244 var guildPrivacy int 245 switch r.PostFormValue("group_privacy") { 246 case "0": 247 guildPrivacy = 0 // Public 248 case "1": 249 guildPrivacy = 1 // Protected 250 case "2": 251 guildPrivacy = 2 // private 252 default: 253 guildPrivacy = 0 254 } 255 256 // Create the backing forum 257 fid, err := c.Forums.Create(guildName, "", true, "") 258 if err != nil { 259 return c.InternalError(err, w, r) 260 } 261 262 gid, err := Gstore.Create(guildName, guildDesc, guildActive, guildPrivacy, user.ID, fid) 263 if err != nil { 264 return c.InternalError(err, w, r) 265 } 266 267 // Add the main backing forum to the forum list 268 err = AttachForum(gid, fid) 269 if err != nil { 270 return c.InternalError(err, w, r) 271 } 272 273 _, err = AddMemberStmt.Exec(gid, user.ID, 2) 274 if err != nil { 275 return c.InternalError(err, w, r) 276 } 277 278 http.Redirect(w, r, BuildGuildURL(c.NameToSlug(guildName), gid), http.StatusSeeOther) 279 return nil 280 } 281 282 func RouteMemberList(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError { 283 header, ferr := c.UserCheck(w, r, &user) 284 if ferr != nil { 285 return ferr 286 } 287 288 _, guildID, err := routes.ParseSEOURL(r.URL.Path[len("/guild/members/"):]) 289 if err != nil { 290 return c.PreError("Not a valid group ID", w, r) 291 } 292 293 guild, err := Gstore.Get(guildID) 294 if err != nil { 295 return c.LocalError("Bad group", w, r, user) 296 } 297 guild.Link = BuildGuildURL(c.NameToSlug(guild.Name), guild.ID) 298 299 GuildWidgets(header, guild) 300 301 rows, err := MemberListJoinStmt.Query(guildID) 302 if err != nil && err != c.ErrNoRows { 303 return c.InternalError(err, w, r) 304 } 305 306 var guildMembers []Member 307 for rows.Next() { 308 gMember := Member{PostCount: 0} 309 err := rows.Scan(&gMember.User.ID, &gMember.Rank, &gMember.PostCount, &gMember.JoinedAt, &gMember.User.Name, &gMember.User.RawAvatar) 310 if err != nil { 311 return c.InternalError(err, w, r) 312 } 313 gMember.Link = c.BuildProfileURL(c.NameToSlug(gMember.User.Name), gMember.User.ID) 314 gMember.User.Avatar, gMember.User.MicroAvatar = c.BuildAvatar(gMember.User.ID, gMember.User.RawAvatar) 315 gMember.JoinedAt, _ = c.RelativeTimeFromString(gMember.JoinedAt) 316 if guild.Owner == gMember.User.ID { 317 gMember.RankString = "Owner" 318 } else { 319 switch gMember.Rank { 320 case 0: 321 gMember.RankString = "Member" 322 case 1: 323 gMember.RankString = "Mod" 324 case 2: 325 gMember.RankString = "Admin" 326 } 327 } 328 guildMembers = append(guildMembers, gMember) 329 } 330 if err = rows.Err(); err != nil { 331 return c.InternalError(err, w, r) 332 } 333 rows.Close() 334 335 pi := MemberListPage{"Guild Member List", user, header, gMembers, guild, 0, 0} 336 // A plugin with plugins. Pluginception! 337 if c.RunPreRenderHook("pre_render_guilds_member_list", w, r, user, &pi) { 338 return nil 339 } 340 err = c.RunThemeTemplate(header.Theme.Name, "guilds_member_list", pi, w) 341 if err != nil { 342 return c.InternalError(err, w, r) 343 } 344 return nil 345 } 346 347 func AttachForum(guildID, fid int) error { 348 _, err := AttachForumStmt.Exec(guildID, fid) 349 return err 350 } 351 352 func UnattachForum(fid int) error { 353 _, err := AttachForumStmt.Exec(fid) 354 return err 355 } 356 357 func BuildGuildURL(slug string, id int) string { 358 if slug == "" || !c.Config.BuildSlugs { 359 return "/guild/" + strconv.Itoa(id) 360 } 361 return "/guild/" + slug + "." + strconv.Itoa(id) 362 } 363 364 /* 365 Hooks 366 */ 367 368 // TODO: Prebuild this template 369 func PreRenderViewForum(w http.ResponseWriter, r *http.Request, user *c.User, data interface{}) (halt bool) { 370 pi := data.(*c.ForumPage) 371 if pi.Header.ExtData.Items != nil { 372 if guildData, ok := pi.Header.ExtData.Items["guilds_current_group"]; ok { 373 guildItem := guildData.(*Guild) 374 375 guildpi := Page{pi.Title, pi.Header, pi.ItemList, pi.Forum, guildItem, pi.Page, pi.LastPage} 376 err := routes.RenderTemplate("guilds_view_guild", w, r, pi.Header, guildpi) 377 if err != nil { 378 c.LogError(err) 379 return false 380 } 381 return true 382 } 383 } 384 return false 385 } 386 387 func TrowAssign(args ...interface{}) interface{} { 388 var forum = args[1].(*c.Forum) 389 if forum.ParentType == "guild" { 390 var topicItem = args[0].(*c.TopicsRow) 391 topicItem.ForumLink = "/guild/" + strings.TrimPrefix(topicItem.ForumLink, c.GetForumURLPrefix()) 392 } 393 return nil 394 } 395 396 // TODO: 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 397 func TopicCreatePreLoop(args ...interface{}) interface{} { 398 var fid = args[2].(int) 399 if c.Forums.DirtyGet(fid).ParentType == "guild" { 400 var strictmode = args[5].(*bool) 401 *strictmode = true 402 } 403 return nil 404 } 405 406 // TODO: Add privacy options 407 // TODO: Add support for multiple boards and add per-board simplified permissions 408 // TODO: Take js into account for routes which expect JSON responses 409 func ForumCheck(args ...interface{}) (skip bool, rerr c.RouteError) { 410 r := args[1].(*http.Request) 411 fid := args[3].(*int) 412 forum := c.Forums.DirtyGet(*fid) 413 414 if forum.ParentType == "guild" { 415 var err error 416 w := args[0].(http.ResponseWriter) 417 guildItem, ok := r.Context().Value("guilds_current_group").(*Guild) 418 if !ok { 419 guildItem, err = Gstore.Get(forum.ParentID) 420 if err != nil { 421 return true, c.InternalError(errors.New("Unable to find the parent group for a forum"), w, r) 422 } 423 if !guildItem.Active { 424 return true, c.NotFound(w, r, nil) // TODO: Can we pull header out of args? 425 } 426 r = r.WithContext(context.WithValue(r.Context(), "guilds_current_group", guildItem)) 427 } 428 429 user := args[2].(*c.User) 430 var rank, posts int 431 var joinedAt string 432 433 // TODO: Group privacy settings. For now, groups are all globally visible 434 435 // Clear the default group permissions 436 // TODO: Do this more efficiently, doing it quick and dirty for now to get this out quickly 437 c.OverrideForumPerms(&user.Perms, false) 438 user.Perms.ViewTopic = true 439 440 err = GetMemberStmt.QueryRow(guildItem.ID, user.ID).Scan(&rank, &posts, &joinedAt) 441 if err != nil && err != c.ErrNoRows { 442 return true, c.InternalError(err, w, r) 443 } else if err != nil { 444 // TODO: Should we let admins / guests into public groups? 445 return true, c.LocalError("You're not part of this group!", w, r, user) 446 } 447 448 // TODO: Implement bans properly by adding the Local Ban API in the next commit 449 // TODO: How does this even work? Refactor it along with the rest of this plugin! 450 if rank < 0 { 451 return true, c.LocalError("You've been banned from this group!", w, r, user) 452 } 453 454 // Basic permissions for members, more complicated permissions coming in the next commit! 455 if guildItem.Owner == user.ID { 456 c.OverrideForumPerms(&user.Perms, true) 457 } else if rank == 0 { 458 user.Perms.LikeItem = true 459 user.Perms.CreateTopic = true 460 user.Perms.CreateReply = true 461 } else { 462 c.OverrideForumPerms(&user.Perms, true) 463 } 464 return true, nil 465 } 466 467 return false, nil 468 } 469 470 // TODO: Override redirects? I don't think this is needed quite yet 471 472 func Widgets(args ...interface{}) interface{} { 473 zone := args[0].(string) 474 h := args[2].(*c.Header) 475 request := args[3].(*http.Request) 476 if zone != "view_forum" { 477 return false 478 } 479 480 f := args[1].(*c.Forum) 481 if f.ParentType == "guild" { 482 // This is why I hate using contexts, all the daisy chains and interface casts x.x 483 guild, ok := request.Context().Value("guilds_current_group").(*Guild) 484 if !ok { 485 c.LogError(errors.New("Unable to find a parent group in the context data")) 486 return false 487 } 488 489 if h.ExtData.Items == nil { 490 h.ExtData.Items = make(map[string]interface{}) 491 } 492 h.ExtData.Items["guilds_current_group"] = guild 493 494 return GuildWidgets(h, guild) 495 } 496 return false 497 }