diff --git a/app/http/endpoints/api/blacklist/blacklist.go b/app/http/endpoints/api/blacklist/blacklist.go index 5f36d69..ea9388e 100644 --- a/app/http/endpoints/api/blacklist/blacklist.go +++ b/app/http/endpoints/api/blacklist/blacklist.go @@ -2,20 +2,20 @@ package api import ( "context" - "fmt" "github.com/TicketsBot/GoPanel/database" "github.com/TicketsBot/GoPanel/rpc/cache" "github.com/gin-gonic/gin" + "github.com/rxdn/gdl/objects/user" "golang.org/x/sync/errgroup" - "strconv" - "sync" ) type userData struct { - Username string `json:"username"` - Discriminator string `json:"discriminator"` + UserId uint64 `json:"id,string"` + Username string `json:"username"` + Discriminator user.Discriminator `json:"discriminator"` } +// TODO: Paginate func GetBlacklistHandler(ctx *gin.Context) { guildId := ctx.Keys["guildid"].(uint64) @@ -23,29 +23,31 @@ func GetBlacklistHandler(ctx *gin.Context) { if err != nil { ctx.JSON(500, gin.H{ "success": false, - "error": err.Error(), + "error": err.Error(), }) return } - data := make(map[string]userData) - var lock sync.Mutex + data := make([]userData, len(blacklistedUsers)) group, _ := errgroup.WithContext(context.Background()) - for _, userId := range blacklistedUsers { + for i, userId := range blacklistedUsers { + i := i + userId := userId + + // TODO: Mass lookup group.Go(func() error { - user, _ := cache.Instance.GetUser(userId) - - lock.Lock() - - // JS cant do big ints - data[strconv.FormatUint(userId, 10)] = userData{ - Username: user.Username, - Discriminator: fmt.Sprintf("%04d", user.Discriminator), + userData := userData{ + UserId: userId, } - lock.Unlock() + user, ok := cache.Instance.GetUser(userId) + if ok { + userData.Username = user.Username + userData.Discriminator = user.Discriminator + } + data[i] = userData return nil }) } diff --git a/app/http/endpoints/api/blacklist/blacklistadd.go b/app/http/endpoints/api/blacklist/blacklistadd.go index bd3dd43..c0b978d 100644 --- a/app/http/endpoints/api/blacklist/blacklistadd.go +++ b/app/http/endpoints/api/blacklist/blacklistadd.go @@ -1,64 +1,37 @@ package api import ( - "context" - "errors" - "fmt" "github.com/TicketsBot/GoPanel/database" - "github.com/TicketsBot/GoPanel/rpc/cache" + "github.com/TicketsBot/GoPanel/utils" + "github.com/TicketsBot/common/permission" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v4" "strconv" ) func AddBlacklistHandler(ctx *gin.Context) { guildId := ctx.Keys["guildid"].(uint64) - var data userData - if err := ctx.BindJSON(&data); err != nil { - ctx.AbortWithStatusJSON(400, gin.H{ - "success": false, - "error": err.Error(), - }) - return - } - - parsedDiscrim, err := strconv.ParseInt(data.Discriminator, 10, 16) + id, err := strconv.ParseUint(ctx.Param("user"), 10, 64) if err != nil { - ctx.AbortWithStatusJSON(400, gin.H{ - "success": false, - "error": err.Error(), - }) + ctx.JSON(400, utils.ErrorJson(err)) return } - var targetId uint64 - if err := cache.Instance.QueryRow(context.Background(), `select users.user_id from "users" where LOWER(users.data->>'Username')=LOWER($1) AND users.data->>'Discriminator'=$2;`, data.Username, strconv.FormatInt(parsedDiscrim, 10)).Scan(&targetId); err != nil { - if errors.Is(err, pgx.ErrNoRows) { - ctx.AbortWithStatusJSON(404, gin.H{ - "success": false, - "error": "user not found", - }) - } else { - fmt.Println(err.Error()) - ctx.AbortWithStatusJSON(500, gin.H{ - "success": false, - "error": err.Error(), - }) - } + permLevel, err := utils.GetPermissionLevel(guildId, id) + if err != nil { + ctx.JSON(500, utils.ErrorJson(err)) return } - // TODO: Don't blacklist staff or guild owner - if err = database.Client.Blacklist.Add(guildId, targetId); err == nil { - ctx.JSON(200, gin.H{ - "success": true, - "user_id": strconv.FormatUint(targetId, 10), - }) - } else { - ctx.JSON(500, gin.H{ - "success": false, - "error": err.Error(), - }) + if permLevel > permission.Everyone { + ctx.JSON(400, utils.ErrorStr("You cannot blacklist staff members!")) + return } + + if err = database.Client.Blacklist.Add(guildId, id); err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + ctx.JSON(200, utils.SuccessResponse) } diff --git a/app/http/endpoints/api/logs/logslist.go b/app/http/endpoints/api/logs/logslist.go deleted file mode 100644 index bb945a5..0000000 --- a/app/http/endpoints/api/logs/logslist.go +++ /dev/null @@ -1,128 +0,0 @@ -package api - -import ( - "context" - "fmt" - "github.com/TicketsBot/GoPanel/botcontext" - dbclient "github.com/TicketsBot/GoPanel/database" - "github.com/TicketsBot/GoPanel/rpc/cache" - "github.com/TicketsBot/GoPanel/utils" - "github.com/TicketsBot/database" - "github.com/apex/log" - "github.com/gin-gonic/gin" - "github.com/rxdn/gdl/rest" - "strconv" -) - -const ( - pageLimit = 30 -) - -func GetLogs(ctx *gin.Context) { - guildId := ctx.Keys["guildid"].(uint64) - - botContext, err := botcontext.ContextForGuild(guildId) - if err != nil { - ctx.AbortWithStatusJSON(500, gin.H{ - "success": false, - "error": err.Error(), - }) - return - } - - before, err := strconv.Atoi(ctx.Query("before")) - if before < 0 { - before = 0 - } - - // Get ticket ID from URL - var ticketId int - if utils.IsInt(ctx.Query("ticketid")) { - ticketId, _ = strconv.Atoi(ctx.Query("ticketid")) - } - - var tickets []database.Ticket - - // Get tickets from DB - if ticketId > 0 { - ticket, err := dbclient.Client.Tickets.Get(ticketId, guildId) - if err != nil { - ctx.AbortWithStatusJSON(500, gin.H{ - "success": false, - "error": err.Error(), - }) - return - } - - if ticket.UserId != 0 && !ticket.Open { - tickets = append(tickets, ticket) - } - } else { - // make slice of user IDs to filter by - filteredIds := make([]uint64, 0) - - // Add userid param to slice - filteredUserId, _ := strconv.ParseUint(ctx.Query("userid"), 10, 64) - if filteredUserId != 0 { - filteredIds = append(filteredIds, filteredUserId) - } - - // Get username from URL - if username := ctx.Query("username"); username != "" { - // username -> user id - rows, err := cache.Instance.PgCache.Query(context.Background(), `select users.user_id from users where LOWER("data"->>'Username') LIKE LOWER($1) and exists(SELECT FROM members where members.guild_id=$2);`, fmt.Sprintf("%%%s%%", username), guildId) - defer rows.Close() - if err != nil { - log.Error(err.Error()) - return - } - - for rows.Next() { - var filteredId uint64 - if err := rows.Scan(&filteredId); err != nil { - continue - } - - if filteredId != 0 { - filteredIds = append(filteredIds, filteredId) - } - } - } - - if ctx.Query("userid") != "" || ctx.Query("username") != "" { - tickets, err = dbclient.Client.Tickets.GetMemberClosedTickets(guildId, filteredIds, pageLimit, before) - } else { - tickets, err = dbclient.Client.Tickets.GetGuildClosedTickets(guildId, pageLimit, before) - } - - if err != nil { - ctx.AbortWithStatusJSON(500, gin.H{ - "success": false, - "error": err.Error(), - }) - return - } - } - - // Select 30 logs + format them - formattedLogs := make([]map[string]interface{}, 0) - for _, ticket := range tickets { - // get username - user, found := cache.Instance.GetUser(ticket.UserId) - if !found { - user, err = rest.GetUser(botContext.Token, botContext.RateLimiter, ticket.UserId) - if err != nil { - log.Error(err.Error()) - } - go cache.Instance.StoreUser(user) - } - - formattedLogs = append(formattedLogs, map[string]interface{}{ - "ticketid": ticket.Id, - "userid": strconv.FormatUint(ticket.UserId, 10), - "username": user.Username, - }) - } - - ctx.JSON(200, formattedLogs) -} diff --git a/app/http/endpoints/api/panel/multipanelcreate.go b/app/http/endpoints/api/panel/multipanelcreate.go index c324ea7..f0e9d5f 100644 --- a/app/http/endpoints/api/panel/multipanelcreate.go +++ b/app/http/endpoints/api/panel/multipanelcreate.go @@ -121,16 +121,22 @@ func (d *multiPanelCreateData) doValidations(guildId uint64) (panels []database. } func (d *multiPanelCreateData) validateTitle() (err error) { - if len(d.Title) > 255 || len(d.Title) < 1 { - err = errors.New("embed title must be between 1 and 255 characters") + if len(d.Title) > 255 { + err = errors.New("Embed title must be between 1 and 255 characters") + } else if len(d.Title) == 0 { + d.Title = "Click to open a ticket" } + return } func (d *multiPanelCreateData) validateContent() (err error) { - if len(d.Content) > 1024 || len(d.Title) < 1 { - err = errors.New("embed content must be between 1 and 1024 characters") + if len(d.Content) > 1024 { + err = errors.New("Embed content must be between 1 and 1024 characters") + } else if len(d.Content) == 0 { // Fill default + d.Content = "Click on the button corresponding to the type of ticket you wish to open" } + return } diff --git a/app/http/endpoints/api/panel/multipanellist.go b/app/http/endpoints/api/panel/multipanellist.go index 1a9ccce..7f8aac9 100644 --- a/app/http/endpoints/api/panel/multipanellist.go +++ b/app/http/endpoints/api/panel/multipanellist.go @@ -12,7 +12,7 @@ import ( func MultiPanelList(ctx *gin.Context) { type multiPanelResponse struct { database.MultiPanel - Panels []database.Panel `json:"panels"` + Panels []int `json:"panels"` } guildId := ctx.Keys["guildid"].(uint64) @@ -33,13 +33,20 @@ func MultiPanelList(ctx *gin.Context) { MultiPanel: multiPanel, } + // TODO: Use a join group.Go(func() error { panels, err := dbclient.Client.MultiPanelTargets.GetPanels(multiPanel.Id) if err != nil { return err } - data[i].Panels = panels + panelIds := make([]int, len(panels)) + for i, panel := range panels { + panelIds[i] = panel.PanelId + } + + data[i].Panels = panelIds + return nil }) } @@ -51,6 +58,6 @@ func MultiPanelList(ctx *gin.Context) { ctx.JSON(200, gin.H{ "success": true, - "data": data, + "data": data, }) } diff --git a/app/http/endpoints/api/panel/panelcreate.go b/app/http/endpoints/api/panel/panelcreate.go index 240b21d..f51cb7b 100644 --- a/app/http/endpoints/api/panel/panelcreate.go +++ b/app/http/endpoints/api/panel/panelcreate.go @@ -26,16 +26,17 @@ import ( const freePanelLimit = 3 type panelBody struct { - ChannelId uint64 `json:"channel_id,string"` - MessageId uint64 `json:"message_id,string"` - Title string `json:"title"` - Content string `json:"content"` - Colour uint32 `json:"colour"` - CategoryId uint64 `json:"category_id,string"` - Emote string `json:"emote"` - WelcomeMessage *string `json:"welcome_message"` - Mentions []string `json:"mentions"` - Teams []string `json:"teams"` + ChannelId uint64 `json:"channel_id,string"` + MessageId uint64 `json:"message_id,string"` + Title string `json:"title"` + Content string `json:"content"` + Colour uint32 `json:"colour"` + CategoryId uint64 `json:"category_id,string"` + Emote string `json:"emote"` + WelcomeMessage *string `json:"welcome_message"` + Mentions []string `json:"mentions"` + WithDefaultTeam bool `json:"default_team"` + Teams []database.SupportTeam `json:"teams"` } func CreatePanel(ctx *gin.Context) { @@ -120,7 +121,7 @@ func CreatePanel(ctx *gin.Context) { TargetCategory: data.CategoryId, ReactionEmote: emoji, WelcomeMessage: data.WelcomeMessage, - WithDefaultTeam: utils.ContainsString(data.Teams, "default"), + WithDefaultTeam: data.WithDefaultTeam, CustomId: customId, } @@ -179,31 +180,22 @@ func CreatePanel(ctx *gin.Context) { } // returns (response_code, error) -func insertTeams(guildId uint64, panelId int, teamIds []string) (int, error) { +func insertTeams(guildId uint64, panelId int, teams []database.SupportTeam) (int, error) { // insert teams group, _ := errgroup.WithContext(context.Background()) - for _, teamId := range teamIds { - if teamId == "default" { - continue // already handled - } - - teamId, err := strconv.Atoi(teamId) - if err != nil { - return 400, err - } - + for _, team := range teams { group.Go(func() error { // ensure team exists - exists, err := dbclient.Client.SupportTeam.Exists(teamId, guildId) + exists, err := dbclient.Client.SupportTeam.Exists(team.Id, guildId) if err != nil { return err } if !exists { - return fmt.Errorf("team with id %d not found", teamId) + return fmt.Errorf("team with id %d not found", team.Id) } - return dbclient.Client.PanelTeams.Add(panelId, teamId) + return dbclient.Client.PanelTeams.Add(panelId, team.Id) }) } @@ -266,11 +258,23 @@ func (p *panelBody) doValidations(ctx *gin.Context, guildId uint64) bool { } func (p *panelBody) verifyTitle() bool { - return len(p.Title) > 0 && len(p.Title) <= 80 + if len(p.Title) > 80 { + return false + } else if len(p.Title) == 0 { // Fill default + p.Title = "Open a ticket!" + } + + return true } func (p *panelBody) verifyContent() bool { - return len(p.Content) > 0 && len(p.Content) < 1025 + if len(p.Content) > 1024 { + return false + } else if len(p.Content) == 0 { // Fill default + p.Content = "By clicking the button, a ticket will be opened for you." + } + + return true } func (p *panelBody) getEmoji() (emoji string, ok bool) { diff --git a/app/http/endpoints/api/panel/panelupdate.go b/app/http/endpoints/api/panel/panelupdate.go index a9b5f22..bcb47a2 100644 --- a/app/http/endpoints/api/panel/panelupdate.go +++ b/app/http/endpoints/api/panel/panelupdate.go @@ -1,7 +1,6 @@ package api import ( - "context" "errors" "github.com/TicketsBot/GoPanel/botcontext" dbclient "github.com/TicketsBot/GoPanel/database" @@ -12,9 +11,7 @@ import ( "github.com/gin-gonic/gin" "github.com/rxdn/gdl/rest" "github.com/rxdn/gdl/rest/request" - "golang.org/x/sync/errgroup" "strconv" - "sync" ) func UpdatePanel(ctx *gin.Context) { @@ -67,48 +64,39 @@ func UpdatePanel(ctx *gin.Context) { return } - var wouldHaveDuplicateEmote bool + premiumTier := rpc.PremiumClient.GetTierByGuildId(guildId, true, botContext.Token, botContext.RateLimiter) - { - var duplicateLock sync.Mutex - - group, _ := errgroup.WithContext(context.Background()) - for _, multiPanelId := range multiPanels { - multiPanelId := multiPanelId - - group.Go(func() error { - // get the sub-panels of the multi-panel - subPanels, err := dbclient.Client.MultiPanelTargets.GetPanels(multiPanelId) - if err != nil { - return err - } - - for _, subPanel := range subPanels { - if subPanel.MessageId == existing.MessageId { - continue - } - - if subPanel.ReactionEmote == data.Emote { - duplicateLock.Lock() - wouldHaveDuplicateEmote = true - duplicateLock.Unlock() - break - } - } - - return nil - }) - } - - if err := group.Wait(); err != nil { + for _, multiPanel := range multiPanels { + panels, err := dbclient.Client.MultiPanelTargets.GetPanels(multiPanel.Id) + if err != nil { ctx.JSON(500, utils.ErrorJson(err)) return } - } - if wouldHaveDuplicateEmote { - ctx.JSON(400, utils.ErrorJson(errors.New("Changing the reaction emote to this value would cause a conflict in a multi-panel"))) - return + // TODO: Optimise this + panelIds := make([]int, len(panels)) + for i, panel := range panels { + panelIds[i] = panel.PanelId + } + + data := multiPanelCreateData{ + Title: multiPanel.Title, + Content: multiPanel.Content, + Colour: int32(multiPanel.Colour), + ChannelId: multiPanel.ChannelId, + Panels: panelIds, + } + + messageId, err := data.sendEmbed(&botContext, premiumTier > premium.None, panels) + if err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + if err := dbclient.Client.MultiPanels.UpdateMessageId(multiPanel.Id, messageId); err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } } // check if we need to update the message @@ -125,7 +113,6 @@ func UpdatePanel(ctx *gin.Context) { // delete old message, ignoring error _ = rest.DeleteMessage(botContext.Token, botContext.RateLimiter, existing.ChannelId, existing.MessageId) - premiumTier := rpc.PremiumClient.GetTierByGuildId(guildId, true, botContext.Token, botContext.RateLimiter) newMessageId, err = data.sendEmbed(&botContext, existing.Title, existing.CustomId, existing.ReactionEmote, premiumTier > premium.None) if err != nil { var unwrapped request.RestError @@ -155,7 +142,7 @@ func UpdatePanel(ctx *gin.Context) { TargetCategory: data.CategoryId, ReactionEmote: emoji, WelcomeMessage: data.WelcomeMessage, - WithDefaultTeam: utils.ContainsString(data.Teams, "default"), + WithDefaultTeam: data.WithDefaultTeam, CustomId: existing.CustomId, } diff --git a/app/http/endpoints/api/premium.go b/app/http/endpoints/api/premium.go index d30672a..0d2f9a2 100644 --- a/app/http/endpoints/api/premium.go +++ b/app/http/endpoints/api/premium.go @@ -14,7 +14,7 @@ func PremiumHandler(ctx *gin.Context) { if err != nil { ctx.AbortWithStatusJSON(500, gin.H{ "success": false, - "error": err.Error(), + "error": err.Error(), }) return } @@ -23,5 +23,6 @@ func PremiumHandler(ctx *gin.Context) { ctx.JSON(200, gin.H{ "premium": premiumTier >= premium.Premium, + "tier": premiumTier, }) } diff --git a/app/http/endpoints/api/reloadguilds.go b/app/http/endpoints/api/reloadguilds.go index 60989be..fcfd1a6 100644 --- a/app/http/endpoints/api/reloadguilds.go +++ b/app/http/endpoints/api/reloadguilds.go @@ -2,10 +2,10 @@ package api import ( "fmt" + "github.com/TicketsBot/GoPanel/app/http/session" "github.com/TicketsBot/GoPanel/messagequeue" "github.com/TicketsBot/GoPanel/utils" "github.com/TicketsBot/GoPanel/utils/discord" - "github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/gin" "time" ) @@ -36,19 +36,22 @@ func ReloadGuildsHandler(ctx *gin.Context) { return } - store := sessions.Default(ctx) - if store == nil { - ctx.JSON(200, gin.H{ - "success": false, - "reauthenticate_required": true, - }) + store, err := session.Store.Get(userId) + if err != nil { + if err == session.ErrNoSession { + ctx.JSON(401, gin.H{ + "success": false, + "auth": true, + }) + } else { + ctx.JSON(500, utils.ErrorJson(err)) + } + return } - accessToken := store.Get("access_token").(string) - expiry := store.Get("expiry").(int64) - if expiry > (time.Now().UnixNano() / int64(time.Second)) { - res, err := discord.RefreshToken(store.Get("refresh_token").(string)) + if store.Expiry > (time.Now().UnixNano() / int64(time.Second)) { + res, err := discord.RefreshToken(store.RefreshToken) if err != nil { // Tell client to re-authenticate ctx.JSON(200, gin.H{ "success": false, @@ -57,15 +60,17 @@ func ReloadGuildsHandler(ctx *gin.Context) { return } - accessToken = res.AccessToken + store.AccessToken = res.AccessToken + store.RefreshToken = res.RefreshToken + store.Expiry = (time.Now().UnixNano()/int64(time.Second))+int64(res.ExpiresIn) - store.Set("access_token", res.AccessToken) - store.Set("refresh_token", res.RefreshToken) - store.Set("expiry", (time.Now().UnixNano()/int64(time.Second))+int64(res.ExpiresIn)) - store.Save() + if err := session.Store.Set(userId, store); err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } } - if err := utils.LoadGuilds(accessToken, userId); err != nil { + if err := utils.LoadGuilds(store.AccessToken, userId); err != nil { ctx.JSON(500, utils.ErrorJson(err)) return } diff --git a/app/http/endpoints/api/searchmembers.go b/app/http/endpoints/api/searchmembers.go index 7d2d191..f687e28 100644 --- a/app/http/endpoints/api/searchmembers.go +++ b/app/http/endpoints/api/searchmembers.go @@ -4,6 +4,7 @@ import ( "github.com/TicketsBot/GoPanel/botcontext" "github.com/TicketsBot/GoPanel/utils" "github.com/gin-gonic/gin" + "github.com/rxdn/gdl/objects/member" ) func SearchMembers(ctx *gin.Context) { @@ -16,12 +17,18 @@ func SearchMembers(ctx *gin.Context) { } query := ctx.Query("query") - if len(query) == 0 || len(query) > 32 { + if len(query) > 32 { ctx.JSON(400, utils.ErrorStr("Invalid query")) return } - members, err := botCtx.SearchMembers(guildId, query) + var members []member.Member + if query == "" { + members, err = botCtx.ListMembers(guildId) + } else { + members, err = botCtx.SearchMembers(guildId, query) + } + if err != nil { ctx.JSON(500, utils.ErrorJson(err)) return diff --git a/app/http/endpoints/api/session.go b/app/http/endpoints/api/session.go new file mode 100644 index 0000000..94e2411 --- /dev/null +++ b/app/http/endpoints/api/session.go @@ -0,0 +1,31 @@ +package api + +import ( + "github.com/TicketsBot/GoPanel/app/http/session" + "github.com/TicketsBot/GoPanel/utils" + "github.com/gin-gonic/gin" +) + +func SessionHandler(ctx *gin.Context) { + userId := ctx.Keys["userid"].(uint64) + + store, err := session.Store.Get(userId) + if err != nil { + if err == session.ErrNoSession { + ctx.JSON(404, gin.H{ + "success": false, + "error": err.Error(), + "auth": true, + }) + } else { + ctx.JSON(500, utils.ErrorJson(err)) + } + + return + } + + ctx.JSON(200, gin.H{ + "username": store.Name, + "avatar": store.Avatar, + }) +} diff --git a/app/http/endpoints/api/settings/settings.go b/app/http/endpoints/api/settings/settings.go index 5dddba6..9f2de6a 100644 --- a/app/http/endpoints/api/settings/settings.go +++ b/app/http/endpoints/api/settings/settings.go @@ -30,18 +30,30 @@ func GetSettingsHandler(ctx *gin.Context) { // prefix group.Go(func() (err error) { settings.Prefix, err = dbclient.Client.Prefix.Get(guildId) + if err == nil && settings.Prefix == "" { + settings.Prefix = "t!" + } + return }) // welcome message group.Go(func() (err error) { settings.WelcomeMessaage, err = dbclient.Client.WelcomeMessages.Get(guildId) + if err == nil && settings.WelcomeMessaage == "" { + settings.WelcomeMessaage = "Thank you for contacting support.\nPlease describe your issue and await a response." + } + return }) // ticket limit group.Go(func() (err error) { settings.TicketLimit, err = dbclient.Client.TicketLimit.Get(guildId) + if err == nil && settings.TicketLimit == 0 { + settings.TicketLimit = 5 // Set default + } + return }) diff --git a/app/http/endpoints/api/team/getmembers.go b/app/http/endpoints/api/team/getmembers.go index df802da..fe61b9c 100644 --- a/app/http/endpoints/api/team/getmembers.go +++ b/app/http/endpoints/api/team/getmembers.go @@ -114,7 +114,7 @@ func formatMembers(guildId uint64, userIds, roleIds []uint64) ([]entity, error) } // map role ids to names - var data []entity + data := make([]entity, 0) for _, roleId := range roleIds { for _, role := range roles { if roleId == role.Id { diff --git a/app/http/endpoints/api/ticket/getticket.go b/app/http/endpoints/api/ticket/getticket.go index 0adc842..dff8c1a 100644 --- a/app/http/endpoints/api/ticket/getticket.go +++ b/app/http/endpoints/api/ticket/getticket.go @@ -86,7 +86,7 @@ func GetTicket(ctx *gin.Context) { } messagesFormatted = append(messagesFormatted, map[string]interface{}{ - "username": message.Author.Username, + "author": message.Author, "content": content, }) } diff --git a/app/http/endpoints/api/ticket/gettickets.go b/app/http/endpoints/api/ticket/gettickets.go index ec4b63e..6d3a5d6 100644 --- a/app/http/endpoints/api/ticket/gettickets.go +++ b/app/http/endpoints/api/ticket/gettickets.go @@ -2,27 +2,31 @@ package api import ( "context" - "fmt" "github.com/TicketsBot/GoPanel/database" "github.com/TicketsBot/GoPanel/rpc/cache" "github.com/gin-gonic/gin" + "github.com/rxdn/gdl/objects/user" "golang.org/x/sync/errgroup" - "strconv" ) func GetTickets(ctx *gin.Context) { + type WithUser struct { + TicketId int `json:"id"` + User *user.User `json:"user,omitempty"` + } + guildId := ctx.Keys["guildid"].(uint64) tickets, err := database.Client.Tickets.GetGuildOpenTickets(guildId) if err != nil { ctx.AbortWithStatusJSON(500, gin.H{ "success": false, - "error": err.Error(), + "error": err.Error(), }) return } - ticketsFormatted := make([]map[string]interface{}, len(tickets)) + data := make([]WithUser, len(tickets)) group, _ := errgroup.WithContext(context.Background()) @@ -31,29 +35,14 @@ func GetTickets(ctx *gin.Context) { ticket := ticket group.Go(func() error { - members, err := database.Client.TicketMembers.Get(guildId, ticket.Id) - if err != nil { - return err + user, ok := cache.Instance.GetUser(ticket.UserId) + + data[i] = WithUser{ + TicketId: ticket.Id, } - membersFormatted := make([]map[string]interface{}, 0) - for _, userId := range members { - user, _ := cache.Instance.GetUser(userId) - - membersFormatted = append(membersFormatted, map[string]interface{}{ - "id": strconv.FormatUint(userId, 10), - "username": user.Username, - "discrim": fmt.Sprintf("%04d", user.Discriminator), - }) - } - - owner, _ := cache.Instance.GetUser(ticket.UserId) - - ticketsFormatted[len(tickets) - 1 - i] = map[string]interface{}{ - "ticketId": ticket.Id, - "username": owner.Username, - "discrim": fmt.Sprintf("%04d", owner.Discriminator), - "members": membersFormatted, + if ok { + data[i].User = &user } return nil @@ -63,10 +52,10 @@ func GetTickets(ctx *gin.Context) { if err := group.Wait(); err != nil { ctx.AbortWithStatusJSON(500, gin.H{ "success": false, - "error": err.Error(), + "error": err.Error(), }) return } - ctx.JSON(200, ticketsFormatted) + ctx.JSON(200, data) } diff --git a/app/http/endpoints/api/token.go b/app/http/endpoints/api/token.go deleted file mode 100644 index c5b8f50..0000000 --- a/app/http/endpoints/api/token.go +++ /dev/null @@ -1,36 +0,0 @@ -package api - -import ( - "fmt" - "github.com/TicketsBot/GoPanel/config" - "github.com/TicketsBot/GoPanel/utils" - "github.com/dgrijalva/jwt-go" - "github.com/gin-gonic/contrib/sessions" - "github.com/gin-gonic/gin" - "strconv" - "time" -) - -func TokenHandler(ctx *gin.Context) { - session := sessions.Default(ctx) - userId := utils.GetUserId(session) - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "userid": strconv.FormatUint(userId, 10), - "timestamp": time.Now(), - }) - - str, err := token.SignedString([]byte(config.Conf.Server.Secret)) - if err != nil { - fmt.Println(err.Error()) - ctx.JSON(500, gin.H{ - "success": false, - "error": err.Error(), - }) - } else { - ctx.JSON(200, gin.H{ - "success": true, - "token": str, - }) - } -} diff --git a/app/http/endpoints/api/transcripts/get.go b/app/http/endpoints/api/transcripts/get.go new file mode 100644 index 0000000..33786fc --- /dev/null +++ b/app/http/endpoints/api/transcripts/get.go @@ -0,0 +1,68 @@ +package api + +import ( + "errors" + "github.com/TicketsBot/GoPanel/database" + "github.com/TicketsBot/GoPanel/utils" + "github.com/TicketsBot/archiverclient" + "github.com/TicketsBot/common/permission" + "github.com/gin-gonic/gin" + "strconv" +) + +func GetTranscriptHandler(ctx *gin.Context) { + guildId := ctx.Keys["guildid"].(uint64) + userId := ctx.Keys["userid"].(uint64) + + // format ticket ID + ticketId, err := strconv.Atoi(ctx.Param("ticketId")) + if err != nil { + ctx.JSON(400, utils.ErrorStr("Invalid ticket ID")) + return + } + + // get ticket object + ticket, err := database.Client.Tickets.Get(ticketId, guildId) + if err != nil { + // TODO: 500 error page + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": err.Error(), + }) + return + } + + // Verify this is a valid ticket and it is closed + if ticket.UserId == 0 || ticket.Open { + ctx.JSON(404, utils.ErrorStr("Transcript not found")) + return + } + + // Verify the user has permissions to be here + if ticket.UserId != userId { + permLevel, err := utils.GetPermissionLevel(guildId, userId) + if err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + if permLevel < permission.Support { + ctx.JSON(403, utils.ErrorStr("You do not have permission to view this transcript")) + return + } + } + + // retrieve ticket messages from bucket + messages, err := utils.ArchiverClient.Get(guildId, ticketId) + if err != nil { + if errors.Is(err, archiverclient.ErrExpired) { + ctx.JSON(404, utils.ErrorStr("Transcript not found")) + } else { + ctx.JSON(500, utils.ErrorJson(err)) + } + + return + } + + ctx.JSON(200, messages) +} diff --git a/app/http/endpoints/api/transcripts/list.go b/app/http/endpoints/api/transcripts/list.go new file mode 100644 index 0000000..252efe5 --- /dev/null +++ b/app/http/endpoints/api/transcripts/list.go @@ -0,0 +1,247 @@ +package api + +import ( + "errors" + "github.com/TicketsBot/GoPanel/botcontext" + dbclient "github.com/TicketsBot/GoPanel/database" + "github.com/TicketsBot/GoPanel/rpc/cache" + "github.com/TicketsBot/GoPanel/utils" + "github.com/TicketsBot/database" + "github.com/gin-gonic/gin" + "math" + "net/http" + "strconv" +) + +const ( + pageLimit = 30 +) + +type filterType uint8 + +const ( + filterTypeNone filterType = iota + filterTypeTicketId + filterTypeUsername + filterTypeUserId +) + +type transcript struct { + TicketId int `json:"ticket_id"` + Username string `json:"username"` + CloseReason *string `json:"close_reason"` +} + +func ListTranscripts(ctx *gin.Context) { + guildId := ctx.Keys["guildid"].(uint64) + + botContext, err := botcontext.ContextForGuild(guildId) + if err != nil { + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": err.Error(), + }) + return + } + + // db functions will handle if 0 + before, _ := strconv.Atoi(ctx.Query("before")) + after, _ := strconv.Atoi(ctx.Query("after")) + + var tickets []database.TicketWithCloseReason + var status int + + filterType := getFilterType(ctx) + switch filterType { + case filterTypeNone: + tickets, status, err = getTickets(guildId, before, after) + case filterTypeTicketId: + tickets, status, err = getTicketsByTicketId(guildId, ctx) + case filterTypeUsername: + tickets, status, err = getTicketsByUsername(guildId, before, after, ctx) + case filterTypeUserId: + tickets, status, err = getTicketsByUserId(guildId, before, after, ctx) + } + + if err != nil { + ctx.JSON(status, utils.ErrorJson(err)) + return + } + + // Create a mapping user_id -> username so we can skip duplicates + usernames := make(map[uint64]string) + for _, ticket := range tickets { + if _, ok := usernames[ticket.UserId]; ok { + continue // don't fetch again + } + + // check cache, for some reason botContext.GetUser doesn't do this + user, ok := cache.Instance.GetUser(ticket.UserId) + if ok { + usernames[ticket.UserId] = user.Username + } else { + user, err = botContext.GetUser(ticket.UserId) + if err != nil { // TODO: Log + usernames[ticket.UserId] = "Unknown User" + } else { + usernames[ticket.UserId] = user.Username + } + } + } + + transcripts := make([]transcript, len(tickets)) + for i, ticket := range tickets { + transcripts[i] = transcript{ + TicketId: ticket.Id, + Username: usernames[ticket.UserId], + CloseReason: ticket.CloseReason, + } + } + + ctx.JSON(200, transcripts) +} + +func getFilterType(ctx *gin.Context) filterType { + if ctx.Query("ticketid") != "" { + return filterTypeTicketId + } else if ctx.Query("username") != "" { + return filterTypeUsername + } else if ctx.Query("userid") != "" { + return filterTypeUserId + } else { + return filterTypeNone + } +} + +func getTickets(guildId uint64, before, after int) ([]database.TicketWithCloseReason, int, error) { + var tickets []database.TicketWithCloseReason + var err error + + if before <= 0 && after <= 0 { + tickets, err = dbclient.Client.Tickets.GetGuildClosedTicketsBeforeWithCloseReason(guildId, pageLimit, math.MaxInt32) + } else if before > 0 { + tickets, err = dbclient.Client.Tickets.GetGuildClosedTicketsBeforeWithCloseReason(guildId, pageLimit, before) + } else { // after > 0 + // returns in ascending order, must reverse + tickets, err = dbclient.Client.Tickets.GetGuildClosedTicketsAfterWithCloseReason(guildId, pageLimit, after) + if err == nil { + reverse(tickets) + } + } + + + status := http.StatusOK + if err != nil { + status = http.StatusInternalServerError + } + + return tickets, status, err +} + +// (tickets, statusCode, error) +func getTicketsByTicketId(guildId uint64, ctx *gin.Context) ([]database.TicketWithCloseReason, int, error) { + ticketId, err := strconv.Atoi(ctx.Query("ticketid")) + if err != nil { + return nil, 400, err + } + + ticket, err := dbclient.Client.Tickets.Get(ticketId, guildId) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + if ticket.Id == 0 { + return nil, http.StatusNotFound, errors.New("ticket not found") + } + + closeReason, ok, err := dbclient.Client.CloseReason.Get(guildId, ticketId) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + data := database.TicketWithCloseReason{ + Ticket: ticket, + } + + if ok { + data.CloseReason = &closeReason + } + + return []database.TicketWithCloseReason{data}, http.StatusOK, nil +} + +// (tickets, statusCode, error) +func getTicketsByUsername(guildId uint64, before, after int, ctx *gin.Context) ([]database.TicketWithCloseReason, int, error) { + username := ctx.Query("username") + + botContext, err := botcontext.ContextForGuild(guildId) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + members, err := botContext.SearchMembers(guildId, username) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + userIds := make([]uint64, len(members)) // capped at 100 + for i, member := range members { + userIds[i] = member.User.Id + } + + var tickets []database.TicketWithCloseReason + if before <= 0 && after <= 0 { + tickets, err = dbclient.Client.Tickets.GetClosedByAnyBeforeWithCloseReason(guildId, userIds, math.MaxInt32, pageLimit) + } else if before > 0 { + tickets, err = dbclient.Client.Tickets.GetClosedByAnyBeforeWithCloseReason(guildId, userIds, before, pageLimit) + } else { // after > 0 + // returns in ascending order, must reverse + tickets, err = dbclient.Client.Tickets.GetClosedByAnyAfterWithCloseReason(guildId, userIds, after, pageLimit) + if err == nil { + reverse(tickets) + } + } + + if err != nil { + return nil, http.StatusInternalServerError, err + } + + return tickets, http.StatusOK, nil +} + +// (tickets, statusCode, error) +func getTicketsByUserId(guildId uint64, before, after int, ctx *gin.Context) ([]database.TicketWithCloseReason, int, error) { + userId, err := strconv.ParseUint(ctx.Query("userid"), 10, 64) + if err != nil { + return nil, 400, err + } + + var tickets []database.TicketWithCloseReason + if before <= 0 && after <= 0 { + tickets, err = dbclient.Client.Tickets.GetClosedByAnyBeforeWithCloseReason(guildId, []uint64{userId}, math.MaxInt32, pageLimit) + } else if before > 0 { + tickets, err = dbclient.Client.Tickets.GetClosedByAnyBeforeWithCloseReason(guildId, []uint64{userId}, before, pageLimit) + } else { // after > 0 + // returns in ascending order, must reverse + tickets, err = dbclient.Client.Tickets.GetClosedByAnyAfterWithCloseReason(guildId, []uint64{userId}, after, pageLimit) + if err == nil { + reverse(tickets) + } + } + + if err != nil { + return nil, http.StatusInternalServerError, err + } + + return tickets, http.StatusOK, nil +} + +func reverse(slice []database.TicketWithCloseReason) { + if len(slice) == 0 { + return + } + + for i, j := 0, len(slice)-1; i < j; i, j = i+1, j-1 { + slice[i], slice[j] = slice[j], slice[i] + } +} diff --git a/app/http/endpoints/manage/blacklist.go b/app/http/endpoints/manage/blacklist.go deleted file mode 100644 index 226349d..0000000 --- a/app/http/endpoints/manage/blacklist.go +++ /dev/null @@ -1,19 +0,0 @@ -package manage - -import ( - "github.com/TicketsBot/GoPanel/config" - "github.com/gin-gonic/contrib/sessions" - "github.com/gin-gonic/gin" -) - -func BlacklistHandler(ctx *gin.Context) { - store := sessions.Default(ctx) - guildId := ctx.Keys["guildid"].(uint64) - - ctx.HTML(200, "manage/blacklist", gin.H{ - "name": store.Get("name").(string), - "guildId": guildId, - "avatar": store.Get("avatar").(string), - "baseUrl": config.Conf.Server.BaseUrl, - }) -} diff --git a/app/http/endpoints/manage/logslist.go b/app/http/endpoints/manage/logslist.go deleted file mode 100644 index ab1d0b6..0000000 --- a/app/http/endpoints/manage/logslist.go +++ /dev/null @@ -1,19 +0,0 @@ -package manage - -import ( - "github.com/TicketsBot/GoPanel/config" - "github.com/gin-gonic/contrib/sessions" - "github.com/gin-gonic/gin" -) - -func LogsHandler(ctx *gin.Context) { - store := sessions.Default(ctx) - guildId := ctx.Keys["guildid"].(uint64) - - ctx.HTML(200, "manage/logs", gin.H{ - "name": store.Get("name").(string), - "guildId": guildId, - "avatar": store.Get("avatar").(string), - "baseUrl": config.Conf.Server.BaseUrl, - }) -} diff --git a/app/http/endpoints/manage/logsview.go b/app/http/endpoints/manage/logsview.go deleted file mode 100644 index 98c5839..0000000 --- a/app/http/endpoints/manage/logsview.go +++ /dev/null @@ -1,98 +0,0 @@ -package manage - -import ( - "errors" - "fmt" - "github.com/TicketsBot/GoPanel/config" - "github.com/TicketsBot/GoPanel/database" - "github.com/TicketsBot/GoPanel/rpc/cache" - "github.com/TicketsBot/GoPanel/utils" - "github.com/TicketsBot/archiverclient" - "github.com/TicketsBot/common/permission" - "github.com/gin-gonic/contrib/sessions" - "github.com/gin-gonic/gin" - "strconv" -) - -var Archiver archiverclient.ArchiverClient - -func LogViewHandler(ctx *gin.Context) { - store := sessions.Default(ctx) - if store == nil { - return - } - - if utils.IsLoggedIn(store) { - userId := utils.GetUserId(store) - - // Verify the guild exists - guildId, err := strconv.ParseUint(ctx.Param("id"), 10, 64) - if err != nil { - ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page - return - } - - // Get object for selected guild - guild, _ := cache.Instance.GetGuild(guildId, false) - - // format ticket ID - ticketId, err := strconv.Atoi(ctx.Param("ticket")); if err != nil { - ctx.Redirect(302, fmt.Sprintf("/manage/%d/logs", guild.Id)) - return - } - - // get ticket object - ticket, err := database.Client.Tickets.Get(ticketId, guildId) - if err != nil { - // TODO: 500 error page - ctx.AbortWithStatusJSON(500, gin.H{ - "success": false, - "error": err.Error(), - }) - return - } - - // Verify this is a valid ticket and it is closed - if ticket.UserId == 0 || ticket.Open { - ctx.Redirect(302, fmt.Sprintf("/manage/%d/logs", guild.Id)) - return - } - - // Verify the user has permissions to be here - if ticket.UserId != userId { - permLevel, err := utils.GetPermissionLevel(guildId, userId) - if err != nil { - ctx.JSON(500, utils.ErrorJson(err)) - return - } - - if permLevel < permission.Support { - ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page - return - } - } - - // retrieve ticket messages from bucket - messages, err := Archiver.Get(guildId, ticketId) - if err != nil { - if errors.Is(err, archiverclient.ErrExpired) { - ctx.String(200, "Failed to retrieve archive - please contact the developers quoting error code: ErrExpired") // TODO: Actual error page - return - } - - ctx.String(500, fmt.Sprintf("Failed to retrieve archive - please contact the developers: %s", err.Error())) - return - } - - // format to html - html, err := Archiver.Encode(messages, fmt.Sprintf("ticket-%d", ticketId)) - if err != nil { - ctx.String(500, fmt.Sprintf("Failed to retrieve archive - please contact the developers: %s", err.Error())) - return - } - - ctx.Data(200, gin.MIMEHTML, html) - } else { - ctx.Redirect(302, fmt.Sprintf("/login?noguilds&state=viewlog.%s.%s", ctx.Param("id"), ctx.Param("ticket"))) - } -} diff --git a/app/http/endpoints/manage/panels.go b/app/http/endpoints/manage/panels.go deleted file mode 100644 index 3786196..0000000 --- a/app/http/endpoints/manage/panels.go +++ /dev/null @@ -1,19 +0,0 @@ -package manage - -import ( - "github.com/TicketsBot/GoPanel/config" - "github.com/gin-gonic/contrib/sessions" - "github.com/gin-gonic/gin" -) - -func PanelHandler(ctx *gin.Context) { - store := sessions.Default(ctx) - guildId := ctx.Keys["guildid"].(uint64) - - ctx.HTML(200, "manage/panels", gin.H{ - "name": store.Get("name").(string), - "guildId": guildId, - "avatar": store.Get("avatar").(string), - "baseUrl": config.Conf.Server.BaseUrl, - }) -} diff --git a/app/http/endpoints/manage/settings.go b/app/http/endpoints/manage/settings.go deleted file mode 100644 index cddfa58..0000000 --- a/app/http/endpoints/manage/settings.go +++ /dev/null @@ -1,19 +0,0 @@ -package manage - -import ( - "github.com/TicketsBot/GoPanel/config" - "github.com/gin-gonic/contrib/sessions" - "github.com/gin-gonic/gin" -) - -func SettingsHandler(ctx *gin.Context) { - store := sessions.Default(ctx) - guildId := ctx.Keys["guildid"].(uint64) - - ctx.HTML(200, "manage/settings", gin.H{ - "name": store.Get("name").(string), - "guildId": guildId, - "avatar": store.Get("avatar").(string), - "baseUrl": config.Conf.Server.BaseUrl, - }) -} diff --git a/app/http/endpoints/manage/tags.go b/app/http/endpoints/manage/tags.go deleted file mode 100644 index d2a4f13..0000000 --- a/app/http/endpoints/manage/tags.go +++ /dev/null @@ -1,19 +0,0 @@ -package manage - -import ( - "github.com/TicketsBot/GoPanel/config" - "github.com/gin-gonic/contrib/sessions" - "github.com/gin-gonic/gin" -) - -func TagsHandler(ctx *gin.Context) { - store := sessions.Default(ctx) - guildId := ctx.Keys["guildid"].(uint64) - - ctx.HTML(200, "manage/tags", gin.H{ - "name": store.Get("name").(string), - "guildId": guildId, - "avatar": store.Get("avatar").(string), - "baseUrl": config.Conf.Server.BaseUrl, - }) -} diff --git a/app/http/endpoints/manage/ticketlist.go b/app/http/endpoints/manage/ticketlist.go deleted file mode 100644 index f49b78e..0000000 --- a/app/http/endpoints/manage/ticketlist.go +++ /dev/null @@ -1,19 +0,0 @@ -package manage - -import ( - "github.com/TicketsBot/GoPanel/config" - "github.com/gin-gonic/contrib/sessions" - "github.com/gin-gonic/gin" -) - -func TicketListHandler(ctx *gin.Context) { - store := sessions.Default(ctx) - guildId := ctx.Keys["guildid"].(uint64) - - ctx.HTML(200, "manage/ticketlist", gin.H{ - "name": store.Get("name").(string), - "guildId": guildId, - "avatar": store.Get("avatar").(string), - "baseUrl": config.Conf.Server.BaseUrl, - }) -} diff --git a/app/http/endpoints/manage/ticketview.go b/app/http/endpoints/manage/ticketview.go deleted file mode 100644 index 61fe34d..0000000 --- a/app/http/endpoints/manage/ticketview.go +++ /dev/null @@ -1,20 +0,0 @@ -package manage - -import ( - "github.com/TicketsBot/GoPanel/config" - "github.com/gin-gonic/contrib/sessions" - "github.com/gin-gonic/gin" -) - -func TicketViewHandler(ctx *gin.Context) { - store := sessions.Default(ctx) - guildId := ctx.Keys["guildid"].(uint64) - - ctx.HTML(200, "manage/ticketview", gin.H{ - "name": store.Get("name").(string), - "guildId": guildId, - "avatar": store.Get("avatar").(string), - "baseUrl": config.Conf.Server.BaseUrl, - "ticketId": ctx.Param("ticketId"), - }) -} diff --git a/app/http/endpoints/manage/webchatws.go b/app/http/endpoints/manage/webchatws.go deleted file mode 100644 index 1cdf81a..0000000 --- a/app/http/endpoints/manage/webchatws.go +++ /dev/null @@ -1,146 +0,0 @@ -package manage - -import ( - "fmt" - "github.com/TicketsBot/GoPanel/botcontext" - "github.com/TicketsBot/GoPanel/rpc" - "github.com/TicketsBot/GoPanel/utils" - "github.com/TicketsBot/common/permission" - "github.com/TicketsBot/common/premium" - "github.com/gin-gonic/contrib/sessions" - "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" - "strconv" - "sync" -) - -var upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, -} - -var SocketsLock sync.Mutex -var Sockets []*Socket - -type ( - Socket struct { - Ws *websocket.Conn - Guild string - Ticket int - } - - WsEvent struct { - Type string - Data interface{} - } - - AuthEvent struct { - Guild string - Ticket string - } -) - -func WebChatWs(ctx *gin.Context) { - store := sessions.Default(ctx) - - conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) - if err != nil { - fmt.Println(err.Error()) - return - } - - socket := &Socket{ - Ws: conn, - } - - conn.SetCloseHandler(func(code int, text string) error { - i := -1 - SocketsLock.Lock() - - for index, element := range Sockets { - if element == socket { - i = index - break - } - } - - if i != -1 { - Sockets = Sockets[:i+copy(Sockets[i:], Sockets[i+1:])] - } - SocketsLock.Unlock() - - return nil - }) - - SocketsLock.Lock() - Sockets = append(Sockets, socket) - SocketsLock.Unlock() - userId := utils.GetUserId(store) - - var guildId string - var guildIdParsed uint64 - var ticket int - - for { - var evnt WsEvent - err := conn.ReadJSON(&evnt) - if err != nil { - break - } - - if guildId == "" && evnt.Type != "auth" { - conn.Close() - break - } else if evnt.Type == "auth" { - data := evnt.Data.(map[string]interface{}) - - guildId = data["guild"].(string) - ticket, err = strconv.Atoi(data["ticket"].(string)) - if err != nil { - conn.Close() - break - } - - socket.Guild = guildId - socket.Ticket = ticket - - // Verify the guild exists - guildIdParsed, err = strconv.ParseUint(guildId, 10, 64) - if err != nil { - fmt.Println(err.Error()) - conn.Close() - return - } - - // Verify the user has permissions to be here - permLevel, err := utils.GetPermissionLevel(guildIdParsed, userId) - if err != nil { - fmt.Println(err.Error()) - conn.Close() - return - } - - if permLevel < permission.Admin { - fmt.Println(err.Error()) - conn.Close() - return - } - - botContext, err := botcontext.ContextForGuild(guildIdParsed) - if err != nil { - ctx.AbortWithStatusJSON(500, gin.H{ - "success": false, - "error": err.Error(), - }) - return - } - - // Verify the guild is premium - premiumTier := rpc.PremiumClient.GetTierByGuildId(guildIdParsed, true, botContext.Token, botContext.RateLimiter) - if premiumTier == premium.None { - conn.Close() - return - } - } - } -} diff --git a/app/http/endpoints/root/callback.go b/app/http/endpoints/root/callback.go index beb717d..1ed6008 100644 --- a/app/http/endpoints/root/callback.go +++ b/app/http/endpoints/root/callback.go @@ -2,14 +2,14 @@ package root import ( "fmt" + "github.com/TicketsBot/GoPanel/app/http/session" "github.com/TicketsBot/GoPanel/config" "github.com/TicketsBot/GoPanel/utils" "github.com/TicketsBot/GoPanel/utils/discord" - "github.com/apex/log" - "github.com/gin-gonic/contrib/sessions" + "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" "github.com/rxdn/gdl/rest" - "strings" + "strconv" "time" ) @@ -33,63 +33,59 @@ type ( ) func CallbackHandler(ctx *gin.Context) { - store := sessions.Default(ctx) - if store == nil { - return - } - defer store.Save() - - if utils.IsLoggedIn(store) && store.Get("has_guilds") == true { - ctx.Redirect(302, config.Conf.Server.BaseUrl) - return - } - code, ok := ctx.GetQuery("code") if !ok { - utils.ErrorPage(ctx, 400, "Discord provided invalid Oauth2 code") + ctx.JSON(400, utils.ErrorStr("Discord provided invalid Oauth2 code")) return } res, err := discord.AccessToken(code) if err != nil { - utils.ErrorPage(ctx, 500, err.Error()) + ctx.JSON(500, utils.ErrorJson(err)) return } - store.Set("access_token", res.AccessToken) - store.Set("refresh_token", res.RefreshToken) - store.Set("expiry", (time.Now().UnixNano()/int64(time.Second))+int64(res.ExpiresIn)) - // Get ID + name currentUser, err := rest.GetCurrentUser(fmt.Sprintf("Bearer %s", res.AccessToken), nil) if err != nil { - ctx.String(500, err.Error()) + ctx.JSON(500, utils.ErrorJson(err)) return } - store.Set("csrf", utils.RandString(32)) - - store.Set("userid", currentUser.Id) - store.Set("name", currentUser.Username) - store.Set("avatar", currentUser.AvatarUrl(256)) - store.Save() + store := session.SessionData{ + AccessToken: res.AccessToken, + Expiry: (time.Now().UnixNano()/int64(time.Second))+int64(res.ExpiresIn), + RefreshToken: res.RefreshToken, + Name: currentUser.Username, + Avatar: currentUser.AvatarUrl(256), + HasGuilds: false, + } if err := utils.LoadGuilds(res.AccessToken, currentUser.Id); err == nil { - store.Set("has_guilds", true) - store.Save() + store.HasGuilds = true } else { - log.Error(err.Error()) + ctx.JSON(500, utils.ErrorJson(err)) + return } - handleRedirect(ctx) -} + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "userid": strconv.FormatUint(currentUser.Id, 10), + "timestamp": time.Now(), + }) -func handleRedirect(ctx *gin.Context) { - state := strings.Split(ctx.Query("state"), ".") - - if len(state) == 3 && state[0] == "viewlog" { - ctx.Redirect(302, fmt.Sprintf("%s/manage/%s/logs/view/%s", config.Conf.Server.BaseUrl, state[1], state[2])) - } else { - ctx.Redirect(302, config.Conf.Server.BaseUrl) + str, err := token.SignedString([]byte(config.Conf.Server.Secret)) + if err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return } + + if err := session.Store.Set(currentUser.Id, store); err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + ctx.JSON(200, gin.H{ + "success": true, + "token": str, + }) } diff --git a/app/http/endpoints/root/index.go b/app/http/endpoints/root/index.go deleted file mode 100644 index 3d6d746..0000000 --- a/app/http/endpoints/root/index.go +++ /dev/null @@ -1,27 +0,0 @@ -package root - -import ( - "fmt" - "github.com/TicketsBot/GoPanel/config" - "github.com/gin-gonic/contrib/sessions" - "github.com/gin-gonic/gin" - "net/url" -) - -func IndexHandler(ctx *gin.Context) { - store := sessions.Default(ctx) - - if _, hasGuilds := store.Get("has_guilds").(bool); !hasGuilds { - redirect := url.QueryEscape(config.Conf.Oauth.RedirectUri) - ctx.Redirect(302, fmt.Sprintf("https://discordapp.com/oauth2/authorize?response_type=code&redirect_uri=%s&scope=identify+guilds&client_id=%d&state=%s", redirect, config.Conf.Oauth.Id, ctx.Query("state"))) - return - } - - ctx.HTML(200, "main/index", gin.H{ - "name": store.Get("name").(string), - "baseurl": config.Conf.Server.BaseUrl, - "avatar": store.Get("avatar").(string), - "referralShow": config.Conf.Referral.Show, - "referralLink": config.Conf.Referral.Link, - }) -} diff --git a/app/http/endpoints/root/login.go b/app/http/endpoints/root/login.go deleted file mode 100644 index ea311b8..0000000 --- a/app/http/endpoints/root/login.go +++ /dev/null @@ -1,31 +0,0 @@ -package root - -import ( - "fmt" - "github.com/TicketsBot/GoPanel/config" - "github.com/TicketsBot/GoPanel/utils" - "github.com/gin-gonic/contrib/sessions" - "github.com/gin-gonic/gin" - "net/url" -) - -func LoginHandler(ctx *gin.Context) { - store := sessions.Default(ctx) - if store == nil { - return - } - defer store.Save() - - if utils.IsLoggedIn(store) { - ctx.Redirect(302, config.Conf.Server.BaseUrl) - } else { - redirect := url.QueryEscape(config.Conf.Oauth.RedirectUri) - - var guildsScope string - if _, noGuilds := ctx.GetQuery("noguilds"); !noGuilds { - guildsScope = "+guilds" - } - - ctx.Redirect(302, fmt.Sprintf("https://discordapp.com/oauth2/authorize?response_type=code&redirect_uri=%s&scope=identify%s&client_id=%d&state=%s", redirect, guildsScope, config.Conf.Oauth.Id, ctx.Query("state"))) - } -} diff --git a/app/http/endpoints/root/logout.go b/app/http/endpoints/root/logout.go index 50d46f9..861798a 100644 --- a/app/http/endpoints/root/logout.go +++ b/app/http/endpoints/root/logout.go @@ -1,18 +1,18 @@ package root import ( - "github.com/gin-gonic/contrib/sessions" + "github.com/TicketsBot/GoPanel/app/http/session" + "github.com/TicketsBot/GoPanel/utils" "github.com/gin-gonic/gin" ) func LogoutHandler(ctx *gin.Context) { - store := sessions.Default(ctx) - if store == nil { + userId := ctx.Keys["userid"].(uint64) + + if err := session.Store.Clear(userId); err != nil { + ctx.JSON(500, utils.ErrorJson(err)) return } - defer store.Save() - store.Clear() - - ctx.Redirect(302, "https://ticketsbot.net") + ctx.Status(204) } diff --git a/app/http/endpoints/root/webchatws.go b/app/http/endpoints/root/webchatws.go new file mode 100644 index 0000000..b3cee82 --- /dev/null +++ b/app/http/endpoints/root/webchatws.go @@ -0,0 +1,193 @@ +package root + +import ( + "encoding/json" + "fmt" + "github.com/TicketsBot/GoPanel/botcontext" + "github.com/TicketsBot/GoPanel/config" + "github.com/TicketsBot/GoPanel/rpc" + "github.com/TicketsBot/GoPanel/utils" + "github.com/TicketsBot/common/permission" + "github.com/TicketsBot/common/premium" + "github.com/dgrijalva/jwt-go" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "net/http" + "strconv" + "sync" + "time" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return r.Header.Get("Origin") == config.Conf.Server.BaseUrl + }, +} + +var SocketsLock sync.RWMutex +var Sockets []*Socket + +type ( + Socket struct { + Ws *websocket.Conn + GuildId uint64 + TicketId int + } + + WsEvent struct { + Type string + Data json.RawMessage + } + + AuthEvent struct { + GuildId uint64 `json:"guild_id,string"` + TicketId int `json:"ticket_id"` + Token string `json:"token"` + } +) + +var timeout = time.Second * 60 + +func WebChatWs(ctx *gin.Context) { + conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) + if err != nil { + fmt.Println(err.Error()) + return + } + + socket := &Socket{ + Ws: conn, + } + + SocketsLock.Lock() + Sockets = append(Sockets, socket) + SocketsLock.Unlock() + + conn.SetCloseHandler(func(code int, text string) error { + i := -1 + SocketsLock.Lock() + defer SocketsLock.Unlock() + + for index, element := range Sockets { + if element == socket { + i = index + break + } + } + + if i != -1 { + Sockets = Sockets[:i+copy(Sockets[i:], Sockets[i+1:])] + } + + return nil + }) + + lastResponse := time.Now() + conn.SetPongHandler(func(a string) error { + lastResponse = time.Now() + return nil + }) + + go func() { + // We can let this func call the CloseHandler + for { + err := conn.WriteMessage(websocket.PingMessage, []byte("keepalive")) + if err != nil { + fmt.Println(err.Error()) + conn.Close() + conn.CloseHandler()(1000, "") + return + } + + time.Sleep(timeout / 2) + if time.Since(lastResponse) > timeout { + conn.Close() + conn.CloseHandler()(1000, "") + return + } + } + }() + + for { + var event WsEvent + err := conn.ReadJSON(&event) + if err != nil { + break + } + + if socket.GuildId == 0 && event.Type != "auth" { + conn.Close() + break + } else if event.Type == "auth" { + var authData AuthEvent + if err := json.Unmarshal(event.Data, &authData); err != nil { + conn.Close() + return + } + + token, err := jwt.Parse(authData.Token, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + return []byte(config.Conf.Server.Secret), nil + }) + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + conn.Close() + return + } + + userIdStr, ok := claims["userid"].(string) + if !ok { + conn.Close() + return + } + + userId, err := strconv.ParseUint(userIdStr, 10, 64) + if err != nil { + conn.Close() + return + } + + // Verify the user has permissions to be here + permLevel, err := utils.GetPermissionLevel(authData.GuildId, userId) + if err != nil { + fmt.Println(err.Error()) + conn.Close() + return + } + + if permLevel < permission.Admin { + fmt.Println(3) + conn.Close() + return + } + + botContext, err := botcontext.ContextForGuild(authData.GuildId) + if err != nil { + ctx.AbortWithStatusJSON(500, gin.H{ + "success": false, + "error": err.Error(), + }) + return + } + + // Verify the guild is premium + premiumTier := rpc.PremiumClient.GetTierByGuildId(authData.GuildId, true, botContext.Token, botContext.RateLimiter) + if premiumTier == premium.None { + fmt.Println(4) + conn.Close() + return + } + + SocketsLock.Lock() + socket.GuildId = authData.GuildId + socket.TicketId = authData.TicketId + SocketsLock.Unlock() + } + } +} diff --git a/app/http/endpoints/root/whitelabel.go b/app/http/endpoints/root/whitelabel.go deleted file mode 100644 index e6b8974..0000000 --- a/app/http/endpoints/root/whitelabel.go +++ /dev/null @@ -1,44 +0,0 @@ -package root - -import ( - "fmt" - "github.com/TicketsBot/GoPanel/config" - "github.com/TicketsBot/GoPanel/rpc" - "github.com/TicketsBot/common/premium" - "github.com/gin-gonic/contrib/sessions" - "github.com/gin-gonic/gin" -) - -func WhitelabelHandler(ctx *gin.Context) { - store := sessions.Default(ctx) - if store == nil { - return - } - defer store.Save() - - userId := store.Get("userid").(uint64) - - premiumTier := rpc.PremiumClient.GetTierByUser(userId, false) - if premiumTier < premium.Whitelabel { - var isForced bool - for _, forced := range config.Conf.ForceWhitelabel { - if forced == userId { - isForced = true - break - } - } - - if !isForced { - ctx.Redirect(302, fmt.Sprintf("%s/premium", config.Conf.Server.MainSite)) - return - } - } - - ctx.HTML(200, "main/whitelabel", gin.H{ - "name": store.Get("name").(string), - "baseurl": config.Conf.Server.BaseUrl, - "avatar": store.Get("avatar").(string), - "referralShow": config.Conf.Referral.Show, - "referralLink": config.Conf.Referral.Link, - }) -} diff --git a/app/http/middleware/authenticatetoken.go b/app/http/middleware/authenticatetoken.go index 8438850..bcb7db3 100644 --- a/app/http/middleware/authenticatetoken.go +++ b/app/http/middleware/authenticatetoken.go @@ -38,6 +38,10 @@ func AuthenticateToken(ctx *gin.Context) { return } + if ctx.Keys == nil { + ctx.Keys = make(map[string]interface{}) + } + ctx.Keys["userid"] = parsedId } else { ctx.AbortWithStatusJSON(401, utils.ErrorStr("Token is invalid")) diff --git a/app/http/middleware/cors.go b/app/http/middleware/cors.go new file mode 100644 index 0000000..98e518c --- /dev/null +++ b/app/http/middleware/cors.go @@ -0,0 +1,25 @@ +package middleware + +import ( + "github.com/TicketsBot/GoPanel/config" + "github.com/gin-gonic/gin" + "net/http" + "strings" +) + +func Cors(config config.Config) func(*gin.Context) { + methods := []string{http.MethodOptions, http.MethodGet, http.MethodPost, http.MethodPatch, http.MethodPut, http.MethodDelete} + headers := []string{"x-tickets", "Content-Type", "Authorization"} + + return func(ctx *gin.Context) { + ctx.Header("Access-Control-Allow-Origin", config.Server.BaseUrl) + ctx.Header("Access-Control-Allow-Methods", strings.Join(methods, ", ")) + ctx.Header("Access-Control-Allow-Headers", strings.Join(headers, ", ")) + ctx.Header("Access-Control-Allow-Credentials", "true") + ctx.Header("Access-Control-Max-Age", "600") + + if ctx.Request.Method == http.MethodOptions { + ctx.AbortWithStatus(http.StatusNoContent) + } + } +} \ No newline at end of file diff --git a/app/http/server.go b/app/http/server.go index d76a788..5e43de3 100644 --- a/app/http/server.go +++ b/app/http/server.go @@ -1,25 +1,23 @@ package http import ( - "fmt" "github.com/TicketsBot/GoPanel/app/http/endpoints/api" api_autoclose "github.com/TicketsBot/GoPanel/app/http/endpoints/api/autoclose" api_blacklist "github.com/TicketsBot/GoPanel/app/http/endpoints/api/blacklist" - api_logs "github.com/TicketsBot/GoPanel/app/http/endpoints/api/logs" api_panels "github.com/TicketsBot/GoPanel/app/http/endpoints/api/panel" api_settings "github.com/TicketsBot/GoPanel/app/http/endpoints/api/settings" api_tags "github.com/TicketsBot/GoPanel/app/http/endpoints/api/tags" api_team "github.com/TicketsBot/GoPanel/app/http/endpoints/api/team" api_ticket "github.com/TicketsBot/GoPanel/app/http/endpoints/api/ticket" + api_transcripts "github.com/TicketsBot/GoPanel/app/http/endpoints/api/transcripts" api_whitelabel "github.com/TicketsBot/GoPanel/app/http/endpoints/api/whitelabel" - "github.com/TicketsBot/GoPanel/app/http/endpoints/manage" "github.com/TicketsBot/GoPanel/app/http/endpoints/root" "github.com/TicketsBot/GoPanel/app/http/middleware" + "github.com/TicketsBot/GoPanel/app/http/session" "github.com/TicketsBot/GoPanel/config" + "github.com/TicketsBot/GoPanel/utils" "github.com/TicketsBot/common/permission" - "github.com/gin-contrib/multitemplate" "github.com/gin-contrib/static" - "github.com/gin-gonic/contrib/sessions" "github.com/gin-gonic/gin" "github.com/ulule/limiter/v3" mgin "github.com/ulule/limiter/v3/drivers/middleware/gin" @@ -34,15 +32,7 @@ func StartServer() { router := gin.Default() // Sessions - store, err := sessions.NewRedisStore( - config.Conf.Server.Session.Threads, - "tcp", fmt.Sprintf("%s:%d", config.Conf.Redis.Host, config.Conf.Redis.Port), - config.Conf.Redis.Password, - []byte(config.Conf.Server.Session.Secret)) - if err != nil { - panic(err) - } - router.Use(sessions.Sessions("panel", store)) + session.Store = session.NewRedisStore() // Handle static asset requests router.Use(static.Serve("/assets/", static.LocalFile("./public/static", false))) @@ -50,39 +40,18 @@ func StartServer() { router.Use(gin.Recovery()) router.Use(createLimiter(600, time.Minute*10)) - // Register templates - router.HTMLRender = createRenderer() + router.Use(middleware.Cors(config.Conf)) - router.GET("/login", root.LoginHandler) - router.GET("/callback", root.CallbackHandler) + router.GET("/webchat", root.WebChatWs) - router.GET("/manage/:id/logs/view/:ticket", manage.LogViewHandler) // we check in the actual handler bc of a custom redirect - - authorized := router.Group("/", middleware.AuthenticateCookie) - { - authorized.POST("/token", createLimiter(2, 10 * time.Second), middleware.VerifyXTicketsHeader, api.TokenHandler) - - authenticateGuildAdmin := authorized.Group("/", middleware.AuthenticateGuild(false, permission.Admin)) - authenticateGuildSupport := authorized.Group("/", middleware.AuthenticateGuild(false, permission.Support)) - - authorized.GET("/", root.IndexHandler) - authorized.GET("/whitelabel", root.WhitelabelHandler) - authorized.GET("/logout", root.LogoutHandler) - - authenticateGuildAdmin.GET("/manage/:id/settings", manage.SettingsHandler) - authenticateGuildSupport.GET("/manage/:id/logs", manage.LogsHandler) - authenticateGuildSupport.GET("/manage/:id/blacklist", manage.BlacklistHandler) - authenticateGuildAdmin.GET("/manage/:id/panels", manage.PanelHandler) - authenticateGuildSupport.GET("/manage/:id/tags", manage.TagsHandler) - authenticateGuildSupport.GET("/manage/:id/teams", serveTemplate("manage/teams")) - - authenticateGuildSupport.GET("/manage/:id/tickets", manage.TicketListHandler) - authenticateGuildSupport.GET("/manage/:id/tickets/view/:ticketId", manage.TicketViewHandler) - - authorized.GET("/webchat", manage.WebChatWs) - } + router.POST("/callback", middleware.VerifyXTicketsHeader, root.CallbackHandler) + router.POST("/logout", middleware.VerifyXTicketsHeader, middleware.AuthenticateToken, root.LogoutHandler) apiGroup := router.Group("/api", middleware.VerifyXTicketsHeader, middleware.AuthenticateToken) + { + apiGroup.GET("/session", api.SessionHandler) + } + guildAuthApiAdmin := apiGroup.Group("/:id", middleware.AuthenticateGuild(true, permission.Admin)) guildAuthApiSupport := apiGroup.Group("/:id", middleware.AuthenticateGuild(true, permission.Support)) { @@ -90,18 +59,18 @@ func StartServer() { guildAuthApiSupport.GET("/premium", api.PremiumHandler) guildAuthApiSupport.GET("/user/:user", api.UserHandler) guildAuthApiSupport.GET("/roles", api.RolesHandler) - guildAuthApiSupport.GET("/members/search", createLimiter(10, time.Second * 30), createLimiter(75, time.Minute * 30), api.SearchMembers) + guildAuthApiSupport.GET("/members/search", createLimiter(5, time.Second), createLimiter(10, time.Second * 30), createLimiter(75, time.Minute * 30), api.SearchMembers) guildAuthApiAdmin.GET("/settings", api_settings.GetSettingsHandler) guildAuthApiAdmin.POST("/settings", api_settings.UpdateSettingsHandler) guildAuthApiSupport.GET("/blacklist", api_blacklist.GetBlacklistHandler) - guildAuthApiSupport.PUT("/blacklist", api_blacklist.AddBlacklistHandler) + guildAuthApiSupport.POST("/blacklist/:user", api_blacklist.AddBlacklistHandler) guildAuthApiSupport.DELETE("/blacklist/:user", api_blacklist.RemoveBlacklistHandler) guildAuthApiAdmin.GET("/panels", api_panels.ListPanels) - guildAuthApiAdmin.PUT("/panels", api_panels.CreatePanel) - guildAuthApiAdmin.PUT("/panels/:panelid", api_panels.UpdatePanel) + guildAuthApiAdmin.POST("/panels", api_panels.CreatePanel) + guildAuthApiAdmin.PATCH("/panels/:panelid", api_panels.UpdatePanel) guildAuthApiAdmin.DELETE("/panels/:panelid", api_panels.DeletePanel) guildAuthApiAdmin.GET("/multipanels", api_panels.MultiPanelList) @@ -109,7 +78,8 @@ func StartServer() { guildAuthApiAdmin.PATCH("/multipanels/:panelid", api_panels.MultiPanelUpdate) guildAuthApiAdmin.DELETE("/multipanels/:panelid", api_panels.MultiPanelDelete) - guildAuthApiSupport.GET("/logs/", api_logs.GetLogs) + guildAuthApiSupport.GET("/transcripts", createLimiter(5, 5 * time.Second), createLimiter(20, time.Minute), api_transcripts.ListTranscripts) + guildAuthApiSupport.GET("/transcripts/:ticketId", createLimiter(10, 10 * time.Second), api_transcripts.GetTranscriptHandler) guildAuthApiSupport.GET("/tickets", api_ticket.GetTickets) guildAuthApiSupport.GET("/tickets/:ticketId", api_ticket.GetTicket) @@ -127,7 +97,7 @@ func StartServer() { guildAuthApiAdmin.POST("/autoclose", api_autoclose.PostAutoClose) guildAuthApiAdmin.GET("/team", api_team.GetTeams) - guildAuthApiAdmin.GET("/team/:teamid", createLimiter(5, time.Second * 15), api_team.GetMembers) + guildAuthApiAdmin.GET("/team/:teamid", createLimiter(10, time.Second * 30), api_team.GetMembers) guildAuthApiAdmin.POST("/team", createLimiter(10, time.Minute), api_team.CreateTeam) guildAuthApiAdmin.PUT("/team/:teamid/:snowflake", createLimiter(5, time.Second * 10), api_team.AddMember) guildAuthApiAdmin.DELETE("/team/:teamid", api_team.DeleteTeam) @@ -141,18 +111,17 @@ func StartServer() { userGroup.GET("/permissionlevel", api.GetPermissionLevel) { - whitelabelGroup := userGroup.Group("/whitelabel", middleware.VerifyWhitelabel(false)) - whitelabelApiGroup := userGroup.Group("/whitelabel", middleware.VerifyWhitelabel(true)) + whitelabelGroup := userGroup.Group("/whitelabel", middleware.VerifyWhitelabel(true)) whitelabelGroup.GET("/", api_whitelabel.WhitelabelGet) - whitelabelApiGroup.GET("/errors", api_whitelabel.WhitelabelGetErrors) - whitelabelApiGroup.GET("/guilds", api_whitelabel.WhitelabelGetGuilds) - whitelabelApiGroup.GET("/public-key", api_whitelabel.WhitelabelGetPublicKey) - whitelabelApiGroup.POST("/public-key", api_whitelabel.WhitelabelPostPublicKey) - whitelabelApiGroup.POST("/create-interactions", api_whitelabel.GetWhitelabelCreateInteractions()) + whitelabelGroup.GET("/errors", api_whitelabel.WhitelabelGetErrors) + whitelabelGroup.GET("/guilds", api_whitelabel.WhitelabelGetGuilds) + whitelabelGroup.GET("/public-key", api_whitelabel.WhitelabelGetPublicKey) + whitelabelGroup.POST("/public-key", api_whitelabel.WhitelabelPostPublicKey) + whitelabelGroup.POST("/create-interactions", api_whitelabel.GetWhitelabelCreateInteractions()) - whitelabelApiGroup.POST("/", createLimiter(10, time.Minute), api_whitelabel.WhitelabelPost) - whitelabelApiGroup.POST("/status", createLimiter(1, time.Second*5), api_whitelabel.WhitelabelStatusPost) + whitelabelGroup.POST("/", createLimiter(10, time.Minute), api_whitelabel.WhitelabelPost) + whitelabelGroup.POST("/status", createLimiter(1, time.Second*5), api_whitelabel.WhitelabelStatusPost) } } @@ -163,82 +132,32 @@ func StartServer() { func serveTemplate(templateName string) func(*gin.Context) { return func(ctx *gin.Context) { - store := sessions.Default(ctx) guildId := ctx.Keys["guildid"].(uint64) + userId := ctx.Keys["userid"].(uint64) + + store, err := session.Store.Get(userId) + if err != nil { + if err == session.ErrNoSession { + ctx.JSON(401, gin.H{ + "success": false, + "auth": true, + }) + } else { + ctx.JSON(500, utils.ErrorJson(err)) + } + + return + } ctx.HTML(200, templateName, gin.H{ - "name": store.Get("name").(string), + "name": store.Name, "guildId": guildId, - "avatar": store.Get("avatar").(string), + "avatar": store.Avatar, "baseUrl": config.Conf.Server.BaseUrl, }) } } -func createRenderer() multitemplate.Renderer { - r := multitemplate.NewRenderer() - - r = addMainTemplate(r, "index") - r = addMainTemplate(r, "whitelabel") - - r = addManageTemplate(r, "blacklist") - r = addManageTemplate(r, "logs") - r = addManageTemplate(r, "modmaillogs") - r = addManageTemplate(r, "settings", "./public/templates/includes/substitutionmodal.tmpl") - r = addManageTemplate(r, "ticketlist") - r = addManageTemplate(r, "ticketview") - r = addManageTemplate(r, "panels", "./public/templates/includes/substitutionmodal.tmpl", "./public/templates/includes/paneleditmodal.tmpl", "./public/templates/includes/multipaneleditmodal.tmpl") - r = addManageTemplate(r, "tags") - r = addManageTemplate(r, "teams") - - r = addErrorTemplate(r) - - return r -} - -func addMainTemplate(renderer multitemplate.Renderer, name string, extra ...string) multitemplate.Renderer { - files := []string{ - "./public/templates/layouts/main.tmpl", - "./public/templates/includes/head.tmpl", - "./public/templates/includes/sidebar.tmpl", - "./public/templates/includes/loadingscreen.tmpl", - "./public/templates/includes/notifymodal.tmpl", - fmt.Sprintf("./public/templates/views/%s.tmpl", name), - } - - files = append(files, extra...) - - renderer.AddFromFiles(fmt.Sprintf("main/%s", name), files...) - return renderer -} - -func addManageTemplate(renderer multitemplate.Renderer, name string, extra ...string) multitemplate.Renderer { - files := []string{ - "./public/templates/layouts/manage.tmpl", - "./public/templates/includes/head.tmpl", - "./public/templates/includes/sidebar.tmpl", - "./public/templates/includes/navbar.tmpl", - "./public/templates/includes/loadingscreen.tmpl", - "./public/templates/includes/notifymodal.tmpl", - fmt.Sprintf("./public/templates/views/%s.tmpl", name), - } - - files = append(files, extra...) - - renderer.AddFromFiles(fmt.Sprintf("manage/%s", name), files...) - return renderer -} - -func addErrorTemplate(renderer multitemplate.Renderer) multitemplate.Renderer { - files := []string{ - "./public/templates/layouts/error.tmpl", - "./public/templates/includes/head.tmpl", - } - - renderer.AddFromFiles("error", files...) - return renderer -} - func createLimiter(limit int64, period time.Duration) func(*gin.Context) { store := memory.NewStore() rate := limiter.Rate{ diff --git a/app/http/session/redisstore.go b/app/http/session/redisstore.go new file mode 100644 index 0000000..ecca6d1 --- /dev/null +++ b/app/http/session/redisstore.go @@ -0,0 +1,54 @@ +package session + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/TicketsBot/GoPanel/messagequeue" + "github.com/go-redis/redis" +) + +var ErrNoSession = errors.New("no session data found") + +type RedisStore struct { + client *redis.Client +} + +func NewRedisStore() *RedisStore { + return &RedisStore{ + client: messagequeue.Client.Client, + } +} + +var keyPrefix = "panel:session:" + +func (s *RedisStore) Get(userId uint64) (SessionData, error) { + raw, err := s.client.Get(fmt.Sprintf("%s:%d", keyPrefix, userId)).Result() + if err != nil { + if err == redis.Nil { + err = ErrNoSession + } + + return SessionData{}, err + } + + var data SessionData + if err := json.Unmarshal([]byte(raw), &data); err != nil { + return SessionData{}, err + } + + return data, nil +} + +func (s *RedisStore) Set(userId uint64, data SessionData) error { + encoded, err := json.Marshal(data) + if err != nil { + return err + } + + return s.client.Set(fmt.Sprintf("%s:%d", keyPrefix, userId), encoded, 0).Err() +} + +func (s *RedisStore) Clear(userId uint64) error { + return s.client.Del(fmt.Sprintf("%s:%d", keyPrefix, userId)).Err() +} \ No newline at end of file diff --git a/app/http/session/sessiondata.go b/app/http/session/sessiondata.go new file mode 100644 index 0000000..5b074ff --- /dev/null +++ b/app/http/session/sessiondata.go @@ -0,0 +1,10 @@ +package session + +type SessionData struct { + AccessToken string `json:"access_token"` + Expiry int64 `json:"expiry"` + RefreshToken string `json:"refresh_token"` + Name string `json:"name"` + Avatar string `json:"avatar_hash"` + HasGuilds bool `json:"has_guilds"` +} diff --git a/app/http/session/store.go b/app/http/session/store.go new file mode 100644 index 0000000..71a4862 --- /dev/null +++ b/app/http/session/store.go @@ -0,0 +1,9 @@ +package session + +type SessionStore interface { + Get(userId uint64) (SessionData, error) + Set(userId uint64, data SessionData) error + Clear(userId uint64) error +} + +var Store SessionStore \ No newline at end of file diff --git a/botcontext/botcontext.go b/botcontext/botcontext.go index 6745e45..b5d86c0 100644 --- a/botcontext/botcontext.go +++ b/botcontext/botcontext.go @@ -112,3 +112,17 @@ func (ctx BotContext) SearchMembers(guildId uint64, query string) (members []mem return } + + +func (ctx BotContext) ListMembers(guildId uint64) (members []member.Member, err error) { + data := rest.ListGuildMembersData{ + Limit: 100, + } + + members, err = rest.ListGuildMembers(ctx.Token, ctx.RateLimiter, guildId, data) + if err == nil { + go cache.Instance.StoreMembers(members, guildId) + } + + return +} diff --git a/cmd/panel/main.go b/cmd/panel/main.go index 681a5c7..1cbe81d 100644 --- a/cmd/panel/main.go +++ b/cmd/panel/main.go @@ -5,7 +5,7 @@ import ( "encoding/binary" "fmt" "github.com/TicketsBot/GoPanel/app/http" - "github.com/TicketsBot/GoPanel/app/http/endpoints/manage" + "github.com/TicketsBot/GoPanel/app/http/endpoints/root" "github.com/TicketsBot/GoPanel/config" "github.com/TicketsBot/GoPanel/database" "github.com/TicketsBot/GoPanel/messagequeue" @@ -13,6 +13,7 @@ import ( "github.com/TicketsBot/GoPanel/rpc/cache" "github.com/TicketsBot/GoPanel/utils" "github.com/TicketsBot/archiverclient" + "github.com/TicketsBot/common/chatrelay" "github.com/TicketsBot/common/premium" "github.com/TicketsBot/worker/bot/i18n" "github.com/apex/log" @@ -36,7 +37,7 @@ func main() { database.ConnectToDatabase() cache.Instance = cache.NewCache() - manage.Archiver = archiverclient.NewArchiverClientWithTimeout(config.Conf.Bot.ObjectStore, time.Second*15, []byte(config.Conf.Bot.AesKey)) + utils.ArchiverClient = archiverclient.NewArchiverClientWithTimeout(config.Conf.Bot.ObjectStore, time.Second*15, []byte(config.Conf.Bot.AesKey)) utils.LoadEmoji() if err := i18n.LoadMessages(database.Client); err != nil { @@ -48,7 +49,7 @@ func main() { } messagequeue.Client = messagequeue.NewRedisClient() - go Listen(messagequeue.Client) + go ListenChat(messagequeue.Client) rpc.PremiumClient = premium.NewPremiumLookupClient( premium.NewPatreonClient(config.Conf.Bot.PremiumLookupProxyUrl, config.Conf.Bot.PremiumLookupProxyKey), @@ -60,19 +61,19 @@ func main() { http.StartServer() } -func Listen(client messagequeue.RedisClient) { - ch := make(chan messagequeue.TicketMessage) - go client.ListenForMessages(ch) +func ListenChat(client messagequeue.RedisClient) { + ch := make(chan chatrelay.MessageData) + go chatrelay.Listen(client.Client, ch) - for decoded := range ch { - manage.SocketsLock.Lock() - for _, socket := range manage.Sockets { - if socket.Guild == decoded.GuildId && socket.Ticket == decoded.TicketId { - if err := socket.Ws.WriteJSON(decoded); err != nil { + for event := range ch { + root.SocketsLock.RLock() + for _, socket := range root.Sockets { + if socket.GuildId == event.Ticket.GuildId && socket.TicketId == event.Ticket.Id { + if err := socket.Ws.WriteJSON(event.Message); err != nil { fmt.Println(err.Error()) } } } - manage.SocketsLock.Unlock() + root.SocketsLock.RUnlock() } } diff --git a/config/config.go b/config/config.go index faf3554..f6f99f5 100644 --- a/config/config.go +++ b/config/config.go @@ -18,7 +18,6 @@ type ( Bot Bot Redis Redis Cache Cache - Referral Referral } Server struct { @@ -119,7 +118,6 @@ func fromEnvvar() { oauthId, _ := strconv.ParseUint(os.Getenv("OAUTH_ID"), 10, 64) redisPort, _ := strconv.Atoi(os.Getenv("REDIS_PORT")) redisThreads, _ := strconv.Atoi(os.Getenv("REDIS_THREADS")) - showReferral, _ := strconv.ParseBool(os.Getenv("REFERRAL_SHOW")) Conf = Config{ Admins: admins, @@ -163,9 +161,5 @@ func fromEnvvar() { Cache: Cache{ Uri: os.Getenv("CACHE_URI"), }, - Referral: Referral{ - Show: showReferral, - Link: os.Getenv("REFERRAL_LINK"), - }, } } diff --git a/envvars.md b/envvars.md index d77a239..15fdcaf 100644 --- a/envvars.md +++ b/envvars.md @@ -1,3 +1,13 @@ +# Build +--- +- CLIENT_ID +- REDIRECT_URI +- API_URL +- WS_URL + +# Runtime +--- + - ADMINS - FORCED_WHITELABEL - SERVER_ADDR @@ -22,5 +32,3 @@ - REDIS_PASSWORD - REDIS_THREADS - CACHE_URI -- REFERRAL_SHOW -- REFERRAL_LINK \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..b39a19d --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,4 @@ +/node_modules/ +/public/build + +.DS_Store diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..7a43b91 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,876 @@ +{ + "name": "svelte-app", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@fortawesome/fontawesome-common-types": { + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.35.tgz", + "integrity": "sha512-IHUfxSEDS9dDGqYwIW7wTN6tn/O8E0n5PcAHz9cAaBoZw6UpG20IG/YM3NNLaGPwPqgjBAFjIURzqoQs3rrtuw==" + }, + "@fortawesome/free-regular-svg-icons": { + "version": "5.15.3", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.3.tgz", + "integrity": "sha512-q4/p8Xehy9qiVTdDWHL4Z+o5PCLRChePGZRTXkl+/Z7erDVL8VcZUuqzJjs6gUz6czss4VIPBRdCz6wP37/zMQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.35" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "5.15.3", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.3.tgz", + "integrity": "sha512-XPeeu1IlGYqz4VWGRAT5ukNMd4VHUEEJ7ysZ7pSSgaEtNvSo+FLurybGJVmiqkQdK50OkSja2bfZXOeyMGRD8Q==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.35" + } + }, + "@polka/url": { + "version": "1.0.0-next.15", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.15.tgz", + "integrity": "sha512-15spi3V28QdevleWBNXE4pIls3nFZmBbUGrW9IVPwiQczuSb9n76TCB4bsk8TSel+I1OkHEdPhu5QKMfY6rQHA==" + }, + "@rollup/plugin-commonjs": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz", + "integrity": "sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "commondir": "^1.0.1", + "estree-walker": "^2.0.1", + "glob": "^7.1.6", + "is-reference": "^1.2.1", + "magic-string": "^0.25.7", + "resolve": "^1.17.0" + } + }, + "@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + } + }, + "@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "dependencies": { + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + } + } + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/node": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.6.1.tgz", + "integrity": "sha512-7EIraBEyRHEe7CH+Fm1XvgqU6uwZN8Q7jppJGcqjROMT29qhAuuOxYB1uEY5UMYQKEmA5D+5tBnhdaPXSsLONA==", + "dev": true + }, + "@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.3.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "console-clear": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/console-clear/-/console-clear-1.1.1.tgz", + "integrity": "sha512-pMD+MVR538ipqkG5JXeOEbKWS5um1H4LUUccUQG68qpeqBYbzYy79Gh55jkd2TtPdRfUaLWdv6LPP//5Zt0aPQ==" + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "fa-svelte": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fa-svelte/-/fa-svelte-3.1.0.tgz", + "integrity": "sha512-RqBOWwt7sc+ta9GFjbu5GOwKFRzn3rMPPSqvSGpIwsfVnpMjiI5ttv84lwNsCMEYI6/lu/iH21HUcE3TLz8RGQ==" + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "follow-redirects": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "requires": { + "@types/estree": "*" + } + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" + }, + "livereload": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz", + "integrity": "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==", + "dev": true, + "requires": { + "chokidar": "^3.5.0", + "livereload-js": "^3.3.1", + "opts": ">= 1.2.0", + "ws": "^7.4.3" + } + }, + "livereload-js": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.3.2.tgz", + "integrity": "sha512-w677WnINxFkuixAoUEXOStewzLYGI76XVag+0JWMMEyjJQKs0ibWZMxkTlB96Lm3EjZ7IeOxVziBEbtxVQqQZA==", + "dev": true + }, + "local-access": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/local-access/-/local-access-1.1.0.tgz", + "integrity": "sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw==" + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mri": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.6.tgz", + "integrity": "sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "opts": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/opts/-/opts-2.0.2.tgz", + "integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + }, + "popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=", + "dev": true + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "rollup": { + "version": "2.50.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.50.4.tgz", + "integrity": "sha512-mBQa9O6bdqur7a6R+TXcbdYgfO2arXlDG+rSrWfwAvsiumpJjD4OS23R9QuhItuz8ysWb8mZ91CFFDQUhJY+8Q==", + "dev": true, + "requires": { + "fsevents": "~2.3.1" + } + }, + "rollup-plugin-css-only": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-3.1.0.tgz", + "integrity": "sha512-TYMOE5uoD76vpj+RTkQLzC9cQtbnJNktHPB507FzRWBVaofg7KhIqq1kGbcVOadARSozWF883Ho9KpSPKH8gqA==", + "dev": true, + "requires": { + "@rollup/pluginutils": "4" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.0.tgz", + "integrity": "sha512-TrBhfJkFxA+ER+ew2U2/fHbebhLT/l/2pRk0hfj9KusXUuRXd2v0R58AfaZK9VXDQ4TogOSEmICVrQAA3zFnHQ==", + "dev": true, + "requires": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + } + } + } + }, + "rollup-plugin-livereload": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-livereload/-/rollup-plugin-livereload-2.0.0.tgz", + "integrity": "sha512-oC/8NqumGYuphkqrfszOHUUIwzKsaHBICw6QRwT5uD07gvePTS+HW+GFwu6f9K8W02CUuTvtIM9AWJrbj4wE1A==", + "dev": true, + "requires": { + "livereload": "^0.9.1" + } + }, + "rollup-plugin-svelte": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.0.tgz", + "integrity": "sha512-vopCUq3G+25sKjwF5VilIbiY6KCuMNHP1PFvx2Vr3REBNMDllKHFZN2B9jwwC+MqNc3UPKkjXnceLPEjTjXGXg==", + "dev": true, + "requires": { + "require-relative": "^0.8.7", + "rollup-pluginutils": "^2.8.2" + } + }, + "rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + } + }, + "rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1" + }, + "dependencies": { + "estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + } + } + }, + "sade": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.7.4.tgz", + "integrity": "sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==", + "requires": { + "mri": "^1.1.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "semiver": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz", + "integrity": "sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==" + }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "sirv": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.12.tgz", + "integrity": "sha512-+jQoCxndz7L2tqQL4ZyzfDhky0W/4ZJip3XoOuxyQWnAwMxindLl3Xv1qT4x1YX/re0leShvTm8Uk0kQspGhBg==", + "requires": { + "@polka/url": "^1.0.0-next.15", + "mime": "^2.3.1", + "totalist": "^1.0.0" + } + }, + "sirv-cli": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/sirv-cli/-/sirv-cli-1.0.12.tgz", + "integrity": "sha512-Rs5PvF3a48zuLmrl8vcqVv9xF/WWPES19QawVkpdzqx7vD5SMZS07+ece1gK4umbslXN43YeIksYtQM5csgIzQ==", + "requires": { + "console-clear": "^1.1.0", + "get-port": "^3.2.0", + "kleur": "^3.0.0", + "local-access": "^1.0.1", + "sade": "^1.6.0", + "semiver": "^1.0.0", + "sirv": "^1.0.12", + "tinydate": "^1.0.0" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "svelte": { + "version": "3.38.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.38.2.tgz", + "integrity": "sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg==", + "dev": true + }, + "svelte-click-outside": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svelte-click-outside/-/svelte-click-outside-1.0.0.tgz", + "integrity": "sha512-TVDn5Vd8L0WI0Y9BFh/2I7judkIqYCbFKkGwGl/f8D0inwBFNyU0weKhrbJY4VQtYnWriq0NPl+mIYGisgALbw==" + }, + "svelte-emoji-selector": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/svelte-emoji-selector/-/svelte-emoji-selector-1.0.1.tgz", + "integrity": "sha512-gGjDydt+79YQIdUyz/r1sHSkjLko2rb9qHNiBveC5RSl6rJ0mob4T5DrADRArjQ/HA8kNfEJFyqbnLoA+dyLqA==", + "requires": { + "@fortawesome/free-regular-svg-icons": "^5.10.1", + "@fortawesome/free-solid-svg-icons": "^5.10.1", + "fa-svelte": "^3.0.0", + "popper.js": "^1.15.0", + "svelte-click-outside": "^1.0.0", + "svelte-tabs": "^1.1.0" + } + }, + "svelte-router-spa": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/svelte-router-spa/-/svelte-router-spa-6.0.2.tgz", + "integrity": "sha512-ySs/2TnjdLnvo0tHfdJsRPhPl0Mj4/h2qi0Zb8t4zC+BBBaCr6cZc7MtRfgzD4IMp80Nqe7ZXd/hCJuHSGtf5A==", + "requires": { + "url-params-parser": "^1.0.3" + } + }, + "svelte-select": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/svelte-select/-/svelte-select-3.17.0.tgz", + "integrity": "sha512-ITmX/XUiSdkaILmsTviKRkZPaXckM5/FA7Y8BhiUPoamaZG/ZDyOo6ydjFu9fDVFTbwoAUGUi6HBjs+ZdK2AwA==" + }, + "svelte-tabs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/svelte-tabs/-/svelte-tabs-1.1.0.tgz", + "integrity": "sha512-bCynxgET2uvqpB6xf/dVyqHjzmumRURQyh2QqXlrki8NxzO7h2WghF8qgpb5qeB5NTX1bMU+9Q5Hf5ey2WLaMg==" + }, + "terser": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.0.tgz", + "integrity": "sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.19" + } + }, + "tinydate": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinydate/-/tinydate-1.3.0.tgz", + "integrity": "sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==" + }, + "url-params-parser": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/url-params-parser/-/url-params-parser-1.0.4.tgz", + "integrity": "sha512-0m6BqGpY2OetTZ3UPTLKkbTfUHigsX2YhrzORT9iYiyUJ/SP2WJ3cggg2YWtvMs36GPwK9Q44ffddyarniu2Tg==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "dev": true + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..6447598 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "svelte-app", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "rollup -c", + "dev": "rollup -c -w", + "start": "sirv public -s --no-clear --host 0.0.0.0" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^17.0.0", + "@rollup/plugin-node-resolve": "^11.0.0", + "@rollup/plugin-replace": "^2.4.2", + "rollup": "^2.3.4", + "rollup-plugin-css-only": "^3.1.0", + "rollup-plugin-livereload": "^2.0.0", + "rollup-plugin-svelte": "^7.0.0", + "rollup-plugin-terser": "^7.0.0", + "svelte": "^3.0.0" + }, + "dependencies": { + "axios": "^0.21.1", + "sirv-cli": "^1.0.0", + "svelte-emoji-selector": "^1.0.1", + "svelte-router-spa": "^6.0.2", + "svelte-select": "^3.17.0" + } +} diff --git a/public/static/img/custom.png b/frontend/public/assets/img/custom.png similarity index 100% rename from public/static/img/custom.png rename to frontend/public/assets/img/custom.png diff --git a/public/static/img/favicon.ico b/frontend/public/assets/img/favicon.ico similarity index 100% rename from public/static/img/favicon.ico rename to frontend/public/assets/img/favicon.ico diff --git a/public/static/img/loading-bubbles.svg b/frontend/public/assets/img/loading-bubbles.svg similarity index 100% rename from public/static/img/loading-bubbles.svg rename to frontend/public/assets/img/loading-bubbles.svg diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..841fcb2 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/global.css b/frontend/public/global.css new file mode 100644 index 0000000..395ad58 --- /dev/null +++ b/frontend/public/global.css @@ -0,0 +1,64 @@ +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans&display=swap'); + +html, body { + position: relative; + width: 100%; + height: 100%; +} + +body, h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6, p, .navbar, .brand, .btn-simple, .alert, a, .td-name, td, button.close { + font-family: 'Noto Sans', sans-serif !important; + font-weight: 400 !important; +} + +h1, h2, h3, h4, h5, h6, p, span { + margin: 0; +} + +body { + line-height: 1.5; + font-size: 1rem; + font-weight: 400; + background-color: #121212 !important; + color: white; + margin: 0; + padding: 0 !important; + box-sizing: border-box; + /*font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;*/ +} + +label { + display: block; +} + +input, button, select, textarea { + font-family: inherit; + font-size: inherit; + -webkit-padding: 0.4em 0; + padding: 0.4em; + box-sizing: border-box; + border: 1px solid #ccc; + border-radius: 2px; +} + +input:disabled { + color: #ccc; +} + +button { + color: #333; + background-color: #f4f4f4; + outline: none; +} + +button:disabled { + color: #999; +} + +button:not(:disabled):active { + background-color: #ddd; +} + +button:focus { + border-color: #666; +} diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..8239834 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,18 @@ + + + + + + + Tickets Dashboard + + + + + + + + + + + diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js new file mode 100644 index 0000000..4f11d02 --- /dev/null +++ b/frontend/rollup.config.js @@ -0,0 +1,86 @@ +import svelte from 'rollup-plugin-svelte'; +import commonjs from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; +import livereload from 'rollup-plugin-livereload'; +import {terser} from 'rollup-plugin-terser'; +import css from 'rollup-plugin-css-only'; +import replace from "@rollup/plugin-replace"; + +const production = !process.env.ROLLUP_WATCH; + +function serve() { + let server; + + function toExit() { + if (server) server.kill(0); + } + + return { + writeBundle() { + if (server) return; + server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { + stdio: ['ignore', 'inherit', 'inherit'], + shell: true + }); + + process.on('SIGTERM', toExit); + process.on('exit', toExit); + } + }; +} + +export default { + input: 'src/main.js', + output: { + sourcemap: true, + format: 'iife', + name: 'app', + file: 'public/build/bundle.js' + }, + plugins: [ + svelte({ + compilerOptions: { + // enable run-time checks when not in production + dev: !production + } + }), + // we'll extract any component CSS out into + // a separate file - better for performance + css({output: 'bundle.css'}), + + // If you have external dependencies installed from + // npm, you'll most likely need these plugins. In + // some cases you'll need additional configuration - + // consult the documentation for details: + // https://github.com/rollup/plugins/tree/master/packages/commonjs + resolve({ + browser: true, + dedupe: ['svelte'] + }), + commonjs(), + + replace({ + env: JSON.stringify({ + CLIENT_ID: process.env.CLIENT_ID, + REDIRECT_URI: process.env.REDIRECT_URI, + API_URL: process.env.API_URL, + WS_URL: process.env.WS_URL, + }) + }), + + // In dev mode, call `npm run start` once + // the bundle has been generated + !production && serve(), + + // Watch the `public` directory and refresh the + // browser on changes when not in production + !production && livereload('public'), + + // If we're building for production (npm run build + // instead of npm run dev), minify + production && terser() + ], + watch: { + clearScreen: false + } +}; diff --git a/frontend/scripts/setupTypeScript.js b/frontend/scripts/setupTypeScript.js new file mode 100644 index 0000000..1eb6b77 --- /dev/null +++ b/frontend/scripts/setupTypeScript.js @@ -0,0 +1,117 @@ +// @ts-check + +/** This script modifies the project to support TS code in .svelte files like: + + + + As well as validating the code for CI. + */ + +/** To work on this script: + rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template +*/ + +const fs = require("fs") +const path = require("path") +const { argv } = require("process") + +const projectRoot = argv[2] || path.join(__dirname, "..") + +// Add deps to pkg.json +const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8")) +packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, { + "svelte-check": "^1.0.0", + "svelte-preprocess": "^4.0.0", + "@rollup/plugin-typescript": "^8.0.0", + "typescript": "^4.0.0", + "tslib": "^2.0.0", + "@tsconfig/svelte": "^1.0.0" +}) + +// Add script for checking +packageJSON.scripts = Object.assign(packageJSON.scripts, { + "validate": "svelte-check" +}) + +// Write the package JSON +fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " ")) + +// mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too +const beforeMainJSPath = path.join(projectRoot, "src", "main.js") +const afterMainTSPath = path.join(projectRoot, "src", "main.ts") +fs.renameSync(beforeMainJSPath, afterMainTSPath) + +// Switch the app.svelte file to use TS +const appSveltePath = path.join(projectRoot, "src", "App.svelte") +let appFile = fs.readFileSync(appSveltePath, "utf8") +appFile = appFile.replace(" diff --git a/frontend/src/components/Button.svelte b/frontend/src/components/Button.svelte new file mode 100644 index 0000000..37bb5ae --- /dev/null +++ b/frontend/src/components/Button.svelte @@ -0,0 +1,68 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/Card.svelte b/frontend/src/components/Card.svelte new file mode 100644 index 0000000..b31ccbb --- /dev/null +++ b/frontend/src/components/Card.svelte @@ -0,0 +1,137 @@ + + +
+
dropdownActive = dropdown && !dropdownActive}> +

+ + No Title :( + +

+
+
+
+ + No Content :( + +
+
+ + {#if footer} + + {/if} +
+ + \ No newline at end of file diff --git a/frontend/src/components/CategoryDropdown.svelte b/frontend/src/components/CategoryDropdown.svelte new file mode 100644 index 0000000..a81aaa9 --- /dev/null +++ b/frontend/src/components/CategoryDropdown.svelte @@ -0,0 +1,22 @@ + + {#each channels as channel} + {#if channel.type === 4} + + {/if} + {/each} + + + \ No newline at end of file diff --git a/frontend/src/components/ChannelDropdown.svelte b/frontend/src/components/ChannelDropdown.svelte new file mode 100644 index 0000000..f74dafe --- /dev/null +++ b/frontend/src/components/ChannelDropdown.svelte @@ -0,0 +1,22 @@ + + {#each channels as channel} + {#if channel.type === 0} + + {/if} + {/each} + + + \ No newline at end of file diff --git a/frontend/src/components/DiscordMessages.svelte b/frontend/src/components/DiscordMessages.svelte new file mode 100644 index 0000000..ed525eb --- /dev/null +++ b/frontend/src/components/DiscordMessages.svelte @@ -0,0 +1,108 @@ +
+
+ #ticket-{ticketId} +
+
+ {#each messages as message} +
+ {message.author.username}: {message.content} +
+ {/each} +
+
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/frontend/src/components/Guild.svelte b/frontend/src/components/Guild.svelte new file mode 100644 index 0000000..5b08bbd --- /dev/null +++ b/frontend/src/components/Guild.svelte @@ -0,0 +1,108 @@ +
+
+ {#if guild.icon === undefined || guild.icon === ""} + + {:else} + Guild Icon + {/if} +
+ +
+ + {guild.name} + +
+
+ + + + diff --git a/frontend/src/components/InviteBadge.svelte b/frontend/src/components/InviteBadge.svelte new file mode 100644 index 0000000..92e6c43 --- /dev/null +++ b/frontend/src/components/InviteBadge.svelte @@ -0,0 +1,15 @@ +
+
+ +
+ +
+ Invite to your server +
+
+ + diff --git a/frontend/src/components/NamingScheme.svelte b/frontend/src/components/NamingScheme.svelte new file mode 100644 index 0000000..de2afb9 --- /dev/null +++ b/frontend/src/components/NamingScheme.svelte @@ -0,0 +1,17 @@ + +
+ + +
+ +
+ + +
+
+ + \ No newline at end of file diff --git a/frontend/src/components/NavElement.svelte b/frontend/src/components/NavElement.svelte new file mode 100644 index 0000000..7b06d1f --- /dev/null +++ b/frontend/src/components/NavElement.svelte @@ -0,0 +1,41 @@ + + + + + diff --git a/frontend/src/components/PanelDropdown.svelte b/frontend/src/components/PanelDropdown.svelte new file mode 100644 index 0000000..4d0a355 --- /dev/null +++ b/frontend/src/components/PanelDropdown.svelte @@ -0,0 +1,28 @@ + +
+ +
+ + + + diff --git a/frontend/src/components/form/Colour.svelte b/frontend/src/components/form/Colour.svelte new file mode 100644 index 0000000..95045f0 --- /dev/null +++ b/frontend/src/components/form/Colour.svelte @@ -0,0 +1,20 @@ +
+ + +
+ + + + diff --git a/frontend/src/components/form/Dropdown.svelte b/frontend/src/components/form/Dropdown.svelte new file mode 100644 index 0000000..272749c --- /dev/null +++ b/frontend/src/components/form/Dropdown.svelte @@ -0,0 +1,22 @@ +
+ + +
+ + + + diff --git a/frontend/src/components/form/EmojiInput.svelte b/frontend/src/components/form/EmojiInput.svelte new file mode 100644 index 0000000..376bf7f --- /dev/null +++ b/frontend/src/components/form/EmojiInput.svelte @@ -0,0 +1,54 @@ +
+ +
+ + {#if !disabled} + + {/if} +
+
+ + + + \ No newline at end of file diff --git a/frontend/src/components/form/Input.svelte b/frontend/src/components/form/Input.svelte new file mode 100644 index 0000000..7ad5180 --- /dev/null +++ b/frontend/src/components/form/Input.svelte @@ -0,0 +1,25 @@ +
+ {#if label !== undefined} + + {/if} + +
+ + + + \ No newline at end of file diff --git a/frontend/src/components/form/MultiSelect.svelte b/frontend/src/components/form/MultiSelect.svelte new file mode 100644 index 0000000..cd5d782 --- /dev/null +++ b/frontend/src/components/form/MultiSelect.svelte @@ -0,0 +1,285 @@ + + + + + + +
+
+ {selected} + {#each selected as value} + value +
+ {values[selected]} +
+ + + +
+
+ {/each} +
+ + + + +
+
+ + + + {#if showOptions} + + {/if} +
\ No newline at end of file diff --git a/frontend/src/components/form/Number.svelte b/frontend/src/components/form/Number.svelte new file mode 100644 index 0000000..9366040 --- /dev/null +++ b/frontend/src/components/form/Number.svelte @@ -0,0 +1,32 @@ +
+ + +
+ + diff --git a/frontend/src/components/form/Radio.svelte b/frontend/src/components/form/Radio.svelte new file mode 100644 index 0000000..081b37a --- /dev/null +++ b/frontend/src/components/form/Radio.svelte @@ -0,0 +1,38 @@ +
+ + +
+ + + + diff --git a/frontend/src/components/form/RoleSelect.svelte b/frontend/src/components/form/RoleSelect.svelte new file mode 100644 index 0000000..a7b8399 --- /dev/null +++ b/frontend/src/components/form/RoleSelect.svelte @@ -0,0 +1,31 @@ +{#if label !== undefined} + +{/if} + +
+ +
+ + + + \ No newline at end of file diff --git a/frontend/src/components/form/UserSelect.svelte b/frontend/src/components/form/UserSelect.svelte new file mode 100644 index 0000000..58cc236 --- /dev/null +++ b/frontend/src/components/form/UserSelect.svelte @@ -0,0 +1,45 @@ +{#if label !== undefined} + +{/if} + +
+ +
+
+ -
- - -
-
- -
- -
-
- -
- -
-
-
#
-
- -
-
-
-
- - -
-
-
- - - - - - - - - -{{end}} \ No newline at end of file diff --git a/public/templates/includes/navbar.tmpl b/public/templates/includes/navbar.tmpl deleted file mode 100644 index 6a767db..0000000 --- a/public/templates/includes/navbar.tmpl +++ /dev/null @@ -1,38 +0,0 @@ -{{define "navbar"}} - -{{end}} \ No newline at end of file diff --git a/public/templates/includes/notifymodal.tmpl b/public/templates/includes/notifymodal.tmpl deleted file mode 100644 index 96740db..0000000 --- a/public/templates/includes/notifymodal.tmpl +++ /dev/null @@ -1,56 +0,0 @@ -{{define "notifymodal"}} - - - -{{end}} \ No newline at end of file diff --git a/public/templates/includes/paneleditmodal.tmpl b/public/templates/includes/paneleditmodal.tmpl deleted file mode 100644 index a7b893c..0000000 --- a/public/templates/includes/paneleditmodal.tmpl +++ /dev/null @@ -1,235 +0,0 @@ -{{define "paneleditmodal"}} - - - -{{end}} \ No newline at end of file diff --git a/public/templates/includes/sidebar.tmpl b/public/templates/includes/sidebar.tmpl deleted file mode 100644 index 31ead53..0000000 --- a/public/templates/includes/sidebar.tmpl +++ /dev/null @@ -1,40 +0,0 @@ -{{define "sidebar"}} - -{{end}} \ No newline at end of file diff --git a/public/templates/includes/substitutionmodal.tmpl b/public/templates/includes/substitutionmodal.tmpl deleted file mode 100644 index 7214a35..0000000 --- a/public/templates/includes/substitutionmodal.tmpl +++ /dev/null @@ -1,43 +0,0 @@ -{{define "substitutions"}} - - - -{{end}} \ No newline at end of file diff --git a/public/templates/layouts/error.tmpl b/public/templates/layouts/error.tmpl deleted file mode 100644 index 1682b41..0000000 --- a/public/templates/layouts/error.tmpl +++ /dev/null @@ -1,47 +0,0 @@ - - - - {{template "head" .}} - - -
-
-
-
-
-
-
-

Error {{.status}}

-
-
-

{{.error}}

-
- -
- -
-
-
-
-
- - diff --git a/public/templates/layouts/main.tmpl b/public/templates/layouts/main.tmpl deleted file mode 100644 index 733d08a..0000000 --- a/public/templates/layouts/main.tmpl +++ /dev/null @@ -1,19 +0,0 @@ - - - - {{template "head" .}} - - -
- {{template "sidebar" .}} -
- {{template "loadingscreen" .}} - - {{template "notifymodal" .}} -
- {{template "content" .}} -
-
-
- - diff --git a/public/templates/layouts/manage.tmpl b/public/templates/layouts/manage.tmpl deleted file mode 100644 index 9e92e0b..0000000 --- a/public/templates/layouts/manage.tmpl +++ /dev/null @@ -1,19 +0,0 @@ - - - - {{template "head" .}} - - -
-
- {{template "navbar" .}} - {{template "loadingscreen" .}} - - - {{template "notifymodal" .}} - - {{template "content" .}} -
-
- - diff --git a/public/templates/views/blacklist.tmpl b/public/templates/views/blacklist.tmpl deleted file mode 100644 index e2b6555..0000000 --- a/public/templates/views/blacklist.tmpl +++ /dev/null @@ -1,141 +0,0 @@ -{{define "content"}} -
-
-
-
-
-
-

Blacklisted Users

-
-
-
-
-
- -
-
-
-
-
-
- - -
-
-
- -
-
-
#
-
- -
-
-
- - -
-
-
- -
-
-
-
-
-
-
-
- - - - - - - - - - - -
User IDUsername#DiscrimRemove
-
-
-
-
-
-
- -
-
-
-
- - -
-{{end}} \ No newline at end of file diff --git a/public/templates/views/index.tmpl b/public/templates/views/index.tmpl deleted file mode 100644 index 979717d..0000000 --- a/public/templates/views/index.tmpl +++ /dev/null @@ -1,120 +0,0 @@ -{{define "content"}} -
-
-
-

Servers

-
-
-
-
-
- - Invite to your server -
-
- -
- -
-
-
-
-
- - -{{end}} \ No newline at end of file diff --git a/public/templates/views/logs.tmpl b/public/templates/views/logs.tmpl deleted file mode 100644 index 76dfd0f..0000000 --- a/public/templates/views/logs.tmpl +++ /dev/null @@ -1,186 +0,0 @@ -{{define "content"}} -
-
-
-
-
-
- -
-
-
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-

Logs

-
-
-
- - - - - - - - - - - -
Ticket IDUsernameUser IDLog URL
- -
-
-
    -
  • -

    Page 1

    -
  • -
-
-
-
-
-
-
-
-
- -
-
-
-
- - - - -
-{{end}} \ No newline at end of file diff --git a/public/templates/views/modmaillogs.tmpl b/public/templates/views/modmaillogs.tmpl deleted file mode 100644 index 816c3bb..0000000 --- a/public/templates/views/modmaillogs.tmpl +++ /dev/null @@ -1,194 +0,0 @@ -{{define "content"}} -
-
-
-
-
-
- -
-
-
-
-
-
- - -
-
-
-
- - -
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-

Logs

-
-
-
- - - - - - - - - - -
UsernameUser IDArchive
- -
-
-
    -
  • -

    Page 1

    -
  • -
-
-
-
-
-
-
-
-
- -
-
-
-
- - - - -
-{{end}} \ No newline at end of file diff --git a/public/templates/views/panels.tmpl b/public/templates/views/panels.tmpl deleted file mode 100644 index a586bd6..0000000 --- a/public/templates/views/panels.tmpl +++ /dev/null @@ -1,561 +0,0 @@ -{{define "content"}} - {{template "substitutions" .}} - {{template "paneleditmodal" .}} - {{template "multipaneleditmodal" .}} - -
-
-
-
-
-
-

Reaction Panels

-
-
-

Your panel quota: /

-

Note: You can create unlimited panels with premium

- - - - - - - - - - - - - -
ChannelPanel TitleTicket Channel CategoryEditDelete
-
-
-
-
-
-
-

Multi-Panels

-
-
- - - - - - - - - - -
Embed TitleEditDelete
-
-
-
-
-
-
-
-
-

Create A Panel

-
-
-
-
-
-
- - -
-
-
-
- - -
-
-
-
-
- -
- -
-
- -
- -
-
-
#
-
- -
-
- -
- -
- -
-
- -
-
- - -
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
- - -
-
-
-
-
-
- - -
-
-
-
- - -
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
-

Create A Multi-Panel

-
-
- -
-
-
-
- - -
-
-
-
-
-
- - -
-
-
-
-
- -
- -
-
- -
- -
-
-
#
-
- -
-
-
- -
-
- - -
-
- -
-
-
- -
-
-
-
-
-
-
-
-
- -
-
-
-
- - -
-{{end}} \ No newline at end of file diff --git a/public/templates/views/settings.tmpl b/public/templates/views/settings.tmpl deleted file mode 100644 index 5de2d2d..0000000 --- a/public/templates/views/settings.tmpl +++ /dev/null @@ -1,511 +0,0 @@ -{{define "content"}} - - {{template "substitutions" .}} - -
-
-
-
-
-
-

Settings

-
-
-
-
-
-
- - -
-
- -
-
- - -
-
- -
-
- -
- -
-
-
- -
-
- -
- -
-
-
- -
-
- -
- -
-
-
-
- -
-
- - -
-
- -
-
- -
-
-
#
-
- -
-
- -
-
- - -
-
-
- - -
- - -
-
- - -
- -
-
-
- -
-
-
-
-
-
-
- - -
-
-
-

Claim Settings

-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
- -
- -
-
-
-

Auto-Close

-
This feature is currently disabled. Changing the settings below may not currently be fully functional
-
-
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
- -
-
- -
-
d
-
-
-
- -
-
h
-
-
-
- -
-
m
-
-
-
-
-
-
-
- -
-
- -
-
d
-
-
-
- -
-
h
-
-
-
- -
-
m
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
- - - - - - - - - - -
-{{end}} diff --git a/public/templates/views/tags.tmpl b/public/templates/views/tags.tmpl deleted file mode 100644 index 9af48f7..0000000 --- a/public/templates/views/tags.tmpl +++ /dev/null @@ -1,124 +0,0 @@ -{{define "content"}} -
-
-
-
-
-
-

Create A Tag

-
-
-
-
-
-
- - -
-
-
-
- - -
-
-
- -
-
-
- -
-
-
-
-
-
- -
-
-

Tags

-
-
- - - - - - - - - - -
IDContentDelete
-
-
-
-
-
- -
-
-
-
- - -
-{{end}} \ No newline at end of file diff --git a/public/templates/views/teams.tmpl b/public/templates/views/teams.tmpl deleted file mode 100644 index 4ac8d28..0000000 --- a/public/templates/views/teams.tmpl +++ /dev/null @@ -1,348 +0,0 @@ -{{define "content"}} -
-
-
-
- Support Teams -
-
-

Create Team

-
-
- - -
-
- -

Manage Teams

-
- - -
-
-

Manage Members

- - - -
-
- -
-
-

Add Member

-
- - -
- -
- - -
-
- -
-

Add Role

-
- - -
- - -
-
-
-
-
-
-
-
-
-
- - -{{end}} \ No newline at end of file diff --git a/public/templates/views/ticketlist.tmpl b/public/templates/views/ticketlist.tmpl deleted file mode 100644 index 7f4b67f..0000000 --- a/public/templates/views/ticketlist.tmpl +++ /dev/null @@ -1,58 +0,0 @@ -{{define "content"}} -
-
-
-
-
-
-

Ticket List

-
-
-
- - - - - - - - - - - -
Ticket IDUserAdditional MembersView
-
-
-
-
-
-
- - -
-{{end}} \ No newline at end of file diff --git a/public/templates/views/ticketview.tmpl b/public/templates/views/ticketview.tmpl deleted file mode 100644 index 3e776d8..0000000 --- a/public/templates/views/ticketview.tmpl +++ /dev/null @@ -1,148 +0,0 @@ -{{define "content"}} -
-
-
-
-
-
-

Close Ticket

-
-
- -
- -
-
-
- -

View Ticket

-
-
- -
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
-
- - - - - - -{{end}} diff --git a/public/templates/views/whitelabel.tmpl b/public/templates/views/whitelabel.tmpl deleted file mode 100644 index 3d384f5..0000000 --- a/public/templates/views/whitelabel.tmpl +++ /dev/null @@ -1,303 +0,0 @@ -{{define "content"}} -
-
-
-
-
-
-

Bot Token

-
-
-
-
-
-
- - -
-
-
- -
-
-

Note: You will not be able to view the token after submitting it

-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
- -
-
-
-

Slash Commands

-
-
-
-
-
- - -
-
-
-
-
- -
-
-
- -
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-

Custom Status

-
-
-
-
-
-
- - -
-
-
- -
-
-
- -
-
-
-
-
-
-
- -
-
-
-

Error Logs

-
-
- - - - - - - - - -
ErrorDate
-
-
-
-
-
-
- -
-
-
-
- - - - -{{end}} \ No newline at end of file diff --git a/utils/archiverclient.go b/utils/archiverclient.go new file mode 100644 index 0000000..b9f5e32 --- /dev/null +++ b/utils/archiverclient.go @@ -0,0 +1,5 @@ +package utils + +import "github.com/TicketsBot/archiverclient" + +var ArchiverClient archiverclient.ArchiverClient diff --git a/utils/discord/auth.go b/utils/discord/auth.go index 5edf0b8..989c6f5 100644 --- a/utils/discord/auth.go +++ b/utils/discord/auth.go @@ -39,7 +39,7 @@ type ( } ) -const TokenEndpoint = "https://discordapp.com/api/oauth2/token" +const TokenEndpoint = "https://discord.com/api/oauth2/token" func AccessToken(code string) (TokenResponse, error) { data := TokenData{ diff --git a/utils/guildutils.go b/utils/guildutils.go index 6bfbf74..793e027 100644 --- a/utils/guildutils.go +++ b/utils/guildutils.go @@ -21,7 +21,7 @@ func LoadGuilds(accessToken string, userId uint64) error { var wrappedGuilds []database.UserGuild - // endpoint's partial guild doesn't include ownerid + // endpoint's partial guild doesn't includes ownerid // we only user cached guilds on the index page, so it doesn't matter if we don't have have the real owner id // if the user isn't the owner, as we pull from the cache on other endpoints for _, guild := range guilds { diff --git a/utils/sessionutils.go b/utils/sessionutils.go index a3ebf44..3ef510f 100644 --- a/utils/sessionutils.go +++ b/utils/sessionutils.go @@ -1,17 +1,20 @@ package utils import ( + "fmt" "github.com/gin-gonic/contrib/sessions" ) func IsLoggedIn(store sessions.Session) bool { - return store.Get("access_token") != nil && - store.Get("expiry") != nil && - store.Get("refresh_token") != nil && - store.Get("userid") != nil && - store.Get("name") != nil && - store.Get("avatar") != nil && - store.Get("csrf") != nil + requiredKeys := []string{"access_token", "expiry", "refresh_token", "userid", "name", "avatar", "csrf"} + for _, key := range requiredKeys { + if store.Get(key) == nil { + fmt.Println(key) + return false + } + } + + return true } func GetUserId(store sessions.Session) uint64 {