diff --git a/app/http/endpoints/api/export/export.go b/app/http/endpoints/api/export/export.go new file mode 100644 index 0000000..6ce6e49 --- /dev/null +++ b/app/http/endpoints/api/export/export.go @@ -0,0 +1,481 @@ +package api + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/TicketsBot/GoPanel/botcontext" + dbclient "github.com/TicketsBot/GoPanel/database" + "github.com/TicketsBot/GoPanel/utils" + "github.com/TicketsBot/GoPanel/utils/types" + "github.com/TicketsBot/database" + v2 "github.com/TicketsBot/logarchiver/pkg/model/v2" + "github.com/TicketsBot/worker/bot/customisation" + "github.com/gin-gonic/gin" + "golang.org/x/sync/errgroup" +) + +type ( + AutoCloseData struct { + Enabled bool `json:"enabled"` + SinceOpenWithNoResponse int64 `json:"since_open_with_no_response"` + SinceLastMessage int64 `json:"since_last_message"` + OnUserLeave bool `json:"on_user_leave"` + } + + Ticket struct { + TicketId int `json:"ticket_id"` + CloseReason *string `json:"close_reason"` + ClosedBy *uint64 `json:"closed_by"` + Rating *uint8 `json:"rating"` + Transcript v2.Transcript `json:"transcript"` + } + + ColourMap map[customisation.Colour]utils.HexColour + + Settings struct { + database.Settings + ClaimSettings database.ClaimSettings `json:"claim_settings"` + AutoCloseSettings AutoCloseData `json:"auto_close"` + TicketPermissions database.TicketPermissions `json:"ticket_permissions"` + Colours ColourMap `json:"colours"` + + WelcomeMessage string `json:"welcome_message"` + TicketLimit uint8 `json:"ticket_limit"` + Category uint64 `json:"category,string"` + ArchiveChannel *uint64 `json:"archive_channel,string"` + NamingScheme database.NamingScheme `json:"naming_scheme"` + UsersCanClose bool `json:"users_can_close"` + CloseConfirmation bool `json:"close_confirmation"` + FeedbackEnabled bool `json:"feedback_enabled"` + Language *string `json:"language"` + } + Panel struct { + database.Panel + WelcomeMessage *types.CustomEmbed `json:"welcome_message"` + UseCustomEmoji bool `json:"use_custom_emoji"` + Emoji types.Emoji `json:"emote"` + Mentions []string `json:"mentions"` + Teams []int `json:"teams"` + UseServerDefaultNamingScheme bool `json:"use_server_default_naming_scheme"` + AccessControlList []database.PanelAccessControlRule `json:"access_control_list"` + } + MultiPanel struct { + database.MultiPanel + Panels []int `json:"panels"` + } + Export struct { + Settings Settings `json:"settings"` + Panels []Panel `json:"panels"` + MultiPanels []MultiPanel `json:"multi_panels"` + Tickets []Ticket `json:"tickets"` + } +) + +var activeColours = []customisation.Colour{customisation.Green, customisation.Red} + +func ExportHandler(ctx *gin.Context) { + guildId, selfId := ctx.Keys["guildid"].(uint64), ctx.Keys["userid"].(uint64) + + botCtx, err := botcontext.ContextForGuild(guildId) + if err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + guild, err := botCtx.GetGuild(context.Background(), guildId) + if err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + if guild.OwnerId != selfId { + ctx.JSON(403, utils.ErrorStr("Only the server owner export server data")) + return + } + + settings := getSettings(ctx, guildId) + + multiPanels, err := dbclient.Client.MultiPanels.GetByGuild(ctx, guildId) + if err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + multiPanelData := make([]MultiPanel, len(multiPanels)) + + group, _ := errgroup.WithContext(context.Background()) + + var dbTickets []database.Ticket + + // ticket list + group.Go(func() (err error) { + dbTickets, err = dbclient.Client.Tickets.GetByOptions(ctx, database.TicketQueryOptions{ + GuildId: guildId, + }) + + return + }) + + for i := range multiPanels { + i := i + multiPanel := multiPanels[i] + + multiPanelData[i] = MultiPanel{ + MultiPanel: multiPanel, + } + + group.Go(func() error { + panels, err := dbclient.Client.MultiPanelTargets.GetPanels(ctx, multiPanel.Id) + if err != nil { + return err + } + + panelIds := make([]int, len(panels)) + for i, panel := range panels { + panelIds[i] = panel.PanelId + } + + multiPanelData[i].Panels = panelIds + + return nil + }) + } + + var dbPanels []database.PanelWithWelcomeMessage + var panelACLs map[int][]database.PanelAccessControlRule + var panelFields map[int][]database.EmbedField + // panels + group.Go(func() (err error) { + dbPanels, err = dbclient.Client.Panel.GetByGuildWithWelcomeMessage(ctx, guildId) + return + }) + + // access control lists + group.Go(func() (err error) { + panelACLs, err = dbclient.Client.PanelAccessControlRules.GetAllForGuild(ctx, guildId) + return + }) + + // all fields + group.Go(func() (err error) { + panelFields, err = dbclient.Client.EmbedFields.GetAllFieldsForPanels(ctx, guildId) + return + }) + + if err := group.Wait(); err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + // ratings + ticketIds := make([]int, len(dbTickets)) + for i, ticket := range dbTickets { + ticketIds[i] = ticket.Id + } + + ticketGroup, _ := errgroup.WithContext(context.Background()) + + var ( + ratings map[int]uint8 + closeReasons map[int]database.CloseMetadata + tickets = make([]Ticket, len(dbTickets)) + ) + + ticketGroup.Go(func() (err error) { + ratings, err = dbclient.Client.ServiceRatings.GetMulti(ctx, guildId, ticketIds) + return + }) + + ticketGroup.Go(func() (err error) { + closeReasons, err = dbclient.Client.CloseReason.GetMulti(ctx, guildId, ticketIds) + return + }) + + if err := ticketGroup.Wait(); err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + transcriptGroup, _ := errgroup.WithContext(context.Background()) + + for i := range dbTickets { + ticket := dbTickets[i] + + transcriptGroup.Go(func() (err error) { + var transcriptMessages v2.Transcript + transcriptMessages, err = utils.ArchiverClient.Get(ctx, guildId, ticket.Id) + if err != nil { + if err.Error() == "Transcript not found" { + err = nil + } + return + } + + transcript := Ticket{ + TicketId: ticket.Id, + Transcript: transcriptMessages, + } + + if v, ok := ratings[ticket.Id]; ok { + transcript.Rating = &v + } + + if v, ok := closeReasons[ticket.Id]; ok { + transcript.CloseReason = v.Reason + transcript.ClosedBy = v.ClosedBy + } + + tickets[i] = transcript + + return + }) + + } + + if err := transcriptGroup.Wait(); err != nil { + fmt.Println(err) + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + panels := make([]Panel, len(dbPanels)) + + panelGroup, _ := errgroup.WithContext(context.Background()) + + for i, p := range dbPanels { + i := i + p := p + + panelGroup.Go(func() error { + var mentions []string + + // get if we should mention the ticket opener + shouldMention, err := dbclient.Client.PanelUserMention.ShouldMentionUser(ctx, p.PanelId) + if err != nil { + return err + } + + if shouldMention { + mentions = append(mentions, "user") + } + + // get role mentions + roles, err := dbclient.Client.PanelRoleMentions.GetRoles(ctx, p.PanelId) + if err != nil { + return err + } + + // convert to strings + for _, roleId := range roles { + mentions = append(mentions, strconv.FormatUint(roleId, 10)) + } + + teams, err := dbclient.Client.PanelTeams.GetTeamIds(ctx, p.PanelId) + if err != nil { + return err + } + + if teams == nil { + teams = make([]int, 0) + } + + var welcomeMessage *types.CustomEmbed + if p.WelcomeMessage != nil { + fields := panelFields[p.WelcomeMessage.Id] + welcomeMessage = types.NewCustomEmbed(p.WelcomeMessage, fields) + } + + acl := panelACLs[p.PanelId] + if acl == nil { + acl = make([]database.PanelAccessControlRule, 0) + } + + panels[i] = Panel{ + Panel: p.Panel, + WelcomeMessage: welcomeMessage, + UseCustomEmoji: p.EmojiId != nil, + Emoji: types.NewEmoji(p.EmojiName, p.EmojiId), + Mentions: mentions, + Teams: teams, + UseServerDefaultNamingScheme: p.NamingScheme == nil, + AccessControlList: acl, + } + + return nil + + }) + } + + if err := panelGroup.Wait(); err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + ctx.JSON(200, Export{ + Settings: settings, + Panels: panels, + MultiPanels: multiPanelData, + Tickets: tickets, + }) +} + +func getSettings(ctx *gin.Context, guildId uint64) Settings { + var settings Settings + group, _ := errgroup.WithContext(context.Background()) + + // main settings + group.Go(func() (err error) { + settings.Settings, err = dbclient.Client.Settings.Get(ctx, guildId) + return + }) + + // claim settings + group.Go(func() (err error) { + settings.ClaimSettings, err = dbclient.Client.ClaimSettings.Get(ctx, guildId) + return + }) + + // auto close settings + group.Go(func() error { + tmp, err := dbclient.Client.AutoClose.Get(ctx, guildId) + if err != nil { + return err + } + + settings.AutoCloseSettings = convertToAutoCloseData(tmp) + return nil + }) + + // ticket permissions + group.Go(func() (err error) { + settings.TicketPermissions, err = dbclient.Client.TicketPermissions.Get(ctx, guildId) + return + }) + + // colour map + group.Go(func() (err error) { + settings.Colours, err = getColourMap(guildId) + return + }) + + // welcome message + group.Go(func() (err error) { + settings.WelcomeMessage, err = dbclient.Client.WelcomeMessages.Get(ctx, guildId) + if err == nil && settings.WelcomeMessage == "" { + settings.WelcomeMessage = "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(ctx, guildId) + if err == nil && settings.TicketLimit == 0 { + settings.TicketLimit = 5 // Set default + } + + return + }) + + // category + group.Go(func() (err error) { + settings.Category, err = dbclient.Client.ChannelCategory.Get(ctx, guildId) + return + }) + + // archive channel + group.Go(func() (err error) { + settings.ArchiveChannel, err = dbclient.Client.ArchiveChannel.Get(ctx, guildId) + return + }) + + // allow users to close + group.Go(func() (err error) { + settings.UsersCanClose, err = dbclient.Client.UsersCanClose.Get(ctx, guildId) + return + }) + + // naming scheme + group.Go(func() (err error) { + settings.NamingScheme, err = dbclient.Client.NamingScheme.Get(ctx, guildId) + return + }) + + // close confirmation + group.Go(func() (err error) { + settings.CloseConfirmation, err = dbclient.Client.CloseConfirmation.Get(ctx, guildId) + return + }) + + // close confirmation + group.Go(func() (err error) { + settings.FeedbackEnabled, err = dbclient.Client.FeedbackEnabled.Get(ctx, guildId) + return + }) + + // language + group.Go(func() error { + locale, err := dbclient.Client.ActiveLanguage.Get(ctx, guildId) + if err != nil { + return err + } + + if locale != "" { + settings.Language = utils.Ptr(locale) + } + + return nil + }) + + if err := group.Wait(); err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + } + + return settings +} + +func getColourMap(guildId uint64) (ColourMap, error) { + raw, err := dbclient.Client.CustomColours.GetAll(context.Background(), guildId) + if err != nil { + return nil, err + } + + colours := make(ColourMap) + for id, hex := range raw { + if !utils.Exists(activeColours, customisation.Colour(id)) { + continue + } + + colours[customisation.Colour(id)] = utils.HexColour(hex) + } + + for _, id := range activeColours { + if _, ok := colours[id]; !ok { + colours[id] = utils.HexColour(customisation.DefaultColours[id]) + } + } + + return colours, nil +} + +func convertToAutoCloseData(settings database.AutoCloseSettings) (body AutoCloseData) { + body.Enabled = settings.Enabled + + if settings.SinceOpenWithNoResponse != nil { + body.SinceOpenWithNoResponse = int64(*settings.SinceOpenWithNoResponse / time.Second) + } + + if settings.SinceLastMessage != nil { + body.SinceLastMessage = int64(*settings.SinceLastMessage / time.Second) + } + + if settings.OnUserLeave != nil { + body.OnUserLeave = *settings.OnUserLeave + } + + return +} diff --git a/app/http/server.go b/app/http/server.go index a5b501e..04e1439 100644 --- a/app/http/server.go +++ b/app/http/server.go @@ -1,9 +1,12 @@ package http import ( + "time" + "github.com/TicketsBot/GoPanel/app/http/endpoints/api" "github.com/TicketsBot/GoPanel/app/http/endpoints/api/admin/botstaff" api_blacklist "github.com/TicketsBot/GoPanel/app/http/endpoints/api/blacklist" + api_export "github.com/TicketsBot/GoPanel/app/http/endpoints/api/export" api_forms "github.com/TicketsBot/GoPanel/app/http/endpoints/api/forms" api_integrations "github.com/TicketsBot/GoPanel/app/http/endpoints/api/integrations" api_panels "github.com/TicketsBot/GoPanel/app/http/endpoints/api/panel" @@ -22,9 +25,7 @@ import ( "github.com/TicketsBot/GoPanel/config" "github.com/TicketsBot/common/permission" "github.com/gin-gonic/gin" - "github.com/penglongli/gin-metrics/ginmetrics" "go.uber.org/zap" - "time" ) func StartServer(logger *zap.Logger, sm *livechat.SocketManager) { @@ -119,6 +120,7 @@ func StartServer(logger *zap.Logger, sm *livechat.SocketManager) { // Must be readable to load transcripts page guildAuthApiSupport.GET("/settings", api_settings.GetSettingsHandler) guildAuthApiAdmin.POST("/settings", api_settings.UpdateSettingsHandler) + guildAuthApiAdmin.GET("/export", api_export.ExportHandler) guildAuthApiSupport.GET("/blacklist", api_blacklist.GetBlacklistHandler) guildAuthApiSupport.POST("/blacklist", api_blacklist.AddBlacklistHandler) diff --git a/frontend/src/components/manage/ExportModal.svelte b/frontend/src/components/manage/ExportModal.svelte new file mode 100644 index 0000000..bcb9141 --- /dev/null +++ b/frontend/src/components/manage/ExportModal.svelte @@ -0,0 +1,116 @@ + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/includes/ManageSidebar.svelte b/frontend/src/includes/ManageSidebar.svelte index 6ed96fc..0ba05a2 100644 --- a/frontend/src/includes/ManageSidebar.svelte +++ b/frontend/src/includes/ManageSidebar.svelte @@ -1,3 +1,6 @@ +{#if modal} + modal = false}/> +{/if}