Migrate to svelte for frontend (#9)
* Svelte: WIP * WIP * WIP * WIP * WIP * WIP * Finished * Remove redundant code * Fix typo * Re-add routes * Form margin * Mobile nicities * Mobile changed * Increase keepalvie * Update Guild.svelte * Update Whitelabel.svelte * Whitelabel changes
@ -2,20 +2,20 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"github.com/TicketsBot/GoPanel/database"
|
"github.com/TicketsBot/GoPanel/database"
|
||||||
"github.com/TicketsBot/GoPanel/rpc/cache"
|
"github.com/TicketsBot/GoPanel/rpc/cache"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/rxdn/gdl/objects/user"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type userData struct {
|
type userData struct {
|
||||||
|
UserId uint64 `json:"id,string"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Discriminator string `json:"discriminator"`
|
Discriminator user.Discriminator `json:"discriminator"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Paginate
|
||||||
func GetBlacklistHandler(ctx *gin.Context) {
|
func GetBlacklistHandler(ctx *gin.Context) {
|
||||||
guildId := ctx.Keys["guildid"].(uint64)
|
guildId := ctx.Keys["guildid"].(uint64)
|
||||||
|
|
||||||
@ -28,24 +28,26 @@ func GetBlacklistHandler(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := make(map[string]userData)
|
data := make([]userData, len(blacklistedUsers))
|
||||||
var lock sync.Mutex
|
|
||||||
|
|
||||||
group, _ := errgroup.WithContext(context.Background())
|
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 {
|
group.Go(func() error {
|
||||||
user, _ := cache.Instance.GetUser(userId)
|
userData := userData{
|
||||||
|
UserId: userId,
|
||||||
lock.Lock()
|
|
||||||
|
|
||||||
// JS cant do big ints
|
|
||||||
data[strconv.FormatUint(userId, 10)] = userData{
|
|
||||||
Username: user.Username,
|
|
||||||
Discriminator: fmt.Sprintf("%04d", user.Discriminator),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lock.Unlock()
|
user, ok := cache.Instance.GetUser(userId)
|
||||||
|
if ok {
|
||||||
|
userData.Username = user.Username
|
||||||
|
userData.Discriminator = user.Discriminator
|
||||||
|
}
|
||||||
|
|
||||||
|
data[i] = userData
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,64 +1,37 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/TicketsBot/GoPanel/database"
|
"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/gin-gonic/gin"
|
||||||
"github.com/jackc/pgx/v4"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AddBlacklistHandler(ctx *gin.Context) {
|
func AddBlacklistHandler(ctx *gin.Context) {
|
||||||
guildId := ctx.Keys["guildid"].(uint64)
|
guildId := ctx.Keys["guildid"].(uint64)
|
||||||
|
|
||||||
var data userData
|
id, err := strconv.ParseUint(ctx.Param("user"), 10, 64)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.AbortWithStatusJSON(400, gin.H{
|
ctx.JSON(400, utils.ErrorJson(err))
|
||||||
"success": false,
|
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetId uint64
|
permLevel, err := utils.GetPermissionLevel(guildId, id)
|
||||||
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 err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
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(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Don't blacklist staff or guild owner
|
if permLevel > permission.Everyone {
|
||||||
if err = database.Client.Blacklist.Add(guildId, targetId); err == nil {
|
ctx.JSON(400, utils.ErrorStr("You cannot blacklist staff members!"))
|
||||||
ctx.JSON(200, gin.H{
|
return
|
||||||
"success": true,
|
|
||||||
"user_id": strconv.FormatUint(targetId, 10),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
ctx.JSON(500, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = database.Client.Blacklist.Add(guildId, id); err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(200, utils.SuccessResponse)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -121,16 +121,22 @@ func (d *multiPanelCreateData) doValidations(guildId uint64) (panels []database.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *multiPanelCreateData) validateTitle() (err error) {
|
func (d *multiPanelCreateData) validateTitle() (err error) {
|
||||||
if len(d.Title) > 255 || len(d.Title) < 1 {
|
if len(d.Title) > 255 {
|
||||||
err = errors.New("embed title must be between 1 and 255 characters")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *multiPanelCreateData) validateContent() (err error) {
|
func (d *multiPanelCreateData) validateContent() (err error) {
|
||||||
if len(d.Content) > 1024 || len(d.Title) < 1 {
|
if len(d.Content) > 1024 {
|
||||||
err = errors.New("embed content must be between 1 and 1024 characters")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import (
|
|||||||
func MultiPanelList(ctx *gin.Context) {
|
func MultiPanelList(ctx *gin.Context) {
|
||||||
type multiPanelResponse struct {
|
type multiPanelResponse struct {
|
||||||
database.MultiPanel
|
database.MultiPanel
|
||||||
Panels []database.Panel `json:"panels"`
|
Panels []int `json:"panels"`
|
||||||
}
|
}
|
||||||
|
|
||||||
guildId := ctx.Keys["guildid"].(uint64)
|
guildId := ctx.Keys["guildid"].(uint64)
|
||||||
@ -33,13 +33,20 @@ func MultiPanelList(ctx *gin.Context) {
|
|||||||
MultiPanel: multiPanel,
|
MultiPanel: multiPanel,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Use a join
|
||||||
group.Go(func() error {
|
group.Go(func() error {
|
||||||
panels, err := dbclient.Client.MultiPanelTargets.GetPanels(multiPanel.Id)
|
panels, err := dbclient.Client.MultiPanelTargets.GetPanels(multiPanel.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,8 @@ type panelBody struct {
|
|||||||
Emote string `json:"emote"`
|
Emote string `json:"emote"`
|
||||||
WelcomeMessage *string `json:"welcome_message"`
|
WelcomeMessage *string `json:"welcome_message"`
|
||||||
Mentions []string `json:"mentions"`
|
Mentions []string `json:"mentions"`
|
||||||
Teams []string `json:"teams"`
|
WithDefaultTeam bool `json:"default_team"`
|
||||||
|
Teams []database.SupportTeam `json:"teams"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreatePanel(ctx *gin.Context) {
|
func CreatePanel(ctx *gin.Context) {
|
||||||
@ -120,7 +121,7 @@ func CreatePanel(ctx *gin.Context) {
|
|||||||
TargetCategory: data.CategoryId,
|
TargetCategory: data.CategoryId,
|
||||||
ReactionEmote: emoji,
|
ReactionEmote: emoji,
|
||||||
WelcomeMessage: data.WelcomeMessage,
|
WelcomeMessage: data.WelcomeMessage,
|
||||||
WithDefaultTeam: utils.ContainsString(data.Teams, "default"),
|
WithDefaultTeam: data.WithDefaultTeam,
|
||||||
CustomId: customId,
|
CustomId: customId,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,31 +180,22 @@ func CreatePanel(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns (response_code, error)
|
// 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
|
// insert teams
|
||||||
group, _ := errgroup.WithContext(context.Background())
|
group, _ := errgroup.WithContext(context.Background())
|
||||||
for _, teamId := range teamIds {
|
for _, team := range teams {
|
||||||
if teamId == "default" {
|
|
||||||
continue // already handled
|
|
||||||
}
|
|
||||||
|
|
||||||
teamId, err := strconv.Atoi(teamId)
|
|
||||||
if err != nil {
|
|
||||||
return 400, err
|
|
||||||
}
|
|
||||||
|
|
||||||
group.Go(func() error {
|
group.Go(func() error {
|
||||||
// ensure team exists
|
// ensure team exists
|
||||||
exists, err := dbclient.Client.SupportTeam.Exists(teamId, guildId)
|
exists, err := dbclient.Client.SupportTeam.Exists(team.Id, guildId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
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 {
|
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 {
|
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) {
|
func (p *panelBody) getEmoji() (emoji string, ok bool) {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/TicketsBot/GoPanel/botcontext"
|
"github.com/TicketsBot/GoPanel/botcontext"
|
||||||
dbclient "github.com/TicketsBot/GoPanel/database"
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
@ -12,9 +11,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rxdn/gdl/rest"
|
"github.com/rxdn/gdl/rest"
|
||||||
"github.com/rxdn/gdl/rest/request"
|
"github.com/rxdn/gdl/rest/request"
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func UpdatePanel(ctx *gin.Context) {
|
func UpdatePanel(ctx *gin.Context) {
|
||||||
@ -67,50 +64,41 @@ func UpdatePanel(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var wouldHaveDuplicateEmote bool
|
premiumTier := rpc.PremiumClient.GetTierByGuildId(guildId, true, botContext.Token, botContext.RateLimiter)
|
||||||
|
|
||||||
{
|
for _, multiPanel := range multiPanels {
|
||||||
var duplicateLock sync.Mutex
|
panels, err := dbclient.Client.MultiPanelTargets.GetPanels(multiPanel.Id)
|
||||||
|
|
||||||
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 {
|
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 {
|
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Optimise this
|
||||||
|
panelIds := make([]int, len(panels))
|
||||||
|
for i, panel := range panels {
|
||||||
|
panelIds[i] = panel.PanelId
|
||||||
}
|
}
|
||||||
|
|
||||||
if wouldHaveDuplicateEmote {
|
data := multiPanelCreateData{
|
||||||
ctx.JSON(400, utils.ErrorJson(errors.New("Changing the reaction emote to this value would cause a conflict in a multi-panel")))
|
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
|
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
|
// check if we need to update the message
|
||||||
shouldUpdateMessage := uint32(existing.Colour) != data.Colour ||
|
shouldUpdateMessage := uint32(existing.Colour) != data.Colour ||
|
||||||
existing.ChannelId != data.ChannelId ||
|
existing.ChannelId != data.ChannelId ||
|
||||||
@ -125,7 +113,6 @@ func UpdatePanel(ctx *gin.Context) {
|
|||||||
// delete old message, ignoring error
|
// delete old message, ignoring error
|
||||||
_ = rest.DeleteMessage(botContext.Token, botContext.RateLimiter, existing.ChannelId, existing.MessageId)
|
_ = 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)
|
newMessageId, err = data.sendEmbed(&botContext, existing.Title, existing.CustomId, existing.ReactionEmote, premiumTier > premium.None)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var unwrapped request.RestError
|
var unwrapped request.RestError
|
||||||
@ -155,7 +142,7 @@ func UpdatePanel(ctx *gin.Context) {
|
|||||||
TargetCategory: data.CategoryId,
|
TargetCategory: data.CategoryId,
|
||||||
ReactionEmote: emoji,
|
ReactionEmote: emoji,
|
||||||
WelcomeMessage: data.WelcomeMessage,
|
WelcomeMessage: data.WelcomeMessage,
|
||||||
WithDefaultTeam: utils.ContainsString(data.Teams, "default"),
|
WithDefaultTeam: data.WithDefaultTeam,
|
||||||
CustomId: existing.CustomId,
|
CustomId: existing.CustomId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,5 +23,6 @@ func PremiumHandler(ctx *gin.Context) {
|
|||||||
|
|
||||||
ctx.JSON(200, gin.H{
|
ctx.JSON(200, gin.H{
|
||||||
"premium": premiumTier >= premium.Premium,
|
"premium": premiumTier >= premium.Premium,
|
||||||
|
"tier": premiumTier,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,10 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/TicketsBot/GoPanel/app/http/session"
|
||||||
"github.com/TicketsBot/GoPanel/messagequeue"
|
"github.com/TicketsBot/GoPanel/messagequeue"
|
||||||
"github.com/TicketsBot/GoPanel/utils"
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
"github.com/TicketsBot/GoPanel/utils/discord"
|
"github.com/TicketsBot/GoPanel/utils/discord"
|
||||||
"github.com/gin-gonic/contrib/sessions"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -36,19 +36,22 @@ func ReloadGuildsHandler(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
store := sessions.Default(ctx)
|
store, err := session.Store.Get(userId)
|
||||||
if store == nil {
|
if err != nil {
|
||||||
ctx.JSON(200, gin.H{
|
if err == session.ErrNoSession {
|
||||||
|
ctx.JSON(401, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"reauthenticate_required": true,
|
"auth": true,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accessToken := store.Get("access_token").(string)
|
if store.Expiry > (time.Now().UnixNano() / int64(time.Second)) {
|
||||||
expiry := store.Get("expiry").(int64)
|
res, err := discord.RefreshToken(store.RefreshToken)
|
||||||
if expiry > (time.Now().UnixNano() / int64(time.Second)) {
|
|
||||||
res, err := discord.RefreshToken(store.Get("refresh_token").(string))
|
|
||||||
if err != nil { // Tell client to re-authenticate
|
if err != nil { // Tell client to re-authenticate
|
||||||
ctx.JSON(200, gin.H{
|
ctx.JSON(200, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@ -57,15 +60,17 @@ func ReloadGuildsHandler(ctx *gin.Context) {
|
|||||||
return
|
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)
|
if err := session.Store.Set(userId, store); err != nil {
|
||||||
store.Set("refresh_token", res.RefreshToken)
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
store.Set("expiry", (time.Now().UnixNano()/int64(time.Second))+int64(res.ExpiresIn))
|
return
|
||||||
store.Save()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := utils.LoadGuilds(accessToken, userId); err != nil {
|
if err := utils.LoadGuilds(store.AccessToken, userId); err != nil {
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/TicketsBot/GoPanel/botcontext"
|
"github.com/TicketsBot/GoPanel/botcontext"
|
||||||
"github.com/TicketsBot/GoPanel/utils"
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/rxdn/gdl/objects/member"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SearchMembers(ctx *gin.Context) {
|
func SearchMembers(ctx *gin.Context) {
|
||||||
@ -16,12 +17,18 @@ func SearchMembers(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query := ctx.Query("query")
|
query := ctx.Query("query")
|
||||||
if len(query) == 0 || len(query) > 32 {
|
if len(query) > 32 {
|
||||||
ctx.JSON(400, utils.ErrorStr("Invalid query"))
|
ctx.JSON(400, utils.ErrorStr("Invalid query"))
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
ctx.JSON(500, utils.ErrorJson(err))
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
return
|
return
|
||||||
|
31
app/http/endpoints/api/session.go
Normal file
@ -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,
|
||||||
|
})
|
||||||
|
}
|
@ -30,18 +30,30 @@ func GetSettingsHandler(ctx *gin.Context) {
|
|||||||
// prefix
|
// prefix
|
||||||
group.Go(func() (err error) {
|
group.Go(func() (err error) {
|
||||||
settings.Prefix, err = dbclient.Client.Prefix.Get(guildId)
|
settings.Prefix, err = dbclient.Client.Prefix.Get(guildId)
|
||||||
|
if err == nil && settings.Prefix == "" {
|
||||||
|
settings.Prefix = "t!"
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
// welcome message
|
// welcome message
|
||||||
group.Go(func() (err error) {
|
group.Go(func() (err error) {
|
||||||
settings.WelcomeMessaage, err = dbclient.Client.WelcomeMessages.Get(guildId)
|
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
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
// ticket limit
|
// ticket limit
|
||||||
group.Go(func() (err error) {
|
group.Go(func() (err error) {
|
||||||
settings.TicketLimit, err = dbclient.Client.TicketLimit.Get(guildId)
|
settings.TicketLimit, err = dbclient.Client.TicketLimit.Get(guildId)
|
||||||
|
if err == nil && settings.TicketLimit == 0 {
|
||||||
|
settings.TicketLimit = 5 // Set default
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ func formatMembers(guildId uint64, userIds, roleIds []uint64) ([]entity, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// map role ids to names
|
// map role ids to names
|
||||||
var data []entity
|
data := make([]entity, 0)
|
||||||
for _, roleId := range roleIds {
|
for _, roleId := range roleIds {
|
||||||
for _, role := range roles {
|
for _, role := range roles {
|
||||||
if roleId == role.Id {
|
if roleId == role.Id {
|
||||||
|
@ -86,7 +86,7 @@ func GetTicket(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
messagesFormatted = append(messagesFormatted, map[string]interface{}{
|
messagesFormatted = append(messagesFormatted, map[string]interface{}{
|
||||||
"username": message.Author.Username,
|
"author": message.Author,
|
||||||
"content": content,
|
"content": content,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,19 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"github.com/TicketsBot/GoPanel/database"
|
"github.com/TicketsBot/GoPanel/database"
|
||||||
"github.com/TicketsBot/GoPanel/rpc/cache"
|
"github.com/TicketsBot/GoPanel/rpc/cache"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/rxdn/gdl/objects/user"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetTickets(ctx *gin.Context) {
|
func GetTickets(ctx *gin.Context) {
|
||||||
|
type WithUser struct {
|
||||||
|
TicketId int `json:"id"`
|
||||||
|
User *user.User `json:"user,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
guildId := ctx.Keys["guildid"].(uint64)
|
guildId := ctx.Keys["guildid"].(uint64)
|
||||||
|
|
||||||
tickets, err := database.Client.Tickets.GetGuildOpenTickets(guildId)
|
tickets, err := database.Client.Tickets.GetGuildOpenTickets(guildId)
|
||||||
@ -22,7 +26,7 @@ func GetTickets(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ticketsFormatted := make([]map[string]interface{}, len(tickets))
|
data := make([]WithUser, len(tickets))
|
||||||
|
|
||||||
group, _ := errgroup.WithContext(context.Background())
|
group, _ := errgroup.WithContext(context.Background())
|
||||||
|
|
||||||
@ -31,29 +35,14 @@ func GetTickets(ctx *gin.Context) {
|
|||||||
ticket := ticket
|
ticket := ticket
|
||||||
|
|
||||||
group.Go(func() error {
|
group.Go(func() error {
|
||||||
members, err := database.Client.TicketMembers.Get(guildId, ticket.Id)
|
user, ok := cache.Instance.GetUser(ticket.UserId)
|
||||||
if err != nil {
|
|
||||||
return err
|
data[i] = WithUser{
|
||||||
|
TicketId: ticket.Id,
|
||||||
}
|
}
|
||||||
|
|
||||||
membersFormatted := make([]map[string]interface{}, 0)
|
if ok {
|
||||||
for _, userId := range members {
|
data[i].User = &user
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -68,5 +57,5 @@ func GetTickets(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(200, ticketsFormatted)
|
ctx.JSON(200, data)
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
68
app/http/endpoints/api/transcripts/get.go
Normal file
@ -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)
|
||||||
|
}
|
247
app/http/endpoints/api/transcripts/list.go
Normal file
@ -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]
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
@ -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")))
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
@ -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"),
|
|
||||||
})
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,14 +2,14 @@ package root
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/TicketsBot/GoPanel/app/http/session"
|
||||||
"github.com/TicketsBot/GoPanel/config"
|
"github.com/TicketsBot/GoPanel/config"
|
||||||
"github.com/TicketsBot/GoPanel/utils"
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
"github.com/TicketsBot/GoPanel/utils/discord"
|
"github.com/TicketsBot/GoPanel/utils/discord"
|
||||||
"github.com/apex/log"
|
"github.com/dgrijalva/jwt-go"
|
||||||
"github.com/gin-gonic/contrib/sessions"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rxdn/gdl/rest"
|
"github.com/rxdn/gdl/rest"
|
||||||
"strings"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,63 +33,59 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func CallbackHandler(ctx *gin.Context) {
|
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")
|
code, ok := ctx.GetQuery("code")
|
||||||
if !ok {
|
if !ok {
|
||||||
utils.ErrorPage(ctx, 400, "Discord provided invalid Oauth2 code")
|
ctx.JSON(400, utils.ErrorStr("Discord provided invalid Oauth2 code"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := discord.AccessToken(code)
|
res, err := discord.AccessToken(code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.ErrorPage(ctx, 500, err.Error())
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
return
|
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
|
// Get ID + name
|
||||||
currentUser, err := rest.GetCurrentUser(fmt.Sprintf("Bearer %s", res.AccessToken), nil)
|
currentUser, err := rest.GetCurrentUser(fmt.Sprintf("Bearer %s", res.AccessToken), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.String(500, err.Error())
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
store.Set("csrf", utils.RandString(32))
|
store := session.SessionData{
|
||||||
|
AccessToken: res.AccessToken,
|
||||||
store.Set("userid", currentUser.Id)
|
Expiry: (time.Now().UnixNano()/int64(time.Second))+int64(res.ExpiresIn),
|
||||||
store.Set("name", currentUser.Username)
|
RefreshToken: res.RefreshToken,
|
||||||
store.Set("avatar", currentUser.AvatarUrl(256))
|
Name: currentUser.Username,
|
||||||
store.Save()
|
Avatar: currentUser.AvatarUrl(256),
|
||||||
|
HasGuilds: false,
|
||||||
|
}
|
||||||
|
|
||||||
if err := utils.LoadGuilds(res.AccessToken, currentUser.Id); err == nil {
|
if err := utils.LoadGuilds(res.AccessToken, currentUser.Id); err == nil {
|
||||||
store.Set("has_guilds", true)
|
store.HasGuilds = true
|
||||||
store.Save()
|
|
||||||
} else {
|
} 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(),
|
||||||
|
})
|
||||||
|
|
||||||
|
str, err := token.SignedString([]byte(config.Conf.Server.Secret))
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRedirect(ctx *gin.Context) {
|
if err := session.Store.Set(currentUser.Id, store); err != nil {
|
||||||
state := strings.Split(ctx.Query("state"), ".")
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if len(state) == 3 && state[0] == "viewlog" {
|
ctx.JSON(200, gin.H{
|
||||||
ctx.Redirect(302, fmt.Sprintf("%s/manage/%s/logs/view/%s", config.Conf.Server.BaseUrl, state[1], state[2]))
|
"success": true,
|
||||||
} else {
|
"token": str,
|
||||||
ctx.Redirect(302, config.Conf.Server.BaseUrl)
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
@ -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")))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +1,18 @@
|
|||||||
package root
|
package root
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/contrib/sessions"
|
"github.com/TicketsBot/GoPanel/app/http/session"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LogoutHandler(ctx *gin.Context) {
|
func LogoutHandler(ctx *gin.Context) {
|
||||||
store := sessions.Default(ctx)
|
userId := ctx.Keys["userid"].(uint64)
|
||||||
if store == nil {
|
|
||||||
|
if err := session.Store.Clear(userId); err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer store.Save()
|
|
||||||
|
|
||||||
store.Clear()
|
ctx.Status(204)
|
||||||
|
|
||||||
ctx.Redirect(302, "https://ticketsbot.net")
|
|
||||||
}
|
}
|
||||||
|
193
app/http/endpoints/root/webchatws.go
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
@ -38,6 +38,10 @@ func AuthenticateToken(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ctx.Keys == nil {
|
||||||
|
ctx.Keys = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Keys["userid"] = parsedId
|
ctx.Keys["userid"] = parsedId
|
||||||
} else {
|
} else {
|
||||||
ctx.AbortWithStatusJSON(401, utils.ErrorStr("Token is invalid"))
|
ctx.AbortWithStatusJSON(401, utils.ErrorStr("Token is invalid"))
|
||||||
|
25
app/http/middleware/cors.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,25 +1,23 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/TicketsBot/GoPanel/app/http/endpoints/api"
|
"github.com/TicketsBot/GoPanel/app/http/endpoints/api"
|
||||||
api_autoclose "github.com/TicketsBot/GoPanel/app/http/endpoints/api/autoclose"
|
api_autoclose "github.com/TicketsBot/GoPanel/app/http/endpoints/api/autoclose"
|
||||||
api_blacklist "github.com/TicketsBot/GoPanel/app/http/endpoints/api/blacklist"
|
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_panels "github.com/TicketsBot/GoPanel/app/http/endpoints/api/panel"
|
||||||
api_settings "github.com/TicketsBot/GoPanel/app/http/endpoints/api/settings"
|
api_settings "github.com/TicketsBot/GoPanel/app/http/endpoints/api/settings"
|
||||||
api_tags "github.com/TicketsBot/GoPanel/app/http/endpoints/api/tags"
|
api_tags "github.com/TicketsBot/GoPanel/app/http/endpoints/api/tags"
|
||||||
api_team "github.com/TicketsBot/GoPanel/app/http/endpoints/api/team"
|
api_team "github.com/TicketsBot/GoPanel/app/http/endpoints/api/team"
|
||||||
api_ticket "github.com/TicketsBot/GoPanel/app/http/endpoints/api/ticket"
|
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"
|
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/endpoints/root"
|
||||||
"github.com/TicketsBot/GoPanel/app/http/middleware"
|
"github.com/TicketsBot/GoPanel/app/http/middleware"
|
||||||
|
"github.com/TicketsBot/GoPanel/app/http/session"
|
||||||
"github.com/TicketsBot/GoPanel/config"
|
"github.com/TicketsBot/GoPanel/config"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
"github.com/TicketsBot/common/permission"
|
"github.com/TicketsBot/common/permission"
|
||||||
"github.com/gin-contrib/multitemplate"
|
|
||||||
"github.com/gin-contrib/static"
|
"github.com/gin-contrib/static"
|
||||||
"github.com/gin-gonic/contrib/sessions"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/ulule/limiter/v3"
|
"github.com/ulule/limiter/v3"
|
||||||
mgin "github.com/ulule/limiter/v3/drivers/middleware/gin"
|
mgin "github.com/ulule/limiter/v3/drivers/middleware/gin"
|
||||||
@ -34,15 +32,7 @@ func StartServer() {
|
|||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
|
|
||||||
// Sessions
|
// Sessions
|
||||||
store, err := sessions.NewRedisStore(
|
session.Store = session.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))
|
|
||||||
|
|
||||||
// Handle static asset requests
|
// Handle static asset requests
|
||||||
router.Use(static.Serve("/assets/", static.LocalFile("./public/static", false)))
|
router.Use(static.Serve("/assets/", static.LocalFile("./public/static", false)))
|
||||||
@ -50,39 +40,18 @@ func StartServer() {
|
|||||||
router.Use(gin.Recovery())
|
router.Use(gin.Recovery())
|
||||||
router.Use(createLimiter(600, time.Minute*10))
|
router.Use(createLimiter(600, time.Minute*10))
|
||||||
|
|
||||||
// Register templates
|
router.Use(middleware.Cors(config.Conf))
|
||||||
router.HTMLRender = createRenderer()
|
|
||||||
|
|
||||||
router.GET("/login", root.LoginHandler)
|
router.GET("/webchat", root.WebChatWs)
|
||||||
router.GET("/callback", root.CallbackHandler)
|
|
||||||
|
|
||||||
router.GET("/manage/:id/logs/view/:ticket", manage.LogViewHandler) // we check in the actual handler bc of a custom redirect
|
router.POST("/callback", middleware.VerifyXTicketsHeader, root.CallbackHandler)
|
||||||
|
router.POST("/logout", middleware.VerifyXTicketsHeader, middleware.AuthenticateToken, root.LogoutHandler)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
apiGroup := router.Group("/api", middleware.VerifyXTicketsHeader, middleware.AuthenticateToken)
|
apiGroup := router.Group("/api", middleware.VerifyXTicketsHeader, middleware.AuthenticateToken)
|
||||||
|
{
|
||||||
|
apiGroup.GET("/session", api.SessionHandler)
|
||||||
|
}
|
||||||
|
|
||||||
guildAuthApiAdmin := apiGroup.Group("/:id", middleware.AuthenticateGuild(true, permission.Admin))
|
guildAuthApiAdmin := apiGroup.Group("/:id", middleware.AuthenticateGuild(true, permission.Admin))
|
||||||
guildAuthApiSupport := apiGroup.Group("/:id", middleware.AuthenticateGuild(true, permission.Support))
|
guildAuthApiSupport := apiGroup.Group("/:id", middleware.AuthenticateGuild(true, permission.Support))
|
||||||
{
|
{
|
||||||
@ -90,18 +59,18 @@ func StartServer() {
|
|||||||
guildAuthApiSupport.GET("/premium", api.PremiumHandler)
|
guildAuthApiSupport.GET("/premium", api.PremiumHandler)
|
||||||
guildAuthApiSupport.GET("/user/:user", api.UserHandler)
|
guildAuthApiSupport.GET("/user/:user", api.UserHandler)
|
||||||
guildAuthApiSupport.GET("/roles", api.RolesHandler)
|
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.GET("/settings", api_settings.GetSettingsHandler)
|
||||||
guildAuthApiAdmin.POST("/settings", api_settings.UpdateSettingsHandler)
|
guildAuthApiAdmin.POST("/settings", api_settings.UpdateSettingsHandler)
|
||||||
|
|
||||||
guildAuthApiSupport.GET("/blacklist", api_blacklist.GetBlacklistHandler)
|
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)
|
guildAuthApiSupport.DELETE("/blacklist/:user", api_blacklist.RemoveBlacklistHandler)
|
||||||
|
|
||||||
guildAuthApiAdmin.GET("/panels", api_panels.ListPanels)
|
guildAuthApiAdmin.GET("/panels", api_panels.ListPanels)
|
||||||
guildAuthApiAdmin.PUT("/panels", api_panels.CreatePanel)
|
guildAuthApiAdmin.POST("/panels", api_panels.CreatePanel)
|
||||||
guildAuthApiAdmin.PUT("/panels/:panelid", api_panels.UpdatePanel)
|
guildAuthApiAdmin.PATCH("/panels/:panelid", api_panels.UpdatePanel)
|
||||||
guildAuthApiAdmin.DELETE("/panels/:panelid", api_panels.DeletePanel)
|
guildAuthApiAdmin.DELETE("/panels/:panelid", api_panels.DeletePanel)
|
||||||
|
|
||||||
guildAuthApiAdmin.GET("/multipanels", api_panels.MultiPanelList)
|
guildAuthApiAdmin.GET("/multipanels", api_panels.MultiPanelList)
|
||||||
@ -109,7 +78,8 @@ func StartServer() {
|
|||||||
guildAuthApiAdmin.PATCH("/multipanels/:panelid", api_panels.MultiPanelUpdate)
|
guildAuthApiAdmin.PATCH("/multipanels/:panelid", api_panels.MultiPanelUpdate)
|
||||||
guildAuthApiAdmin.DELETE("/multipanels/:panelid", api_panels.MultiPanelDelete)
|
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", api_ticket.GetTickets)
|
||||||
guildAuthApiSupport.GET("/tickets/:ticketId", api_ticket.GetTicket)
|
guildAuthApiSupport.GET("/tickets/:ticketId", api_ticket.GetTicket)
|
||||||
@ -127,7 +97,7 @@ func StartServer() {
|
|||||||
guildAuthApiAdmin.POST("/autoclose", api_autoclose.PostAutoClose)
|
guildAuthApiAdmin.POST("/autoclose", api_autoclose.PostAutoClose)
|
||||||
|
|
||||||
guildAuthApiAdmin.GET("/team", api_team.GetTeams)
|
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.POST("/team", createLimiter(10, time.Minute), api_team.CreateTeam)
|
||||||
guildAuthApiAdmin.PUT("/team/:teamid/:snowflake", createLimiter(5, time.Second * 10), api_team.AddMember)
|
guildAuthApiAdmin.PUT("/team/:teamid/:snowflake", createLimiter(5, time.Second * 10), api_team.AddMember)
|
||||||
guildAuthApiAdmin.DELETE("/team/:teamid", api_team.DeleteTeam)
|
guildAuthApiAdmin.DELETE("/team/:teamid", api_team.DeleteTeam)
|
||||||
@ -141,18 +111,17 @@ func StartServer() {
|
|||||||
userGroup.GET("/permissionlevel", api.GetPermissionLevel)
|
userGroup.GET("/permissionlevel", api.GetPermissionLevel)
|
||||||
|
|
||||||
{
|
{
|
||||||
whitelabelGroup := userGroup.Group("/whitelabel", middleware.VerifyWhitelabel(false))
|
whitelabelGroup := userGroup.Group("/whitelabel", middleware.VerifyWhitelabel(true))
|
||||||
whitelabelApiGroup := userGroup.Group("/whitelabel", middleware.VerifyWhitelabel(true))
|
|
||||||
|
|
||||||
whitelabelGroup.GET("/", api_whitelabel.WhitelabelGet)
|
whitelabelGroup.GET("/", api_whitelabel.WhitelabelGet)
|
||||||
whitelabelApiGroup.GET("/errors", api_whitelabel.WhitelabelGetErrors)
|
whitelabelGroup.GET("/errors", api_whitelabel.WhitelabelGetErrors)
|
||||||
whitelabelApiGroup.GET("/guilds", api_whitelabel.WhitelabelGetGuilds)
|
whitelabelGroup.GET("/guilds", api_whitelabel.WhitelabelGetGuilds)
|
||||||
whitelabelApiGroup.GET("/public-key", api_whitelabel.WhitelabelGetPublicKey)
|
whitelabelGroup.GET("/public-key", api_whitelabel.WhitelabelGetPublicKey)
|
||||||
whitelabelApiGroup.POST("/public-key", api_whitelabel.WhitelabelPostPublicKey)
|
whitelabelGroup.POST("/public-key", api_whitelabel.WhitelabelPostPublicKey)
|
||||||
whitelabelApiGroup.POST("/create-interactions", api_whitelabel.GetWhitelabelCreateInteractions())
|
whitelabelGroup.POST("/create-interactions", api_whitelabel.GetWhitelabelCreateInteractions())
|
||||||
|
|
||||||
whitelabelApiGroup.POST("/", createLimiter(10, time.Minute), api_whitelabel.WhitelabelPost)
|
whitelabelGroup.POST("/", createLimiter(10, time.Minute), api_whitelabel.WhitelabelPost)
|
||||||
whitelabelApiGroup.POST("/status", createLimiter(1, time.Second*5), api_whitelabel.WhitelabelStatusPost)
|
whitelabelGroup.POST("/status", createLimiter(1, time.Second*5), api_whitelabel.WhitelabelStatusPost)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,82 +132,32 @@ func StartServer() {
|
|||||||
|
|
||||||
func serveTemplate(templateName string) func(*gin.Context) {
|
func serveTemplate(templateName string) func(*gin.Context) {
|
||||||
return func(ctx *gin.Context) {
|
return func(ctx *gin.Context) {
|
||||||
store := sessions.Default(ctx)
|
|
||||||
guildId := ctx.Keys["guildid"].(uint64)
|
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{
|
ctx.HTML(200, templateName, gin.H{
|
||||||
"name": store.Get("name").(string),
|
"name": store.Name,
|
||||||
"guildId": guildId,
|
"guildId": guildId,
|
||||||
"avatar": store.Get("avatar").(string),
|
"avatar": store.Avatar,
|
||||||
"baseUrl": config.Conf.Server.BaseUrl,
|
"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) {
|
func createLimiter(limit int64, period time.Duration) func(*gin.Context) {
|
||||||
store := memory.NewStore()
|
store := memory.NewStore()
|
||||||
rate := limiter.Rate{
|
rate := limiter.Rate{
|
||||||
|
54
app/http/session/redisstore.go
Normal file
@ -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()
|
||||||
|
}
|
10
app/http/session/sessiondata.go
Normal file
@ -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"`
|
||||||
|
}
|
9
app/http/session/store.go
Normal file
@ -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
|
@ -112,3 +112,17 @@ func (ctx BotContext) SearchMembers(guildId uint64, query string) (members []mem
|
|||||||
|
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/TicketsBot/GoPanel/app/http"
|
"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/config"
|
||||||
"github.com/TicketsBot/GoPanel/database"
|
"github.com/TicketsBot/GoPanel/database"
|
||||||
"github.com/TicketsBot/GoPanel/messagequeue"
|
"github.com/TicketsBot/GoPanel/messagequeue"
|
||||||
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/TicketsBot/GoPanel/rpc/cache"
|
"github.com/TicketsBot/GoPanel/rpc/cache"
|
||||||
"github.com/TicketsBot/GoPanel/utils"
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
"github.com/TicketsBot/archiverclient"
|
"github.com/TicketsBot/archiverclient"
|
||||||
|
"github.com/TicketsBot/common/chatrelay"
|
||||||
"github.com/TicketsBot/common/premium"
|
"github.com/TicketsBot/common/premium"
|
||||||
"github.com/TicketsBot/worker/bot/i18n"
|
"github.com/TicketsBot/worker/bot/i18n"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
@ -36,7 +37,7 @@ func main() {
|
|||||||
database.ConnectToDatabase()
|
database.ConnectToDatabase()
|
||||||
cache.Instance = cache.NewCache()
|
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()
|
utils.LoadEmoji()
|
||||||
if err := i18n.LoadMessages(database.Client); err != nil {
|
if err := i18n.LoadMessages(database.Client); err != nil {
|
||||||
@ -48,7 +49,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
messagequeue.Client = messagequeue.NewRedisClient()
|
messagequeue.Client = messagequeue.NewRedisClient()
|
||||||
go Listen(messagequeue.Client)
|
go ListenChat(messagequeue.Client)
|
||||||
|
|
||||||
rpc.PremiumClient = premium.NewPremiumLookupClient(
|
rpc.PremiumClient = premium.NewPremiumLookupClient(
|
||||||
premium.NewPatreonClient(config.Conf.Bot.PremiumLookupProxyUrl, config.Conf.Bot.PremiumLookupProxyKey),
|
premium.NewPatreonClient(config.Conf.Bot.PremiumLookupProxyUrl, config.Conf.Bot.PremiumLookupProxyKey),
|
||||||
@ -60,19 +61,19 @@ func main() {
|
|||||||
http.StartServer()
|
http.StartServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Listen(client messagequeue.RedisClient) {
|
func ListenChat(client messagequeue.RedisClient) {
|
||||||
ch := make(chan messagequeue.TicketMessage)
|
ch := make(chan chatrelay.MessageData)
|
||||||
go client.ListenForMessages(ch)
|
go chatrelay.Listen(client.Client, ch)
|
||||||
|
|
||||||
for decoded := range ch {
|
for event := range ch {
|
||||||
manage.SocketsLock.Lock()
|
root.SocketsLock.RLock()
|
||||||
for _, socket := range manage.Sockets {
|
for _, socket := range root.Sockets {
|
||||||
if socket.Guild == decoded.GuildId && socket.Ticket == decoded.TicketId {
|
if socket.GuildId == event.Ticket.GuildId && socket.TicketId == event.Ticket.Id {
|
||||||
if err := socket.Ws.WriteJSON(decoded); err != nil {
|
if err := socket.Ws.WriteJSON(event.Message); err != nil {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
manage.SocketsLock.Unlock()
|
root.SocketsLock.RUnlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ type (
|
|||||||
Bot Bot
|
Bot Bot
|
||||||
Redis Redis
|
Redis Redis
|
||||||
Cache Cache
|
Cache Cache
|
||||||
Referral Referral
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Server struct {
|
Server struct {
|
||||||
@ -119,7 +118,6 @@ func fromEnvvar() {
|
|||||||
oauthId, _ := strconv.ParseUint(os.Getenv("OAUTH_ID"), 10, 64)
|
oauthId, _ := strconv.ParseUint(os.Getenv("OAUTH_ID"), 10, 64)
|
||||||
redisPort, _ := strconv.Atoi(os.Getenv("REDIS_PORT"))
|
redisPort, _ := strconv.Atoi(os.Getenv("REDIS_PORT"))
|
||||||
redisThreads, _ := strconv.Atoi(os.Getenv("REDIS_THREADS"))
|
redisThreads, _ := strconv.Atoi(os.Getenv("REDIS_THREADS"))
|
||||||
showReferral, _ := strconv.ParseBool(os.Getenv("REFERRAL_SHOW"))
|
|
||||||
|
|
||||||
Conf = Config{
|
Conf = Config{
|
||||||
Admins: admins,
|
Admins: admins,
|
||||||
@ -163,9 +161,5 @@ func fromEnvvar() {
|
|||||||
Cache: Cache{
|
Cache: Cache{
|
||||||
Uri: os.Getenv("CACHE_URI"),
|
Uri: os.Getenv("CACHE_URI"),
|
||||||
},
|
},
|
||||||
Referral: Referral{
|
|
||||||
Show: showReferral,
|
|
||||||
Link: os.Getenv("REFERRAL_LINK"),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
envvars.md
@ -1,3 +1,13 @@
|
|||||||
|
# Build
|
||||||
|
---
|
||||||
|
- CLIENT_ID
|
||||||
|
- REDIRECT_URI
|
||||||
|
- API_URL
|
||||||
|
- WS_URL
|
||||||
|
|
||||||
|
# Runtime
|
||||||
|
---
|
||||||
|
|
||||||
- ADMINS
|
- ADMINS
|
||||||
- FORCED_WHITELABEL
|
- FORCED_WHITELABEL
|
||||||
- SERVER_ADDR
|
- SERVER_ADDR
|
||||||
@ -22,5 +32,3 @@
|
|||||||
- REDIS_PASSWORD
|
- REDIS_PASSWORD
|
||||||
- REDIS_THREADS
|
- REDIS_THREADS
|
||||||
- CACHE_URI
|
- CACHE_URI
|
||||||
- REFERRAL_SHOW
|
|
||||||
- REFERRAL_LINK
|
|
4
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/node_modules/
|
||||||
|
/public/build
|
||||||
|
|
||||||
|
.DS_Store
|
876
frontend/package-lock.json
generated
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
frontend/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 964 B After Width: | Height: | Size: 964 B |
BIN
frontend/public/favicon.ico
Normal file
After Width: | Height: | Size: 1.1 KiB |
64
frontend/public/global.css
Normal file
@ -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;
|
||||||
|
}
|
18
frontend/public/index.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'>
|
||||||
|
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||||
|
|
||||||
|
<title>Tickets Dashboard</title>
|
||||||
|
|
||||||
|
<link rel='icon' type='image/ico' href='/favicon.ico'>
|
||||||
|
<link rel='stylesheet' href='/global.css'>
|
||||||
|
<link rel='stylesheet' href='/build/bundle.css'>
|
||||||
|
|
||||||
|
<script defer src='/build/bundle.js'></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
86
frontend/rollup.config.js
Normal file
@ -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
|
||||||
|
}
|
||||||
|
};
|
117
frontend/scripts/setupTypeScript.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
/** This script modifies the project to support TS code in .svelte files like:
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export let name: string;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
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("<script>", '<script lang="ts">')
|
||||||
|
appFile = appFile.replace("export let name;", 'export let name: string;')
|
||||||
|
fs.writeFileSync(appSveltePath, appFile)
|
||||||
|
|
||||||
|
// Edit rollup config
|
||||||
|
const rollupConfigPath = path.join(projectRoot, "rollup.config.js")
|
||||||
|
let rollupConfig = fs.readFileSync(rollupConfigPath, "utf8")
|
||||||
|
|
||||||
|
// Edit imports
|
||||||
|
rollupConfig = rollupConfig.replace(`'rollup-plugin-terser';`, `'rollup-plugin-terser';
|
||||||
|
import sveltePreprocess from 'svelte-preprocess';
|
||||||
|
import typescript from '@rollup/plugin-typescript';`)
|
||||||
|
|
||||||
|
// Replace name of entry point
|
||||||
|
rollupConfig = rollupConfig.replace(`'src/main.js'`, `'src/main.ts'`)
|
||||||
|
|
||||||
|
// Add preprocessor
|
||||||
|
rollupConfig = rollupConfig.replace(
|
||||||
|
'compilerOptions:',
|
||||||
|
'preprocess: sveltePreprocess({ sourceMap: !production }),\n\t\t\tcompilerOptions:'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add TypeScript
|
||||||
|
rollupConfig = rollupConfig.replace(
|
||||||
|
'commonjs(),',
|
||||||
|
'commonjs(),\n\t\ttypescript({\n\t\t\tsourceMap: !production,\n\t\t\tinlineSources: !production\n\t\t}),'
|
||||||
|
);
|
||||||
|
fs.writeFileSync(rollupConfigPath, rollupConfig)
|
||||||
|
|
||||||
|
// Add TSConfig
|
||||||
|
const tsconfig = `{
|
||||||
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
|
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
|
||||||
|
}`
|
||||||
|
const tsconfigPath = path.join(projectRoot, "tsconfig.json")
|
||||||
|
fs.writeFileSync(tsconfigPath, tsconfig)
|
||||||
|
|
||||||
|
// Delete this script, but not during testing
|
||||||
|
if (!argv[2]) {
|
||||||
|
// Remove the script
|
||||||
|
fs.unlinkSync(path.join(__filename))
|
||||||
|
|
||||||
|
// Check for Mac's DS_store file, and if it's the only one left remove it
|
||||||
|
const remainingFiles = fs.readdirSync(path.join(__dirname))
|
||||||
|
if (remainingFiles.length === 1 && remainingFiles[0] === '.DS_store') {
|
||||||
|
fs.unlinkSync(path.join(__dirname, '.DS_store'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the scripts folder is empty
|
||||||
|
if (fs.readdirSync(path.join(__dirname)).length === 0) {
|
||||||
|
// Remove the scripts folder
|
||||||
|
fs.rmdirSync(path.join(__dirname))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds the extension recommendation
|
||||||
|
fs.mkdirSync(path.join(projectRoot, ".vscode"), { recursive: true })
|
||||||
|
fs.writeFileSync(path.join(projectRoot, ".vscode", "extensions.json"), `{
|
||||||
|
"recommendations": ["svelte.svelte-vscode"]
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
console.log("Converted to TypeScript.")
|
||||||
|
|
||||||
|
if (fs.existsSync(path.join(projectRoot, "node_modules"))) {
|
||||||
|
console.log("\nYou will need to re-run your dependency manager to get started.")
|
||||||
|
}
|
6
frontend/src/App.svelte
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<Router {routes} />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Router } from 'svelte-router-spa'
|
||||||
|
import { routes } from './routes'
|
||||||
|
</script>
|
68
frontend/src/components/Button.svelte
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<button on:click isTrigger="1" class:fullWidth class:danger {disabled} {type}>
|
||||||
|
{#if icon !== undefined}
|
||||||
|
<i class="{icon}"></i>
|
||||||
|
{/if}
|
||||||
|
<span class="content">
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let icon;
|
||||||
|
export let fullWidth = false;
|
||||||
|
export let disabled = false;
|
||||||
|
export let type = "submit";
|
||||||
|
export let danger = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
button {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
background-color: #3472f7;
|
||||||
|
border-color: #3472f7;
|
||||||
|
border-width: 2px;
|
||||||
|
border-radius: .25rem;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 150ms ease-in-out, border-color 150ms ease-in-out;
|
||||||
|
box-shadow: 0 4px 4px rgb(0 0 0 / 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active, button:hover:enabled {
|
||||||
|
background-color: #0062cc;
|
||||||
|
border-color: #0062cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background-color: #6c757d;
|
||||||
|
border-color: #6c757d;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullWidth {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
background-color: #dc3545 !important;
|
||||||
|
border-color: #dc3545 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger:hover:enabled, .danger:active {
|
||||||
|
background-color: #c32232 !important;
|
||||||
|
border-color: #c32232 !important;
|
||||||
|
}
|
||||||
|
</style>
|
137
frontend/src/components/Card.svelte
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<script>
|
||||||
|
export let footer = true;
|
||||||
|
export let fill = true;
|
||||||
|
export let footerRight = false;
|
||||||
|
export let dropdown = false;
|
||||||
|
export let ref;
|
||||||
|
|
||||||
|
let dropdownActive = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card" class:fill>
|
||||||
|
<div class="card-header" class:dropdown on:click={() => dropdownActive = dropdown && !dropdownActive}>
|
||||||
|
<h4 class="card-title">
|
||||||
|
<slot name="title">
|
||||||
|
No Title :(
|
||||||
|
</slot>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" class:dropdown class:dropdownActive class:dropdownInactive={dropdown && !dropdownActive} {ref}>
|
||||||
|
<div class="inner" class:dropdown>
|
||||||
|
<slot name="body">
|
||||||
|
No Content :(
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if footer}
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="footer-content" class:footerRight>
|
||||||
|
<slot name="footer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
background-color: #272727 !important;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
|
||||||
|
transition: all .3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
color: white;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bolder;
|
||||||
|
|
||||||
|
padding: 10px 20px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, .125);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header.dropdown {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 75px;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
margin: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner.dropdown {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body.dropdown {
|
||||||
|
position: relative;
|
||||||
|
transition: min-height .3s ease-in-out, margin-top .3s ease-in-out, margin-bottom .3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body.dropdownInactive {
|
||||||
|
height: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
flex: unset;
|
||||||
|
min-height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body.dropdownActive {
|
||||||
|
visibility: visible;
|
||||||
|
min-height: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
color: white;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, .125);
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerRight {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(div [slot=footer]) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner > * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
22
frontend/src/components/CategoryDropdown.svelte
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<Dropdown col1={col1} col2={col2} col3={col3} col4={col4} bind:value={value} label={label}>
|
||||||
|
{#each channels as channel}
|
||||||
|
{#if channel.type === 4}
|
||||||
|
<option value={channel.id}>
|
||||||
|
{channel.name}
|
||||||
|
</option>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Dropdown from "./form/Dropdown.svelte"
|
||||||
|
|
||||||
|
export let value;
|
||||||
|
export let label;
|
||||||
|
export let channels;
|
||||||
|
|
||||||
|
export let col1 = false;
|
||||||
|
export let col2 = false;
|
||||||
|
export let col3 = false;
|
||||||
|
export let col4 = false;
|
||||||
|
</script>
|
22
frontend/src/components/ChannelDropdown.svelte
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<Dropdown {col1} {col2} {col3} {col4} bind:value label={label}>
|
||||||
|
{#each channels as channel}
|
||||||
|
{#if channel.type === 0}
|
||||||
|
<option value="{channel.id}">
|
||||||
|
#{channel.name}
|
||||||
|
</option>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</Dropdown>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Dropdown from "./form/Dropdown.svelte"
|
||||||
|
|
||||||
|
export let value;
|
||||||
|
export let label;
|
||||||
|
export let channels = [];
|
||||||
|
|
||||||
|
export let col1 = false;
|
||||||
|
export let col2 = false;
|
||||||
|
export let col3 = false;
|
||||||
|
export let col4 = false;
|
||||||
|
</script>
|
108
frontend/src/components/DiscordMessages.svelte
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<div class="discord-container">
|
||||||
|
<div class="channel-header">
|
||||||
|
<span id="channel-name">#ticket-{ticketId}</span>
|
||||||
|
</div>
|
||||||
|
<div id="message-container" bind:this={container}>
|
||||||
|
{#each messages as message}
|
||||||
|
<div class="message">
|
||||||
|
<b>{message.author.username}:</b> {message.content}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="input-container">
|
||||||
|
<form on:submit|preventDefault={sendMessage}>
|
||||||
|
<input type="text" class="message-input" bind:value={sendContent} disabled={!isPremium}
|
||||||
|
placeholder="{isPremium ? `Message #ticket-${ticketId}` : 'Premium users can receive messages in real-time and response to tickets through the dashboard'}">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {createEventDispatcher} from "svelte";
|
||||||
|
|
||||||
|
export let ticketId;
|
||||||
|
export let isPremium = false;
|
||||||
|
export let messages = [];
|
||||||
|
export let container;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
let sendContent = '';
|
||||||
|
|
||||||
|
function sendMessage() {
|
||||||
|
dispatch('send', sendContent);
|
||||||
|
sendContent = '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Catamaran:wght@300;400;500;600;700;800&display=swap');
|
||||||
|
|
||||||
|
.discord-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
background-color: #2e3136;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 80vh;
|
||||||
|
max-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'Catamaran', sans-serif !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
background-color: #1e2124;
|
||||||
|
height: 5vh;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#channel-name {
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
color: white !important;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-container:last-child {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
border-color: #2e3136 !important;
|
||||||
|
background-color: #2e3136 !important;
|
||||||
|
color: white !important;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-input:focus, .message-input:focus-visible {
|
||||||
|
outline-width: 0;
|
||||||
|
}
|
||||||
|
</style>
|
108
frontend/src/components/Guild.svelte
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<div class="guild-badge" on:click={goto(guild.id)}>
|
||||||
|
<div class="guild-icon-bg">
|
||||||
|
{#if guild.icon === undefined || guild.icon === ""}
|
||||||
|
<i class="fas fa-question guild-icon-fa"></i>
|
||||||
|
{:else}
|
||||||
|
<img class="guild-icon" src="{getIconUrl()}" alt="Guild Icon"/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="guild-name">
|
||||||
|
{guild.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
import {API_URL} from "../js/constants";
|
||||||
|
import {notifyError} from "../js/util";
|
||||||
|
|
||||||
|
export let guild;
|
||||||
|
|
||||||
|
function isAnimated() {
|
||||||
|
if (guild.icon === undefined || guild.icon === "") {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return guild.icon.startsWith('a_')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconUrl() {
|
||||||
|
if (isAnimated()) {
|
||||||
|
return `https:\/\/cdn.discordapp.com/icons/${guild.id}/${guild.icon}.gif?size=256`
|
||||||
|
} else {
|
||||||
|
return `https:\/\/cdn.discordapp.com/icons/${guild.id}/${guild.icon}.webp?size=256`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function goto(guildId) {
|
||||||
|
const permissionLevels = await getPermissionLevel(guildId);
|
||||||
|
if (permissionLevels[guildId] === 2) {
|
||||||
|
window.location.href = `/manage/${guildId}/settings`;
|
||||||
|
} else {
|
||||||
|
window.location.href = `/manage/${guildId}/transcripts`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPermissionLevel(guildId) {
|
||||||
|
const res = await axios.get(`${API_URL}/user/permissionlevel?guilds=${guildId}`);
|
||||||
|
if (res.status !== 200 || !res.data.success) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data.levels;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.guild-badge) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
|
||||||
|
|
||||||
|
width: 33%;
|
||||||
|
background-color: #121212;
|
||||||
|
height: 100px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 950px) {
|
||||||
|
:global(.guild-badge) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.guild-icon-bg) {
|
||||||
|
height: 80px;
|
||||||
|
width: 80px;
|
||||||
|
background-color: #272727;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.guild-icon) {
|
||||||
|
height: 80px;
|
||||||
|
width: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.guild-icon-fa) {
|
||||||
|
border-radius: 50%;
|
||||||
|
color: white;
|
||||||
|
font-size: 60px !important;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.guild-name) {
|
||||||
|
color: white !important;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
15
frontend/src/components/InviteBadge.svelte
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<div class="guild-badge" on:click={invite}>
|
||||||
|
<div class="guild-icon-bg">
|
||||||
|
<i class="fas fa-plus fa-2x guild-icon-fa"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="guild-name">Invite to your server</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function invite() {
|
||||||
|
window.location.href = `https://invite.ticketsbot.net`;
|
||||||
|
}
|
||||||
|
</script>
|
17
frontend/src/components/NamingScheme.svelte
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<Radio label="Naming Scheme" col4=true>
|
||||||
|
<div class="radio-row">
|
||||||
|
<input class="radio-input" type=radio bind:group={value} value="id">
|
||||||
|
<label class="radio-label">#ticket-1</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="radio-row">
|
||||||
|
<input class="radio-input" type=radio bind:group={value} value="username">
|
||||||
|
<label class="radio-label">#ticket-ryan</label>
|
||||||
|
</div>
|
||||||
|
</Radio>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Radio from "./form/Radio.svelte";
|
||||||
|
|
||||||
|
export let value;
|
||||||
|
</script>
|
41
frontend/src/components/NavElement.svelte
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<div class="nav-element">
|
||||||
|
{#if link}
|
||||||
|
<Navigate to="{link}" styles="link row" on:click>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="{icon}"></i>
|
||||||
|
</div>
|
||||||
|
<slot/>
|
||||||
|
</Navigate>
|
||||||
|
{:else}
|
||||||
|
<a class="link row" on:click>
|
||||||
|
<div class="icon">
|
||||||
|
<i class="{icon}"></i>
|
||||||
|
</div>
|
||||||
|
<slot/>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let icon;
|
||||||
|
export let link;
|
||||||
|
|
||||||
|
import {Navigate} from 'svelte-router-spa'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-element {
|
||||||
|
padding: 20px 0 20px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.link) {
|
||||||
|
display: flex;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
28
frontend/src/components/PanelDropdown.svelte
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<label class="form-label">{label}</label>
|
||||||
|
<div class="multiselect-super">
|
||||||
|
<Select placeholder="Select..." items={panels} optionIdentifier="panel_id" getOptionLabel={labelMapper}
|
||||||
|
getSelectionLabel={labelMapper} bind:selectedValue={panelsRaw}
|
||||||
|
on:select={update} isMulti={true}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Select from 'svelte-select';
|
||||||
|
|
||||||
|
export let label;
|
||||||
|
export let panels = [];
|
||||||
|
export let selected = [];
|
||||||
|
|
||||||
|
let panelsRaw = [];
|
||||||
|
|
||||||
|
function labelMapper(panel) {
|
||||||
|
return panel.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
if (panelsRaw === undefined) {
|
||||||
|
panelsRaw = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
selected = panelsRaw.map((panel) => panel.panel_id);
|
||||||
|
}
|
||||||
|
</script>
|
21
frontend/src/components/form/Checkbox.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}>
|
||||||
|
<label for="input" class="form-label">{label}</label>
|
||||||
|
<input id="input" class="form-checkbox" type=checkbox bind:checked={value} on:change>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
export let label;
|
||||||
|
|
||||||
|
export let col1 = false;
|
||||||
|
export let col2 = false;
|
||||||
|
export let col3 = false;
|
||||||
|
export let col4 = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.form-checkbox {
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
</style>
|
20
frontend/src/components/form/Colour.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}>
|
||||||
|
<label for="input" class="form-label">{label}</label>
|
||||||
|
<input id="input" class="form-input" type="color" on:input on:change bind:value={value}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
export let label;
|
||||||
|
|
||||||
|
export let col1 = false;
|
||||||
|
export let col2 = false;
|
||||||
|
export let col3 = false;
|
||||||
|
export let col4 = false;
|
||||||
|
</script>
|
22
frontend/src/components/form/Dropdown.svelte
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}>
|
||||||
|
<label for="input" class="form-label">{label}</label>
|
||||||
|
<select id="input" class="form-input" on:change bind:value={value}>
|
||||||
|
<slot />
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
export let label;
|
||||||
|
|
||||||
|
export let col1 = false;
|
||||||
|
export let col2 = false;
|
||||||
|
export let col3 = false;
|
||||||
|
export let col4 = false;
|
||||||
|
</script>
|
54
frontend/src/components/form/EmojiInput.svelte
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}>
|
||||||
|
<label for="input" class="form-label">{label}</label>
|
||||||
|
<div class="wrapper">
|
||||||
|
<input id="input" class="form-input" readonly placeholder="{placeholder}" disabled="{disabled}" bind:value={value}>
|
||||||
|
{#if !disabled}
|
||||||
|
<EmojiSelector on:emoji={onUpdate}/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
export let label;
|
||||||
|
export let placeholder;
|
||||||
|
export let disabled = false;
|
||||||
|
|
||||||
|
export let col1 = false;
|
||||||
|
export let col2 = false;
|
||||||
|
export let col3 = false;
|
||||||
|
export let col4 = false;
|
||||||
|
|
||||||
|
import EmojiSelector from 'svelte-emoji-selector'
|
||||||
|
|
||||||
|
function onUpdate(e) {
|
||||||
|
value = e.detail;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
border-top-right-radius: 0 !important;
|
||||||
|
border-bottom-right-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.svelte-emoji-picker__trigger) {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
background-color: #272727;
|
||||||
|
border-color: #2e3136 !important;
|
||||||
|
border-left: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.svelte-emoji-picker__trigger:active) {
|
||||||
|
background-color: #2e3136 !important;
|
||||||
|
}
|
||||||
|
</style>
|
25
frontend/src/components/form/Input.svelte
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}>
|
||||||
|
{#if label !== undefined}
|
||||||
|
<label for="input" class="form-label">{label}</label>
|
||||||
|
{/if}
|
||||||
|
<input id="input" class="form-input" placeholder="{placeholder}" disabled="{disabled}" on:input on:change
|
||||||
|
bind:value={value}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
export let label;
|
||||||
|
export let placeholder;
|
||||||
|
export let disabled = false;
|
||||||
|
|
||||||
|
export let col1 = false;
|
||||||
|
export let col2 = false;
|
||||||
|
export let col3 = false;
|
||||||
|
export let col4 = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
285
frontend/src/components/form/MultiSelect.svelte
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
<!--
|
||||||
|
Based upon https://svelte.dev/repl/c7094fb1004b440482d2a88f4d1d7ef5?version=3.14.0
|
||||||
|
Heavily ammended
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {fly} from 'svelte/transition';
|
||||||
|
|
||||||
|
export let values = {};
|
||||||
|
export let selected = [];
|
||||||
|
let filtered = [];
|
||||||
|
|
||||||
|
let input,
|
||||||
|
inputValue = '',
|
||||||
|
options = [],
|
||||||
|
activeOption,
|
||||||
|
showOptions = false,
|
||||||
|
first = true,
|
||||||
|
slot
|
||||||
|
const iconClearPath = 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z';
|
||||||
|
|
||||||
|
function updateFiltered() {
|
||||||
|
filtered = Object.entries(values).filter(([_, name]) => name.includes(inputValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFiltered();
|
||||||
|
|
||||||
|
/*afterUpdate(() => {
|
||||||
|
let newOptions = [];
|
||||||
|
slot.querySelectorAll('option').forEach(o => {
|
||||||
|
o.selected && !value.includes(o.value) && (value = [...value, o.value]);
|
||||||
|
newOptions = [...newOptions, {value: o.value, name: o.textContent}]
|
||||||
|
});
|
||||||
|
value && (selected = newOptions.reduce((obj, op) => value.includes(op.value) ? {
|
||||||
|
...obj,
|
||||||
|
[op.value]: op
|
||||||
|
} : obj, {}));
|
||||||
|
first = false;
|
||||||
|
options = newOptions;
|
||||||
|
});
|
||||||
|
|
||||||
|
$: if (!first) value = Object.values(selected).map(o => o.value);
|
||||||
|
$: filtered = options.filter(o => inputValue ? o.name.toLowerCase().includes(inputValue.toLowerCase()) : o);
|
||||||
|
$: if (activeOption && !filtered.includes(activeOption) || !activeOption && inputValue) activeOption = filtered[0];*/
|
||||||
|
|
||||||
|
|
||||||
|
function add(value) {
|
||||||
|
selected = [...selected, value];
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(value) {
|
||||||
|
selected = selected.filter((e) => e !== value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionsVisibility(show) {
|
||||||
|
if (typeof show === 'boolean') {
|
||||||
|
showOptions = show;
|
||||||
|
show && input.focus();
|
||||||
|
} else {
|
||||||
|
showOptions = !showOptions;
|
||||||
|
}
|
||||||
|
if (!showOptions) {
|
||||||
|
activeOption = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyup(e) {
|
||||||
|
updateFiltered();
|
||||||
|
/*if (e.keyCode === 13) {
|
||||||
|
Object.keys(selected).includes(activeOption.value) ? remove(activeOption.value) : add(activeOption);
|
||||||
|
inputValue = '';
|
||||||
|
}
|
||||||
|
if ([38, 40].includes(e.keyCode)) { // up and down arrows
|
||||||
|
const increment = e.keyCode === 38 ? -1 : 1;
|
||||||
|
const calcIndex = filtered.indexOf(activeOption) + increment;
|
||||||
|
activeOption = calcIndex < 0 ? filtered[filtered.length - 1]
|
||||||
|
: calcIndex === filtered.length ? filtered[0]
|
||||||
|
: filtered[calcIndex];
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBlur(e) {
|
||||||
|
optionsVisibility(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTokenClick(e) {
|
||||||
|
if (e.target.closest('.token-remove')) {
|
||||||
|
e.stopPropagation();
|
||||||
|
remove(e.target.closest('.token').dataset.id);
|
||||||
|
} else {
|
||||||
|
updateFiltered();
|
||||||
|
optionsVisibility(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(id) {
|
||||||
|
if (isSelected(id)) {
|
||||||
|
selected = selected.filter((e) => e !== id);
|
||||||
|
} else {
|
||||||
|
$: selected.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelected(value) {
|
||||||
|
return selected.find((option) => option === value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.multiselect {
|
||||||
|
background-color: #2e3136;
|
||||||
|
border-color: #2e3136;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.tokens {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
position: relative;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tokens::after {
|
||||||
|
background: none repeat scroll 0 0 transparent;
|
||||||
|
bottom: -1px;
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
height: 2px;
|
||||||
|
left: 50%;
|
||||||
|
position: absolute;
|
||||||
|
background: hsl(45, 100%, 51%);
|
||||||
|
transition: width 0.3s ease 0s, left 0.3s ease 0s;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tokens.showOptions::after {
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token {
|
||||||
|
align-items: center;
|
||||||
|
background-color: #272727;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
margin: .25rem .5rem .25rem 0;
|
||||||
|
max-height: 1.3rem;
|
||||||
|
padding: .25rem .5rem .25rem .5rem;
|
||||||
|
transition: background-color .3s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-remove, .remove-all {
|
||||||
|
align-items: center;
|
||||||
|
background-color: #3472f7;
|
||||||
|
transition: background-color .3s;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
height: 1.25rem;
|
||||||
|
margin-left: .25rem;
|
||||||
|
min-width: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-remove:hover, .remove-all:hover {
|
||||||
|
background-color: #0062cc;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
margin: 0;
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-arrow path {
|
||||||
|
fill: hsl(0, 0%, 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect:hover .dropdown-arrow path {
|
||||||
|
fill: hsl(0, 0%, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-clear path {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, .1), 0 -2px 4px rgba(0, 0, 0, .1);
|
||||||
|
left: 0;
|
||||||
|
list-style: none;
|
||||||
|
margin-block-end: 0;
|
||||||
|
margin-block-start: 0;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-inline-start: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 1px);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
background-color: #2e3136;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.selected {
|
||||||
|
background-color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.selected:hover {
|
||||||
|
background-color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:hover {
|
||||||
|
background-color: #272727;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:last-child {
|
||||||
|
border-bottom-left-radius: .2rem;
|
||||||
|
border-bottom-right-radius: .2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
background-color: #2e3136;
|
||||||
|
caret-color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="multiselect">
|
||||||
|
<div class="tokens" class:showOptions on:click={handleTokenClick}>
|
||||||
|
{selected}
|
||||||
|
{#each selected as value}
|
||||||
|
value
|
||||||
|
<div class="token" data-id="{value}">
|
||||||
|
<span>{values[selected]}</span>
|
||||||
|
<div class="token-remove" title="Remove {values[selected]}">
|
||||||
|
<svg class="icon-clear" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
|
||||||
|
<path d="{iconClearPath}"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div class="actions">
|
||||||
|
<input class="search" autocomplete="off" bind:value={inputValue} bind:this={input}
|
||||||
|
on:keyup={handleKeyup} on:blur={handleBlur}>
|
||||||
|
<svg class="dropdown-arrow" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
|
||||||
|
<path d="M5 8l4 4 4-4z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select bind:this={slot} type="multiple" class="hidden">
|
||||||
|
<slot/>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{#if showOptions}
|
||||||
|
<ul class="options" transition:fly="{{duration: 200, y: 5}}">
|
||||||
|
{#each filtered as option}
|
||||||
|
<li class:selected={isSelected[option[0]]} data-value="{option[0]}" on:click={toggle(option[0])}>{option[1]}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</div>
|
32
frontend/src/components/form/Number.svelte
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}>
|
||||||
|
<label for="input" class="form-label">{label}</label>
|
||||||
|
<input
|
||||||
|
id="input" class="form-input" type="number"
|
||||||
|
min={min} max={max} bind:value={value}
|
||||||
|
on:input={validateMax} on:change={validateMin}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
export let label;
|
||||||
|
export let min;
|
||||||
|
export let max;
|
||||||
|
|
||||||
|
export let col1 = false;
|
||||||
|
export let col2 = false;
|
||||||
|
export let col3 = false;
|
||||||
|
export let col4 = false;
|
||||||
|
|
||||||
|
function validateMax() {
|
||||||
|
if (value > max) {
|
||||||
|
value = max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we validateMin on input, the user can never backspace to enter a number
|
||||||
|
function validateMin() {
|
||||||
|
if (value < min) {
|
||||||
|
value = min;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
38
frontend/src/components/form/Radio.svelte
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}>
|
||||||
|
<label class="form-label">{label}</label>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let label;
|
||||||
|
|
||||||
|
export let col1 = false;
|
||||||
|
export let col2 = false;
|
||||||
|
export let col3 = false;
|
||||||
|
export let col4 = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.radio-row) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.radio-label) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 0 0 7px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.radio-input) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
31
frontend/src/components/form/RoleSelect.svelte
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{#if label !== undefined}
|
||||||
|
<label class="form-label">{label}</label>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="multiselect-super">
|
||||||
|
<Select placeholder="Search..." optionIdentifier="id" items={roles}
|
||||||
|
bind:selectedValue={value} getOptionLabel={labelMapper} getSelectionLabel={labelMapper}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Select from 'svelte-select';
|
||||||
|
import axios from "axios";
|
||||||
|
import {onMount} from 'svelte'
|
||||||
|
import {setDefaultHeaders} from '../../includes/Auth.svelte'
|
||||||
|
import {API_URL} from "../../js/constants";
|
||||||
|
import {notifyError, notifyRatelimit} from "../../js/util";
|
||||||
|
|
||||||
|
export let label;
|
||||||
|
export let roles = [];
|
||||||
|
export let guildId;
|
||||||
|
|
||||||
|
export let value;
|
||||||
|
|
||||||
|
function labelMapper(role) {
|
||||||
|
return role.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setDefaultHeaders();
|
||||||
|
})
|
||||||
|
</script>
|
22
frontend/src/components/form/Textarea.svelte
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<div class:col-1={col1} class:col-2={col2} class:col-3={col3} class:col-4={col4}>
|
||||||
|
<label for="input" class="form-label">{label}</label>
|
||||||
|
<textarea id="input" class="form-input" placeholder="{placeholder}" bind:value on:change on:input></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let value;
|
||||||
|
export let label;
|
||||||
|
export let placeholder;
|
||||||
|
|
||||||
|
export let col1 = false;
|
||||||
|
export let col2 = false;
|
||||||
|
export let col3 = false;
|
||||||
|
export let col4 = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
</style>
|
45
frontend/src/components/form/UserSelect.svelte
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{#if label !== undefined}
|
||||||
|
<label class="form-label">{label}</label>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="multiselect-super">
|
||||||
|
<Select placeholder="Search..." loadOptionsInterval={500} {loadOptions} optionIdentifier="id"
|
||||||
|
bind:selectedValue={value} getOptionLabel={labelMapper} getSelectionLabel={labelMapper}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Select from 'svelte-select';
|
||||||
|
import axios from "axios";
|
||||||
|
import {onMount} from 'svelte'
|
||||||
|
import {setDefaultHeaders} from '../../includes/Auth.svelte'
|
||||||
|
import {API_URL} from "../../js/constants";
|
||||||
|
import {notifyError, notifyRatelimit} from "../../js/util";
|
||||||
|
|
||||||
|
export let label;
|
||||||
|
export let guildId;
|
||||||
|
|
||||||
|
export let value;
|
||||||
|
|
||||||
|
async function loadOptions(filterText) {
|
||||||
|
const res = await axios.get(`${API_URL}/api/${guildId}/members/search?query=${filterText}`)
|
||||||
|
if (res.status !== 200) {
|
||||||
|
if (res.status === 429) {
|
||||||
|
notifyRatelimit();
|
||||||
|
} else {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {cancelled: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data.map((m) => m.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelMapper(user) {
|
||||||
|
return `${user.username}#${user.discriminator}`
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setDefaultHeaders();
|
||||||
|
})
|
||||||
|
</script>
|
42
frontend/src/components/manage/AutoCloseCard.svelte
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<Card footer="{false}">
|
||||||
|
<span slot="title">
|
||||||
|
Auto Close
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div slot="body" class="body-wrapper">
|
||||||
|
<div class="alert danger">
|
||||||
|
<span class="alert-text">
|
||||||
|
<span>
|
||||||
|
This feature is currently disabled. Discord will soon be releasing message threads,
|
||||||
|
which will incorporate their own auto-close behaviour. Thank you for your patience.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.body-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
background-color: #fc727a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-text {
|
||||||
|
width: 95%
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Card from "../Card.svelte";
|
||||||
|
</script>
|
103
frontend/src/components/manage/ClaimsCard.svelte
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<Card footer="{false}" fill="{false}">
|
||||||
|
<span slot="title">
|
||||||
|
Claim Settings
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div slot="body" class="body-wrapper">
|
||||||
|
<form class="settings-form" on:submit|preventDefault={updateSettings}>
|
||||||
|
<div class="row">
|
||||||
|
<Checkbox label="SUPPORT REPS CAN VIEW CLAIMED TICKETS" col2=true bind:value={data.support_can_view} on:change={validateView} />
|
||||||
|
<Checkbox label="SUPPORT REPS CAN TYPE IN CLAIMED TICKETS" col2=true bind:value={data.support_can_type} on:change={validateType} />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-1">
|
||||||
|
<Button icon="fas fa-paper-plane" fullWidth=true>Submit</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let guildId;
|
||||||
|
|
||||||
|
import Card from "../Card.svelte";
|
||||||
|
import Checkbox from "../form/Checkbox.svelte";
|
||||||
|
|
||||||
|
import {setDefaultHeaders} from '../../includes/Auth.svelte'
|
||||||
|
import axios from "axios";
|
||||||
|
import {notifyError, notifySuccess, withLoadingScreen} from "../../js/util";
|
||||||
|
import {API_URL} from "../../js/constants";
|
||||||
|
import Button from "../Button.svelte";
|
||||||
|
|
||||||
|
setDefaultHeaders();
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
support_can_view: true,
|
||||||
|
support_can_type: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
function validateView() {
|
||||||
|
if (!data.support_can_view && data.support_can_type) {
|
||||||
|
data.support_can_type = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateType() {
|
||||||
|
if (!data.support_can_view && data.support_can_type) {
|
||||||
|
data.support_can_view = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSettings() {
|
||||||
|
const res = await axios.post(`${API_URL}/api/${guildId}/claimsettings`, data);
|
||||||
|
if (res.status === 200 && res.data.success) {
|
||||||
|
notifySuccess("Your settings have been saved.");
|
||||||
|
} else {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
const res = await axios.get(`${API_URL}/api/${guildId}/claimsettings`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
withLoadingScreen(async () => {
|
||||||
|
await loadData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.body-wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 900px) {
|
||||||
|
.row {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
75
frontend/src/components/manage/MultiPanelCreationForm.svelte
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<form on:submit|preventDefault>
|
||||||
|
<div class="row">
|
||||||
|
<Input col1={true} label="Panel Title" placeholder="Click to open a ticket" bind:value={data.title}/>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<Textarea col1={true} label="Panel Content" bind:value={data.content}
|
||||||
|
placeholder="Click on the button corresponding to the type of ticket you wish to open. Let users know which button responds to which category. You are able to use emojis here."/>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-1-3">
|
||||||
|
<Colour col1={true} label="Panel Colour" on:change={updateColour} bind:value={tempColour}/>
|
||||||
|
</div>
|
||||||
|
<div class="col-2-3">
|
||||||
|
<ChannelDropdown col1={true} {channels} label="Panel Channel" bind:value={data.channel_id}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-1">
|
||||||
|
<PanelDropdown label="Panels" bind:panels bind:selected={data.panels} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Input from "../form/Input.svelte";
|
||||||
|
import Textarea from "../form/Textarea.svelte";
|
||||||
|
import Colour from "../form/Colour.svelte";
|
||||||
|
import {colourToInt} from "../../js/util";
|
||||||
|
import ChannelDropdown from "../ChannelDropdown.svelte";
|
||||||
|
import PanelDropdown from "../PanelDropdown.svelte";
|
||||||
|
|
||||||
|
export let data = {};
|
||||||
|
|
||||||
|
export let guildId;
|
||||||
|
export let channels = [];
|
||||||
|
export let panels = [];
|
||||||
|
|
||||||
|
export let seedDefault = true;
|
||||||
|
if (seedDefault) {
|
||||||
|
data = {
|
||||||
|
colour: 0x7289da,
|
||||||
|
channels: channels[0].id,
|
||||||
|
panels: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tempColour = '#7289da';
|
||||||
|
|
||||||
|
function updateColour() {
|
||||||
|
data.colour = colourToInt(tempColour);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 950px) {
|
||||||
|
.row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
82
frontend/src/components/manage/MultiPanelEditModal.svelte
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<div class="modal" transition:fade>
|
||||||
|
<div class="modal-wrapper">
|
||||||
|
<Card footer="{true}" footerRight="{true}" fill="{false}">
|
||||||
|
<span slot="title">Edit Multi-Panel</span>
|
||||||
|
|
||||||
|
<div slot="body" class="body-wrapper">
|
||||||
|
<MultiPanelCreationForm {guildId} {channels} {panels} bind:data seedDefault={false}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div slot="footer">
|
||||||
|
<Button danger={true} on:click={dispatchClose}>Cancel</Button>
|
||||||
|
<div style="margin-left: 12px">
|
||||||
|
<Button icon="fas fa-paper-plane" on:click={dispatchConfirm}>Submit</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-backdrop" transition:fade>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {createEventDispatcher} from 'svelte';
|
||||||
|
import {fade} from 'svelte/transition'
|
||||||
|
import Card from "../Card.svelte";
|
||||||
|
import Button from "../Button.svelte";
|
||||||
|
import MultiPanelCreationForm from "./MultiPanelCreationForm.svelte";
|
||||||
|
|
||||||
|
export let guildId;
|
||||||
|
export let data;
|
||||||
|
export let channels = [];
|
||||||
|
export let panels = [];
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
function dispatchClose() {
|
||||||
|
dispatch('close', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch with data
|
||||||
|
function dispatchConfirm() {
|
||||||
|
dispatch('confirm', data);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 501;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 1280px) {
|
||||||
|
.modal-wrapper {
|
||||||
|
width: 96%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 500;
|
||||||
|
background-color: #000;
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
</style>
|
284
frontend/src/components/manage/PanelCreationForm.svelte
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
<form class="settings-form" on:submit|preventDefault>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-1-3">
|
||||||
|
<Input label="Panel Title" placeholder="Open a ticket!" col1=true bind:value={data.title}/>
|
||||||
|
</div>
|
||||||
|
<div class="col-2-3">
|
||||||
|
<Textarea col1=true label="Panel Content" placeholder="By clicking the button, a ticket will be opened for you."
|
||||||
|
bind:value={data.content}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<Colour col4=true label="Panel Colour" on:change={updateColour} bind:value={tempColour}/>
|
||||||
|
<ChannelDropdown label="Panel Channel" col4=true {channels} bind:value={data.channel_id}/>
|
||||||
|
<CategoryDropdown label="Ticket Category" col4=true {channels} bind:value={data.category_id}/>
|
||||||
|
<EmojiInput label="Button Emoji" col4=true bind:value={data.emote}/>
|
||||||
|
</div>
|
||||||
|
<div class="row" style="justify-content: center">
|
||||||
|
<div class="col-3">
|
||||||
|
<Button icon="fas fa-sliders-h" fullWidth=true type="button"
|
||||||
|
on:click={toggleAdvancedSettings}>Toggle Advanced Settings
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row advanced-settings" class:advanced-settings-show={advancedSettings}
|
||||||
|
class:advanced-settings-hide={!advancedSettings} class:show-overflow={overflowShow}>
|
||||||
|
<div class="inner" class:inner-show={advancedSettings}>
|
||||||
|
<div class="row">
|
||||||
|
<Textarea col1=true bind:value={data.welcome_message} label="Welcome Message"
|
||||||
|
placeholder="If blank, your server's default welcome message will be used"
|
||||||
|
on:input={handleWelcomeMessageUpdate}/>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-2">
|
||||||
|
<label class="form-label">Mention On Open</label>
|
||||||
|
<div class="multiselect-super">
|
||||||
|
<Select items={mentionValues} bind:selectedValue={mentionsRaw} on:select={updateMentions} isMulti={true}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<label class="form-label">Support Teams</label>
|
||||||
|
<div class="multiselect-super">
|
||||||
|
<Select items={teamsItems} bind:selectedValue={teamsRaw} on:select={updateTeams} isMulti={true}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Input from "../form/Input.svelte";
|
||||||
|
import Textarea from "../form/Textarea.svelte";
|
||||||
|
import Colour from "../form/Colour.svelte";
|
||||||
|
import Button from "../Button.svelte";
|
||||||
|
import ChannelDropdown from "../ChannelDropdown.svelte";
|
||||||
|
|
||||||
|
import {createEventDispatcher, onMount} from 'svelte';
|
||||||
|
import {colourToInt} from "../../js/util";
|
||||||
|
import {setDefaultHeaders} from "../../includes/Auth.svelte";
|
||||||
|
import CategoryDropdown from "../CategoryDropdown.svelte";
|
||||||
|
import EmojiInput from "../form/EmojiInput.svelte";
|
||||||
|
import Select from 'svelte-select';
|
||||||
|
|
||||||
|
export let guildId;
|
||||||
|
export let seedDefault = true;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let tempColour = '#2ECC71';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
if (seedDefault) {
|
||||||
|
data = {
|
||||||
|
//title: 'Open a ticket!',
|
||||||
|
//content: 'By clicking the button, a ticket will be opened for you.',
|
||||||
|
colour: 0x2ECC71,
|
||||||
|
emote: '📩',
|
||||||
|
welcome_message: null,
|
||||||
|
mentions: [],
|
||||||
|
default_team: true,
|
||||||
|
teams: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export let channels = [];
|
||||||
|
export let roles = [];
|
||||||
|
export let teams = [];
|
||||||
|
|
||||||
|
let advancedSettings = false;
|
||||||
|
let overflowShow = false;
|
||||||
|
|
||||||
|
// Oh my
|
||||||
|
// TODO: Clean up
|
||||||
|
let mentionValues = [{value: 'user', label: 'Ticket Opener'}];
|
||||||
|
let mentionsRaw = [];
|
||||||
|
|
||||||
|
function updateMentions() {
|
||||||
|
if (mentionsRaw === undefined) {
|
||||||
|
mentionsRaw = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
data.mentions = mentionsRaw.map((option) => option.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let teamsItems = [{value: 'default', label: 'Default'}];
|
||||||
|
let teamsRaw = [];
|
||||||
|
if (seedDefault) {
|
||||||
|
teamsRaw = [{value: 'default', label: 'Default'}];
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTeams() {
|
||||||
|
if (teamsRaw === undefined) {
|
||||||
|
data.teams = [];
|
||||||
|
} else {
|
||||||
|
data.default_team = teamsRaw.find((option) => option.value === 'default') !== undefined;
|
||||||
|
data.teams = teamsRaw
|
||||||
|
.filter((option) => option.value !== 'default')
|
||||||
|
.map((option) => teams.find((team) => team.id == option.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAdvancedSettings() {
|
||||||
|
advancedSettings = !advancedSettings;
|
||||||
|
if (advancedSettings) {
|
||||||
|
setTimeout(() => {
|
||||||
|
overflowShow = true;
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
overflowShow = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWelcomeMessageUpdate() {
|
||||||
|
if (data.welcome_message === "") {
|
||||||
|
data.welcome_message = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateColour() {
|
||||||
|
data.colour = colourToInt(tempColour);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMentionValues() {
|
||||||
|
mentionValues = [{value: 'user', label: 'Ticket Opener'}];
|
||||||
|
$: roles.forEach((role) => mentionValues.push({value: role.id, label: role.name}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTeamsItems() {
|
||||||
|
teamsItems = [{value: 'default', label: 'Default'}];
|
||||||
|
$: teams.forEach((team) => teamsItems.push({value: team.id, label: team.name}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyOverrides() {
|
||||||
|
if (data.default_team === true) {
|
||||||
|
$: teamsRaw.push({value: 'default', label: 'Default'});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.teams) {
|
||||||
|
$: data.teams.forEach((team) => teamsRaw.push({value: team.id.toString(), label: team.name}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.mentions) {
|
||||||
|
$: data.mentions.forEach((id) => mentionsRaw.push(mentionValues.find((val) => val.value === id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
updateMentionValues();
|
||||||
|
updateTeamsItems();
|
||||||
|
|
||||||
|
if (seedDefault) {
|
||||||
|
data.channel_id = channels.find((c) => c.type === 0).id;
|
||||||
|
data.category_id = channels.find((c) => c.type === 4).id;
|
||||||
|
} else {
|
||||||
|
applyOverrides();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.col-1-3) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 32%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.col-2-3) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 64%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 950px) {
|
||||||
|
.row {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.col-1-3, .col-2-3) {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings {
|
||||||
|
transition: min-height .3s ease-in-out, margin-top .3s ease-in-out, margin-bottom .3s ease-in-out;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings-hide {
|
||||||
|
height: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
flex: unset;
|
||||||
|
min-height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-settings-show {
|
||||||
|
visibility: visible;
|
||||||
|
min-height: 250px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-overflow {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.multiselect-super) {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
--background: #2e3136;
|
||||||
|
--border: #2e3136;
|
||||||
|
--borderRadius: 4px;
|
||||||
|
--itemHoverBG: #121212;
|
||||||
|
--listBackground: #2e3136;
|
||||||
|
--itemColor: white;
|
||||||
|
--multiItemBG: #272727;
|
||||||
|
--multiItemActiveBG: #272727;
|
||||||
|
--multiClearFill: #272727;
|
||||||
|
--multiClearHoverFill: #272727;
|
||||||
|
--inputColor: white;
|
||||||
|
--inputFontSize: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.multiselect-super > .selectContainer) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.selectContainer > .multiSelect, .selectContainer > .multiSelect > input) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
83
frontend/src/components/manage/PanelEditModal.svelte
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<div class="modal" transition:fade>
|
||||||
|
<div class="modal-wrapper">
|
||||||
|
<Card footer="{true}" footerRight="{true}" fill="{false}">
|
||||||
|
<span slot="title">Edit Panel</span>
|
||||||
|
|
||||||
|
<div slot="body" class="body-wrapper">
|
||||||
|
<PanelCreationForm {guildId} {channels} {roles} {teams} bind:data={panel} seedDefault={false} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div slot="footer">
|
||||||
|
<Button danger={true} on:click={dispatchClose}>Cancel</Button>
|
||||||
|
<div style="margin-left: 12px">
|
||||||
|
<Button icon="fas fa-paper-plane" on:click={dispatchConfirm}>Submit</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-backdrop" transition:fade>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {createEventDispatcher} from 'svelte';
|
||||||
|
import {fade} from 'svelte/transition'
|
||||||
|
import PanelCreationForm from "./PanelCreationForm.svelte";
|
||||||
|
import Card from "../Card.svelte";
|
||||||
|
import Button from "../Button.svelte";
|
||||||
|
|
||||||
|
export let guildId;
|
||||||
|
export let panel = {};
|
||||||
|
export let channels = [];
|
||||||
|
export let roles = [];
|
||||||
|
export let teams = [];
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
function dispatchClose() {
|
||||||
|
dispatch('close', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch with data
|
||||||
|
function dispatchConfirm() {
|
||||||
|
dispatch('confirm', panel);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 501;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 1280px) {
|
||||||
|
.modal-wrapper {
|
||||||
|
width: 96%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 500;
|
||||||
|
background-color: #000;
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
</style>
|
216
frontend/src/components/manage/SettingsCard.svelte
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
<Card footer="{false}" fill="{false}">
|
||||||
|
<span slot="title">
|
||||||
|
Settings
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div slot="body" class="body-wrapper">
|
||||||
|
<form class="settings-form" on:submit|preventDefault={updateSettings}>
|
||||||
|
<div class="row">
|
||||||
|
<Input label="prefix (max len. 8)" placeholder="t!" col4=true bind:value={data.prefix} />
|
||||||
|
<Number label="per user ticket limit" col4=true min=1 max=10 bind:value={data.ticket_limit} />
|
||||||
|
<Checkbox label="allow users to close tickets" col4=true bind:value={data.users_can_close}/>
|
||||||
|
<Checkbox label="ticket close confirmation" col4=true bind:value={data.close_confirmation}/>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<Textarea label="welcome message" placeholder="Thanks for opening a ticket!" col1=true bind:value={data.welcome_message} />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<ChannelDropdown label="Archive Channel" col2=true channels={channels} bind:value={data.archive_channel} />
|
||||||
|
<CategoryDropdown label="Channel Category" col2=true channels={channels} bind:value={data.category} />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<NamingScheme col4=true bind:value={data.naming_scheme} />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-1">
|
||||||
|
<Button icon="fas fa-paper-plane" fullWidth=true>Submit</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ChannelDropdown from "../ChannelDropdown.svelte";
|
||||||
|
|
||||||
|
export let guildId;
|
||||||
|
|
||||||
|
import Card from "../Card.svelte";
|
||||||
|
import Input from "../form/Input.svelte";
|
||||||
|
import Number from "../form/Number.svelte";
|
||||||
|
import Checkbox from "../form/Checkbox.svelte";
|
||||||
|
import Textarea from "../form/Textarea.svelte";
|
||||||
|
|
||||||
|
import {setDefaultHeaders} from '../../includes/Auth.svelte'
|
||||||
|
import axios from "axios";
|
||||||
|
import {notify, notifyError, notifySuccess, withLoadingScreen} from "../../js/util";
|
||||||
|
import {API_URL} from "../../js/constants";
|
||||||
|
import CategoryDropdown from "../CategoryDropdown.svelte";
|
||||||
|
import Button from "../Button.svelte";
|
||||||
|
import NamingScheme from "../NamingScheme.svelte";
|
||||||
|
|
||||||
|
setDefaultHeaders();
|
||||||
|
|
||||||
|
let channels = [];
|
||||||
|
|
||||||
|
async function loadChannels() {
|
||||||
|
const res = await axios.get(`${API_URL}/api/${guildId}/channels`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
channels = res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
ticket_limit: 5,
|
||||||
|
users_can_close: true,
|
||||||
|
close_confirmation: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function updateSettings() {
|
||||||
|
const res = await axios.post(`${API_URL}/api/${guildId}/settings`, data);
|
||||||
|
if (res.status === 200) {
|
||||||
|
if (showValidations(res.data)) {
|
||||||
|
notifySuccess('Your settings have been saved.');
|
||||||
|
} else {
|
||||||
|
// Load valid data
|
||||||
|
await loadData();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showValidations(data) {
|
||||||
|
let success = true;
|
||||||
|
|
||||||
|
if (!data.prefix) {
|
||||||
|
success = false;
|
||||||
|
notify("Warning", "Your prefix has not been saved.\nPrefixes must be between 1 - 8 characters in length.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.welcome_message) {
|
||||||
|
success = false;
|
||||||
|
notify("Warning", "Your welcome message has not been saved.\nWelcome messages must be between 1 - 1000 characters in length.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.ticket_limit) {
|
||||||
|
success = false;
|
||||||
|
notify("Warning", "Your ticket limit has not been saved.\nTicket limits must be in the range 1 - 10.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.archive_channel) {
|
||||||
|
success = false;
|
||||||
|
notify("Warning", "Your archive channel has not been saved.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.category) {
|
||||||
|
success = false;
|
||||||
|
notify("Warning", "Your channel category has not been saved.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.naming_scheme) {
|
||||||
|
success = false;
|
||||||
|
notify("Warning", "Your archive channel has not been saved.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
const res = await axios.get(`${API_URL}/api/${guildId}/settings`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = res.data;
|
||||||
|
|
||||||
|
// Overrides
|
||||||
|
if (data.archive_channel === "0") {
|
||||||
|
let first = channels.find((c) => c.type === 0);
|
||||||
|
if (first !== undefined) {
|
||||||
|
data.archive_channel = first.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.category === "0") {
|
||||||
|
let first = channels.find((c) => c.type === 4);
|
||||||
|
if (first !== undefined) {
|
||||||
|
data.category = first.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
withLoadingScreen(async () => {
|
||||||
|
await loadChannels();
|
||||||
|
await loadData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.body-wrapper) {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 950px) {
|
||||||
|
.row {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.col-4, .col-3, .col-2) {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.col-1) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.col-2) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 49%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.col-3) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 31%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.col-4) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 23%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
49
frontend/src/includes/Auth.svelte
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<script context="module">
|
||||||
|
import axios from 'axios';
|
||||||
|
import {API_URL, OAUTH} from "../js/constants";
|
||||||
|
|
||||||
|
const _tokenKey = 'token';
|
||||||
|
|
||||||
|
export function getToken() {
|
||||||
|
let token = window.localStorage.getItem(_tokenKey);
|
||||||
|
if (token == null) {
|
||||||
|
redirectLogin();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setToken(token) {
|
||||||
|
window.localStorage.setItem(_tokenKey, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redirectLogin() {
|
||||||
|
// TODO: State
|
||||||
|
window.location.href = `https://discordapp.com/oauth2/authorize?response_type=code&redirect_uri=${OAUTH.redirectUri}&scope=identify%20guilds&client_id=${OAUTH.clientId}&state=`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearLocalStorage() {
|
||||||
|
window.localStorage.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setDefaultHeaders() {
|
||||||
|
axios.defaults.headers.common['Authorization'] = getToken();
|
||||||
|
axios.defaults.headers.common['x-tickets'] = 'true'; // arbitrary header name and value
|
||||||
|
axios.defaults.validateStatus = (s) => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRefreshInterceptor() {
|
||||||
|
axios.interceptors.response.use(async (res) => { // we set validateStatus to false
|
||||||
|
if (res.status === 401) {
|
||||||
|
await _refreshToken();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}, async (err) => {
|
||||||
|
if (err.response.status === 401) {
|
||||||
|
await _refreshToken();
|
||||||
|
}
|
||||||
|
return err.response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
27
frontend/src/includes/Head.svelte
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<svelte:head>
|
||||||
|
<title>Tickets | A Discord Support Manager Bot</title>
|
||||||
|
|
||||||
|
<!-- Meta -->
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="description" content="Management panel for the Discord Tickets bot">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/assets/img/favicon.ico" type="image/x-icon">
|
||||||
|
<link rel="icon" href="/assets/img/favicon.ico" type="image/x-icon">
|
||||||
|
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<!--<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700" rel="stylesheet">-->
|
||||||
|
|
||||||
|
<!-- Icons -->
|
||||||
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css">
|
||||||
|
|
||||||
|
<!-- GA -->
|
||||||
|
<script async src='https://www.google-analytics.com/analytics.js'
|
||||||
|
type="80be96f83bbfbba3d4097e23-text/javascript"></script>
|
||||||
|
|
||||||
|
<script type="80be96f83bbfbba3d4097e23-text/javascript">
|
||||||
|
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
|
||||||
|
ga('create', 'UA-161945537-1', 'auto');
|
||||||
|
ga('send', 'pageview');
|
||||||
|
</script>
|
||||||
|
</svelte:head>
|
85
frontend/src/includes/LoadingScreen.svelte
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
{#if $loadingScreen}
|
||||||
|
<div id="loading-container" out:fade>
|
||||||
|
<div class="loader">Loading...</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {loadingScreen} from '../js/stores'
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#loading-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #121212;
|
||||||
|
z-index: 1001;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader,
|
||||||
|
.loader:before,
|
||||||
|
.loader:after {
|
||||||
|
background: #ffffff;
|
||||||
|
-webkit-animation: load1 1s infinite ease-in-out;
|
||||||
|
animation: load1 1s infinite ease-in-out;
|
||||||
|
width: 1em;
|
||||||
|
height: 4em;
|
||||||
|
}
|
||||||
|
.loader {
|
||||||
|
color: #ffffff;
|
||||||
|
text-indent: -9999em;
|
||||||
|
margin: 88px auto;
|
||||||
|
position: relative;
|
||||||
|
font-size: 11px;
|
||||||
|
-webkit-transform: translateZ(0);
|
||||||
|
-ms-transform: translateZ(0);
|
||||||
|
transform: translateZ(0);
|
||||||
|
-webkit-animation-delay: -0.16s;
|
||||||
|
animation-delay: -0.16s;
|
||||||
|
}
|
||||||
|
.loader:before,
|
||||||
|
.loader:after {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
.loader:before {
|
||||||
|
left: -1.5em;
|
||||||
|
-webkit-animation-delay: -0.32s;
|
||||||
|
animation-delay: -0.32s;
|
||||||
|
}
|
||||||
|
.loader:after {
|
||||||
|
left: 1.5em;
|
||||||
|
}
|
||||||
|
@-webkit-keyframes load1 {
|
||||||
|
0%,
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0;
|
||||||
|
height: 4em;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
box-shadow: 0 -2em;
|
||||||
|
height: 5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes load1 {
|
||||||
|
0%,
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0;
|
||||||
|
height: 4em;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
box-shadow: 0 -2em;
|
||||||
|
height: 5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
115
frontend/src/includes/Navbar.svelte
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<div class="navbar" class:dropdown>
|
||||||
|
<div class="wrapper" class:dropdown>
|
||||||
|
<div>
|
||||||
|
<div class="burger-menu">
|
||||||
|
<NavElement icon="fas fa-bars" on:click={dropdownNav}>Menu</NavElement>
|
||||||
|
</div>
|
||||||
|
<div class="nav-section" class:dropdown>
|
||||||
|
<!-- on:click required to close dropdown again -->
|
||||||
|
<NavElement icon="fas fa-cogs" link="/manage/{guildId}/settings" on:click={closeDropdown}>Settings</NavElement>
|
||||||
|
<NavElement icon="fas fa-copy" link="/manage/{guildId}/transcripts" on:click={closeDropdown}>Transcripts</NavElement>
|
||||||
|
<NavElement icon="fas fa-mouse-pointer" link="/manage/{guildId}/panels" on:click={closeDropdown}>Reaction Panels</NavElement>
|
||||||
|
<NavElement icon="fas fa-users" link="/manage/{guildId}/teams" on:click={closeDropdown}>Teams</NavElement>
|
||||||
|
<NavElement icon="fas fa-ticket-alt" link="/manage/{guildId}/tickets" on:click={closeDropdown}>Tickets</NavElement>
|
||||||
|
<NavElement icon="fas fa-ban" link="/manage/{guildId}/blacklist" on:click={closeDropdown}>Blacklist</NavElement>
|
||||||
|
<NavElement icon="fas fa-tags" link="/manage/{guildId}/tags" on:click={closeDropdown}>Tags</NavElement>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="nav-section" class:dropdown>
|
||||||
|
<NavElement icon="fas fa-server" link="/#">Servers</NavElement>
|
||||||
|
<NavElement icon="fas fa-sign-out-alt" link="/logout">Logout</NavElement>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let guildId;
|
||||||
|
export let dropdown = false;
|
||||||
|
|
||||||
|
import NavElement from "../components/NavElement.svelte";
|
||||||
|
|
||||||
|
function dropdownNav() {
|
||||||
|
dropdown = !dropdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDropdown() {
|
||||||
|
dropdown = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #272727;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 98%;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 950px) {
|
||||||
|
.nav-section {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger-menu {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-section.dropdown {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
transition-property: height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
position: relative;
|
||||||
|
height: 49px;
|
||||||
|
transition: all .3s ease-in-out;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar.dropdown, .wrapper.dropdown {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper.dropdown {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.nav-element) {
|
||||||
|
padding: 15px 0 10px 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.super-container.dropdown) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
96
frontend/src/includes/NotifyModal.svelte
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
{#if $notifyModal}
|
||||||
|
<div class="modal" transition:fade>
|
||||||
|
<div class="modal-wrapper" bind:this={wrapper}>
|
||||||
|
<Card footer="{true}" footerRight="{true}" fill="{false}">
|
||||||
|
<span slot="title">{$notifyTitle}</span>
|
||||||
|
|
||||||
|
<span slot="body">{$notifyMessage}</span>
|
||||||
|
|
||||||
|
<div slot="footer">
|
||||||
|
<Button on:click={closeNotificationModal}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-backdrop" transition:fade>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {notifyMessage, notifyModal, notifyTitle} from "../js/stores";
|
||||||
|
import {closeNotificationModal} from "../js/util";
|
||||||
|
import {fade} from 'svelte/transition'
|
||||||
|
import Card from '../components/Card.svelte'
|
||||||
|
import Button from '../components/Button.svelte'
|
||||||
|
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!notifyModal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current = e.target;
|
||||||
|
let wrapperFound = false;
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
if (current.attributes) {
|
||||||
|
if (current.hasAttribute('istrigger')) {
|
||||||
|
wrapperFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current === wrapper) {
|
||||||
|
wrapperFound = true;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
current = current.parentNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wrapperFound) {
|
||||||
|
closeNotificationModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 999;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 998;
|
||||||
|
background-color: #000;
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
168
frontend/src/includes/Sidebar.svelte
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<script>
|
||||||
|
export let referralShow;
|
||||||
|
export let referralLink;
|
||||||
|
|
||||||
|
export let name;
|
||||||
|
|
||||||
|
import {Navigate} from 'svelte-router-spa'
|
||||||
|
|
||||||
|
export let avatar;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-container" id="sidebar-nav">
|
||||||
|
<div class="inner">
|
||||||
|
<Navigate to="/" styles="sidebar-link">
|
||||||
|
<div class="sidebar-element">
|
||||||
|
<i class="fas fa-server sidebar-icon"></i>
|
||||||
|
<span class="sidebar-text">Servers</span>
|
||||||
|
</div>
|
||||||
|
</Navigate>
|
||||||
|
<Navigate to="/whitelabel" styles="sidebar-link">
|
||||||
|
<div class="sidebar-element">
|
||||||
|
<i class="fas fa-edit sidebar-icon"></i>
|
||||||
|
<span class="sidebar-text">Whitelabel</span>
|
||||||
|
</div>
|
||||||
|
</Navigate>
|
||||||
|
|
||||||
|
{#if referralShow}
|
||||||
|
<div class="sidebar-element ref">
|
||||||
|
<a class="ref-wrapper" href="{referralLink}">
|
||||||
|
<img src="/assets/img/custom.png" id="custom-image" alt="Ref Logo"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-container">
|
||||||
|
<div class="sidebar-element">
|
||||||
|
<Navigate to="/logout" onclick="clearLocalStorage();" styles="sidebar-link">
|
||||||
|
<i class="sidebar-icon fas fa-sign-out-alt sidebar-icon"></i>
|
||||||
|
<span class="sidebar-text">Logout</span>
|
||||||
|
</Navigate>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-element user-element">
|
||||||
|
<a class="sidebar-link">
|
||||||
|
<i id="avatar-sidebar" style="background: url('{avatar}') center center;"></i>
|
||||||
|
{#if name !== undefined}
|
||||||
|
<span class="sidebar-text">{name}</span>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 16.6%;
|
||||||
|
background-color: #272727;
|
||||||
|
float: left;
|
||||||
|
background-size: cover;
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-container {
|
||||||
|
margin-bottom: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-element {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 5px 0 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-element:hover {
|
||||||
|
background-color: #121212;
|
||||||
|
transition: background-color 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#custom-image {
|
||||||
|
max-height: 70px;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.sidebar-link) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: white !important;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-left: 4%;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-text {
|
||||||
|
margin-left: 4%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar-nav {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ref {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ref-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin: 0 !important
|
||||||
|
}
|
||||||
|
|
||||||
|
#avatar-sidebar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: block;
|
||||||
|
background-size: cover !important;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 950px) {
|
||||||
|
.sidebar {
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
height: unset;
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ref {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-container {
|
||||||
|
margin-bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-element {
|
||||||
|
width: unset;
|
||||||
|
padding: 20px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link {
|
||||||
|
width: unset;
|
||||||
|
margin-left: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-element {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
7
frontend/src/js/constants.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const API_URL = env.API_URL || "http://172.26.50.75:3000"
|
||||||
|
export const PLACEHOLDER_DOCS_URL = "https://docs.ticketsbot.net/setup/placeholders.html"
|
||||||
|
|
||||||
|
export const OAUTH = {
|
||||||
|
clientId: env.CLIENT_ID || "700742994386747404",
|
||||||
|
redirectUri: env.REDIRECT_URI || "http://localhost:5000/callback"
|
||||||
|
}
|
24
frontend/src/js/stores.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {get, writable} from "svelte/store";
|
||||||
|
|
||||||
|
const loadingCount = writable(0);
|
||||||
|
export const loadingScreen = writable(true);
|
||||||
|
export const dropdown = writable(false);
|
||||||
|
|
||||||
|
export function addLoadingScreenTicket() {
|
||||||
|
loadingCount.update(n => n + 1);
|
||||||
|
loadingScreen.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeLoadingScreenTicket() {
|
||||||
|
loadingCount.update(n => n - 1);
|
||||||
|
if (get(loadingCount) === 0) {
|
||||||
|
loadingScreen.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const notifyModal = writable(false);
|
||||||
|
export const notifyTitle = writable("");
|
||||||
|
export const notifyMessage = writable("");
|
||||||
|
|
||||||
|
export const isErrorPage = writable(false);
|
||||||
|
export const errorMessage = writable("");
|
42
frontend/src/js/util.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import * as Stores from './stores'
|
||||||
|
import {navigateTo} from "svelte-router-spa";
|
||||||
|
|
||||||
|
export async function withLoadingScreen(func) {
|
||||||
|
Stores.addLoadingScreenTicket();
|
||||||
|
await func();
|
||||||
|
Stores.removeLoadingScreenTicket();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function errorPage(message) {
|
||||||
|
navigateTo(`/error?message=${encodeURIComponent(message)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notify(title, message) {
|
||||||
|
Stores.notifyTitle.set(title);
|
||||||
|
Stores.notifyMessage.set(message);
|
||||||
|
Stores.notifyModal.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notifyError(message) {
|
||||||
|
notify('Error', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notifySuccess(message) {
|
||||||
|
notify('Success', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notifyRatelimit() {
|
||||||
|
notifyError("You're doing that too fast: please wait a few seconds and try again");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeNotificationModal() {
|
||||||
|
Stores.notifyModal.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colourToInt(colour) {
|
||||||
|
return parseInt(`0x${colour.slice(1)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function intToColour(i) {
|
||||||
|
return `#${i.toString(16)}`
|
||||||
|
}
|
73
frontend/src/layouts/ErrorPage.svelte
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<div class="wrapper">
|
||||||
|
<div class="card-wrapper">
|
||||||
|
<Card footer=true footerRight=true>
|
||||||
|
<span slot="title">
|
||||||
|
Error
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div slot="body">
|
||||||
|
<Route {currentRoute} {params}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span slot="footer">
|
||||||
|
<span class="buttons">
|
||||||
|
<span style="margin-right: 20px">
|
||||||
|
<Button icon="fas fa-arrow-left" on:click={back}>Back</Button>
|
||||||
|
</span>
|
||||||
|
<Button icon="fas fa-home" on:click={home}>Home</Button>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(body) {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
background-color: #121212;
|
||||||
|
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 50%;
|
||||||
|
height: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Head from '../includes/Head.svelte'
|
||||||
|
import Card from '../components/Card.svelte'
|
||||||
|
import Button from "../components/Button.svelte";
|
||||||
|
|
||||||
|
import {Route} from 'svelte-router-spa'
|
||||||
|
|
||||||
|
export let currentRoute;
|
||||||
|
export let params = {};
|
||||||
|
|
||||||
|
function back() {
|
||||||
|
window.history.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
function home() {
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
</script>
|
87
frontend/src/layouts/IndexLayout.svelte
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<Head/>
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<Sidebar referralShow=true referralLink="https://www.digitalocean.com?refcode=371f56712ea4" name="{name}"
|
||||||
|
avatar="{avatar}"/>
|
||||||
|
<div class="super-container">
|
||||||
|
<LoadingScreen/>
|
||||||
|
<NotifyModal/>
|
||||||
|
<div class="content-container" class:hide={$loadingScreen}>
|
||||||
|
<Route {currentRoute} {params}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {Route} from 'svelte-router-spa'
|
||||||
|
import Head from '../includes/Head.svelte'
|
||||||
|
import Sidebar from '../includes/Sidebar.svelte'
|
||||||
|
import LoadingScreen from '../includes/LoadingScreen.svelte'
|
||||||
|
import NotifyModal from '../includes/NotifyModal.svelte'
|
||||||
|
import axios from "axios";
|
||||||
|
import {API_URL} from '../js/constants'
|
||||||
|
import {notifyError} from '../js/util'
|
||||||
|
import {loadingScreen} from "../js/stores"
|
||||||
|
import {redirectLogin, setDefaultHeaders} from '../includes/Auth.svelte'
|
||||||
|
|
||||||
|
export let currentRoute;
|
||||||
|
export let params = {};
|
||||||
|
|
||||||
|
setDefaultHeaders()
|
||||||
|
|
||||||
|
let name;
|
||||||
|
let avatar;
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
const res = await axios.get(`${API_URL}/api/session`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
if (res.data.auth === true) {
|
||||||
|
redirectLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
name = res.data.username;
|
||||||
|
avatar = res.data.avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.super-container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 950px) {
|
||||||
|
.wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
50
frontend/src/layouts/ManageLayout.svelte
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<Head/>
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<Navbar guildId={guildId} bind:dropdown={$dropdown}/>
|
||||||
|
<div class="super-container" class:dropdown={$dropdown}>
|
||||||
|
<LoadingScreen/>
|
||||||
|
<div class="content-container" class:hide={$loadingScreen}>
|
||||||
|
<Route {currentRoute} {params}/>
|
||||||
|
</div>
|
||||||
|
<NotifyModal/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let currentRoute;
|
||||||
|
export let params = {};
|
||||||
|
|
||||||
|
let guildId = currentRoute.namedParams.id
|
||||||
|
|
||||||
|
import Head from '../includes/Head.svelte'
|
||||||
|
import LoadingScreen from '../includes/LoadingScreen.svelte'
|
||||||
|
import NotifyModal from '../includes/NotifyModal.svelte'
|
||||||
|
import Navbar from '../includes/Navbar.svelte'
|
||||||
|
|
||||||
|
import {Route} from 'svelte-router-spa'
|
||||||
|
import {loadingScreen, dropdown} from '../js/stores'
|
||||||
|
</script>
|
19
frontend/src/layouts/TranscriptViewLayout.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<Head/>
|
||||||
|
|
||||||
|
<NotifyModal/>
|
||||||
|
<Route {currentRoute} {params}/>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let currentRoute;
|
||||||
|
export let params = {};
|
||||||
|
|
||||||
|
import Head from '../includes/Head.svelte'
|
||||||
|
import NotifyModal from '../includes/NotifyModal.svelte'
|
||||||
|
import {Route} from 'svelte-router-spa'
|
||||||
|
</script>
|
7
frontend/src/main.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import App from './App.svelte';
|
||||||
|
|
||||||
|
const app = new App({
|
||||||
|
target: document.body
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
83
frontend/src/routes.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import IndexLayout from './layouts/IndexLayout.svelte'
|
||||||
|
import ManageLayout from './layouts/ManageLayout.svelte'
|
||||||
|
import ErrorLayout from './layouts/ErrorPage.svelte'
|
||||||
|
import TranscriptViewLayout from './layouts/TranscriptViewLayout.svelte'
|
||||||
|
|
||||||
|
import Index from './views/Index.svelte'
|
||||||
|
import LoginCallback from './views/LoginCallback.svelte'
|
||||||
|
import Login from './views/Login.svelte'
|
||||||
|
import Logout from './views/Logout.svelte'
|
||||||
|
import Whitelabel from './views/Whitelabel.svelte'
|
||||||
|
import Settings from './views/Settings.svelte'
|
||||||
|
import Error from './views/Error.svelte'
|
||||||
|
import Error404 from './views/Error404.svelte'
|
||||||
|
import Transcripts from './views/Transcripts.svelte'
|
||||||
|
import TranscriptView from './views/TranscriptView.svelte'
|
||||||
|
import Blacklist from './views/Blacklist.svelte'
|
||||||
|
import Panels from './views/Panels.svelte'
|
||||||
|
import Tags from './views/Tags.svelte'
|
||||||
|
import Teams from './views/Teams.svelte'
|
||||||
|
import Tickets from './views/Tickets.svelte'
|
||||||
|
import TicketView from './views/TicketView.svelte'
|
||||||
|
|
||||||
|
export const routes = [
|
||||||
|
{name: '/', component: Index, layout: IndexLayout},
|
||||||
|
{name: '404', path: '404', component: Error404, layout: ErrorLayout},
|
||||||
|
{name: '/callback', component: LoginCallback},
|
||||||
|
{name: '/login', component: Login},
|
||||||
|
{name: '/logout', component: Logout},
|
||||||
|
{name: '/error', component: Error, layout: ErrorLayout},
|
||||||
|
{name: '/whitelabel', component: Whitelabel, layout: IndexLayout},
|
||||||
|
{
|
||||||
|
name: 'manage/:id',
|
||||||
|
nestedRoutes: [
|
||||||
|
{name: 'index', component: Error404, layout: ErrorLayout},
|
||||||
|
{name: 'settings', component: Settings, layout: ManageLayout},
|
||||||
|
{
|
||||||
|
name: 'transcripts',
|
||||||
|
nestedRoutes: [
|
||||||
|
{
|
||||||
|
name: 'index',
|
||||||
|
component: Transcripts,
|
||||||
|
layout: ManageLayout,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'view/:ticketid',
|
||||||
|
component: TranscriptView, // just to test
|
||||||
|
layout: TranscriptViewLayout,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Backwards compatibility
|
||||||
|
{
|
||||||
|
name: 'logs',
|
||||||
|
nestedRoutes: [
|
||||||
|
{
|
||||||
|
name: 'view/:ticketid',
|
||||||
|
component: TranscriptView,
|
||||||
|
layout: TranscriptViewLayout,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{name: 'panels', component: Panels, layout: ManageLayout},
|
||||||
|
{name: 'blacklist', component: Blacklist, layout: ManageLayout},
|
||||||
|
{name: 'tags', component: Tags, layout: ManageLayout},
|
||||||
|
{name: 'teams', component: Teams, layout: ManageLayout},
|
||||||
|
{
|
||||||
|
name: 'tickets',
|
||||||
|
nestedRoutes: [
|
||||||
|
{
|
||||||
|
name: 'index',
|
||||||
|
component: Tickets,
|
||||||
|
layout: ManageLayout,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'view/:ticketid',
|
||||||
|
component: TicketView,
|
||||||
|
layout: ManageLayout,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
172
frontend/src/views/Blacklist.svelte
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
<div class="parent">
|
||||||
|
<div class="content">
|
||||||
|
<div class="main-col">
|
||||||
|
<Card footer={false}>
|
||||||
|
<span slot="title">Blacklisted Users</span>
|
||||||
|
<div slot="body" class="body-wrapper">
|
||||||
|
<table class="nice">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>User ID</th>
|
||||||
|
<th>Remove</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each blacklistedUsers as user}
|
||||||
|
<tr>
|
||||||
|
{#if user.username !== '' && user.discriminator !== ''}
|
||||||
|
<td>{user.username}#{user.discriminator}</td>
|
||||||
|
{:else}
|
||||||
|
<td>Unknown</td>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<td>{user.id}</td>
|
||||||
|
<td>
|
||||||
|
<Button type="button" on:click={() => removeBlacklist(user)}>Remove</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div class="right-col">
|
||||||
|
<Card footer={false}>
|
||||||
|
<span slot="title">Blacklist A User</span>
|
||||||
|
<div slot="body" class="body-wrapper">
|
||||||
|
<form class="body-wrapper" on:submit|preventDefault={addBlacklist}>
|
||||||
|
<div class="row" style="flex-direction: column">
|
||||||
|
<UserSelect {guildId} label="User" bind:value={addUser}/>
|
||||||
|
</div>
|
||||||
|
<div class="row" style="justify-content: center">
|
||||||
|
<div class="col-2">
|
||||||
|
<Button fullWidth={true} icon="fas fa-plus"
|
||||||
|
disabled={addUser === undefined || addUser === ''}>Blacklist</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Card from "../components/Card.svelte";
|
||||||
|
import UserSelect from "../components/form/UserSelect.svelte";
|
||||||
|
import {notifyError, notifySuccess, withLoadingScreen} from '../js/util'
|
||||||
|
import Button from "../components/Button.svelte";
|
||||||
|
import axios from "axios";
|
||||||
|
import {API_URL} from "../js/constants";
|
||||||
|
import {setDefaultHeaders} from '../includes/Auth.svelte'
|
||||||
|
|
||||||
|
export let currentRoute;
|
||||||
|
let guildId = currentRoute.namedParams.id;
|
||||||
|
|
||||||
|
let addUser;
|
||||||
|
let blacklistedUsers = [];
|
||||||
|
|
||||||
|
async function addBlacklist() {
|
||||||
|
const res = await axios.post(`${API_URL}/api/${guildId}/blacklist/${addUser.id}`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifySuccess(`${addUser.username}#${addUser.discriminator} has been blacklisted`);
|
||||||
|
blacklistedUsers = [...blacklistedUsers, {
|
||||||
|
id: addUser.id,
|
||||||
|
username: addUser.username,
|
||||||
|
discriminator: addUser.discriminator,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeBlacklist(user) {
|
||||||
|
const res = await axios.delete(`${API_URL}/api/${guildId}/blacklist/${user.id}`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifySuccess(`${user.username}#${user.discriminator} has been removed from the blacklist`);
|
||||||
|
blacklistedUsers = blacklistedUsers.filter((u) => u.id !== user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUsers() {
|
||||||
|
const res = await axios.get(`${API_URL}/api/${guildId}/blacklist`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
blacklistedUsers = res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
withLoadingScreen(async () => {
|
||||||
|
setDefaultHeaders();
|
||||||
|
await loadUsers();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.parent {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 96%;
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 64%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 34%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin-bottom: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 950px) {
|
||||||
|
.content {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-col {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 4%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-col {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
6
frontend/src/views/Error.svelte
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{message}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const message = urlParams.get('message');
|
||||||
|
</script>
|