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  }