Integrations
This commit is contained in:
parent
0a2eb1a130
commit
145ebf30ea
96
app/http/endpoints/api/integrations/activateintegration.go
Normal file
96
app/http/endpoints/api/integrations/activateintegration.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type activateIntegrationBody struct {
|
||||||
|
Secrets map[string]string `json:"secrets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ActivateIntegrationHandler(ctx *gin.Context) {
|
||||||
|
userId := ctx.Keys["userid"].(uint64)
|
||||||
|
guildId := ctx.Keys["guildid"].(uint64)
|
||||||
|
|
||||||
|
activeCount, err := dbclient.Client.CustomIntegrationGuilds.GetGuildIntegrationCount(guildId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if activeCount >= 5 {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("You can only have 5 integrations active at once"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
integrationId, err := strconv.Atoi(ctx.Param("integrationid"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Invalid integration ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data activateIntegrationBody
|
||||||
|
if err := ctx.BindJSON(&data); err != nil {
|
||||||
|
ctx.JSON(400, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the integration is public or the user created it
|
||||||
|
canActivate, err := dbclient.Client.CustomIntegrationGuilds.CanActivate(integrationId, userId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !canActivate {
|
||||||
|
ctx.JSON(403, utils.ErrorStr("You do not have permission to activate this integration"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the secret values are valid
|
||||||
|
secrets, err := dbclient.Client.CustomIntegrationSecrets.GetByIntegration(integrationId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(secrets) != len(data.Secrets) {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Invalid secret values"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we've checked the length, we can just iterate over the secrets and they're guaranteed to be correct
|
||||||
|
secretMap := make(map[int]string)
|
||||||
|
for secretName, value := range data.Secrets {
|
||||||
|
if len(value) == 0 || len(value) > 255 {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Secret values must be between 1 and 255 characters"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
|
||||||
|
inner:
|
||||||
|
for _, secret := range secrets {
|
||||||
|
if secret.Name == secretName {
|
||||||
|
found = true
|
||||||
|
secretMap[secret.Id] = value
|
||||||
|
break inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Invalid secret values"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbclient.Client.CustomIntegrationGuilds.AddToGuildWithSecrets(integrationId, guildId, secretMap); err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(204)
|
||||||
|
}
|
130
app/http/endpoints/api/integrations/createintegration.go
Normal file
130
app/http/endpoints/api/integrations/createintegration.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/TicketsBot/database"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type integrationCreateBody struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=32"`
|
||||||
|
Description string `json:"description" validate:"required,min=1,max=255"`
|
||||||
|
ImageUrl *string `json:"image_url" validate:"omitempty,url,max=255,startswith=https://"`
|
||||||
|
PrivacyPolicyUrl *string `json:"privacy_policy_url" validate:"omitempty,url,max=255,startswith=https://"`
|
||||||
|
|
||||||
|
Method string `json:"http_method" validate:"required,oneof=GET POST"`
|
||||||
|
WebhookUrl string `json:"webhook_url" validate:"required,url,max=255,startswith=https://"`
|
||||||
|
|
||||||
|
Secrets []struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=32,excludesall=% "`
|
||||||
|
} `json:"secrets" validate:"dive,omitempty,min=0,max=5"`
|
||||||
|
|
||||||
|
Headers []struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=32,excludes= "`
|
||||||
|
Value string `json:"value" validate:"required,min=1,max=255"`
|
||||||
|
} `json:"headers" validate:"dive,omitempty,min=0,max=5"`
|
||||||
|
|
||||||
|
Placeholders []struct {
|
||||||
|
Placeholder string `json:"name" validate:"required,min=1,max=32,excludesall=% "`
|
||||||
|
JsonPath string `json:"json_path" validate:"required,min=1,max=255"`
|
||||||
|
} `json:"placeholders" validate:"dive,omitempty,min=0,max=15"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var validate = validator.New()
|
||||||
|
|
||||||
|
func CreateIntegrationHandler(ctx *gin.Context) {
|
||||||
|
userId := ctx.Keys["userid"].(uint64)
|
||||||
|
|
||||||
|
ownedCount, err := dbclient.Client.CustomIntegrations.GetOwnedCount(userId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ownedCount >= 5 {
|
||||||
|
ctx.JSON(403, utils.ErrorStr("You have reached the integration limit (5/5)"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data integrationCreateBody
|
||||||
|
if err := ctx.BindJSON(&data); err != nil {
|
||||||
|
ctx.JSON(400, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validate.Struct(data); err != nil {
|
||||||
|
validationErrors, ok := err.(validator.ValidationErrors)
|
||||||
|
if !ok {
|
||||||
|
ctx.JSON(500, utils.ErrorStr("An error occurred while validating the integration"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted := "Your input contained the following errors:"
|
||||||
|
for _, validationError := range validationErrors {
|
||||||
|
formatted += fmt.Sprintf("\n%s", validationError.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted = strings.TrimSuffix(formatted, "\n")
|
||||||
|
ctx.JSON(400, utils.ErrorStr(formatted))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
integration, err := dbclient.Client.CustomIntegrations.Create(userId, data.WebhookUrl, data.Method, data.Name, data.Description, data.ImageUrl, data.PrivacyPolicyUrl)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store secrets
|
||||||
|
if len(data.Secrets) > 0 {
|
||||||
|
secrets := make([]database.CustomIntegrationSecret, len(data.Secrets))
|
||||||
|
for i, secret := range data.Secrets {
|
||||||
|
secrets[i] = database.CustomIntegrationSecret{
|
||||||
|
Name: secret.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := dbclient.Client.CustomIntegrationSecrets.CreateOrUpdate(integration.Id, secrets); err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store headers
|
||||||
|
if len(data.Headers) > 0 {
|
||||||
|
headers := make([]database.CustomIntegrationHeader, len(data.Headers))
|
||||||
|
for i, header := range data.Headers {
|
||||||
|
headers[i] = database.CustomIntegrationHeader{
|
||||||
|
Name: header.Name,
|
||||||
|
Value: header.Value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := dbclient.Client.CustomIntegrationHeaders.CreateOrUpdate(integration.Id, headers); err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store placeholders
|
||||||
|
if len(data.Placeholders) > 0 {
|
||||||
|
placeholders := make([]database.CustomIntegrationPlaceholder, len(data.Placeholders))
|
||||||
|
for i, placeholder := range data.Placeholders {
|
||||||
|
placeholders[i] = database.CustomIntegrationPlaceholder{
|
||||||
|
Name: placeholder.Placeholder,
|
||||||
|
JsonPath: placeholder.JsonPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := dbclient.Client.CustomIntegrationPlaceholders.Set(integration.Id, placeholders); err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(200, integration)
|
||||||
|
}
|
42
app/http/endpoints/api/integrations/deleteintegration.go
Normal file
42
app/http/endpoints/api/integrations/deleteintegration.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeleteIntegrationHandler(ctx *gin.Context) {
|
||||||
|
userId := ctx.Keys["userid"].(uint64)
|
||||||
|
|
||||||
|
integrationId, err := strconv.Atoi(ctx.Param("integrationid"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Invalid integration ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
integration, ok, err := dbclient.Client.CustomIntegrations.Get(integrationId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
ctx.JSON(404, utils.ErrorStr("Integration not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has permission to manage this integration
|
||||||
|
if integration.OwnerId != userId {
|
||||||
|
ctx.JSON(403, utils.ErrorStr("You do not have permission to delete this integration"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbclient.Client.CustomIntegrations.Delete(integration.Id); err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(204)
|
||||||
|
}
|
79
app/http/endpoints/api/integrations/editsecrets.go
Normal file
79
app/http/endpoints/api/integrations/editsecrets.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UpdateIntegrationSecretsHandler(ctx *gin.Context) {
|
||||||
|
guildId := ctx.Keys["guildid"].(uint64)
|
||||||
|
|
||||||
|
integrationId, err := strconv.Atoi(ctx.Param("integrationid"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Invalid integration ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check integration is active
|
||||||
|
active, err := dbclient.Client.CustomIntegrationGuilds.IsActive(integrationId, guildId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !active {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Integration is not active"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data activateIntegrationBody
|
||||||
|
if err := ctx.BindJSON(&data); err != nil {
|
||||||
|
ctx.JSON(400, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Check the secret values are valid
|
||||||
|
secrets, err := dbclient.Client.CustomIntegrationSecrets.GetByIntegration(integrationId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(secrets) != len(data.Secrets) {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Invalid secret values"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we've checked the length, we can just iterate over the secrets and they're guaranteed to be correct
|
||||||
|
secretMap := make(map[int]string)
|
||||||
|
for secretName, value := range data.Secrets {
|
||||||
|
if len(value) == 0 || len(value) > 255 {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Secret values must be between 1 and 255 characters"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
|
||||||
|
inner:
|
||||||
|
for _, secret := range secrets {
|
||||||
|
if secret.Name == secretName {
|
||||||
|
found = true
|
||||||
|
secretMap[secret.Id] = value
|
||||||
|
break inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Invalid secret values"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbclient.Client.CustomIntegrationSecretValues.UpdateAll(guildId, integrationId, secretMap); err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(204)
|
||||||
|
}
|
101
app/http/endpoints/api/integrations/getintegration.go
Normal file
101
app/http/endpoints/api/integrations/getintegration.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/TicketsBot/database"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type integrationResponse struct {
|
||||||
|
// Strip out the sensitive fields
|
||||||
|
Id int `json:"id"`
|
||||||
|
OwnerId uint64 `json:"owner_id"`
|
||||||
|
WebhookHost string `json:"webhook_url"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ImageUrl *string `json:"image_url"`
|
||||||
|
ProxyToken *string `json:"proxy_token,omitempty"`
|
||||||
|
PrivacyPolicyUrl *string `json:"privacy_policy_url"`
|
||||||
|
Public bool `json:"public"`
|
||||||
|
Approved bool `json:"approved"`
|
||||||
|
|
||||||
|
Placeholders []database.CustomIntegrationPlaceholder `json:"placeholders"`
|
||||||
|
Secrets []database.CustomIntegrationSecret `json:"secrets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIntegrationHandler(ctx *gin.Context) {
|
||||||
|
userId := ctx.Keys["userid"].(uint64)
|
||||||
|
|
||||||
|
integrationId, err := strconv.Atoi(ctx.Param("integrationid"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Invalid integration ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
integration, ok, err := dbclient.Client.CustomIntegrations.Get(integrationId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
ctx.JSON(404, utils.ErrorStr("Integration not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has permission to view this integration
|
||||||
|
if integration.OwnerId != userId && !(integration.Public && integration.Approved) {
|
||||||
|
ctx.JSON(403, utils.ErrorStr("You do not have permission to view this integration"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
placeholders, err := dbclient.Client.CustomIntegrationPlaceholders.GetByIntegration(integrationId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't serve null
|
||||||
|
if placeholders == nil {
|
||||||
|
placeholders = make([]database.CustomIntegrationPlaceholder, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets, err := dbclient.Client.CustomIntegrationSecrets.GetByIntegration(integrationId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't serve null
|
||||||
|
if secrets == nil {
|
||||||
|
secrets = make([]database.CustomIntegrationSecret, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxyToken *string
|
||||||
|
if integration.ImageUrl != nil {
|
||||||
|
tmp, err := utils.GenerateImageProxyToken(*integration.ImageUrl)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyToken = &tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(200, integrationResponse{
|
||||||
|
Id: integration.Id,
|
||||||
|
OwnerId: integration.OwnerId,
|
||||||
|
WebhookHost: utils.GetUrlHost(integration.WebhookUrl),
|
||||||
|
Name: integration.Name,
|
||||||
|
Description: integration.Description,
|
||||||
|
ImageUrl: integration.ImageUrl,
|
||||||
|
ProxyToken: proxyToken,
|
||||||
|
PrivacyPolicyUrl: integration.PrivacyPolicyUrl,
|
||||||
|
Public: integration.Public,
|
||||||
|
Approved: integration.Approved,
|
||||||
|
Placeholders: placeholders,
|
||||||
|
Secrets: secrets,
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/TicketsBot/database"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type detailedResponse struct {
|
||||||
|
database.CustomIntegration
|
||||||
|
Placeholders []database.CustomIntegrationPlaceholder `json:"placeholders"`
|
||||||
|
Headers []database.CustomIntegrationHeader `json:"headers"`
|
||||||
|
Secrets []database.CustomIntegrationSecret `json:"secrets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIntegrationDetailedHandler(ctx *gin.Context) {
|
||||||
|
userId := ctx.Keys["userid"].(uint64)
|
||||||
|
|
||||||
|
integrationId, err := strconv.Atoi(ctx.Param("integrationid"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Invalid integration ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
integration, ok, err := dbclient.Client.CustomIntegrations.Get(integrationId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
ctx.JSON(404, utils.ErrorStr("Integration not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has permission to view this integration
|
||||||
|
if integration.OwnerId != userId {
|
||||||
|
ctx.JSON(403, utils.ErrorStr("You do not have permission to view this integration"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get placeholders
|
||||||
|
placeholders, err := dbclient.Client.CustomIntegrationPlaceholders.GetByIntegration(integrationId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't serve null
|
||||||
|
if placeholders == nil {
|
||||||
|
placeholders = make([]database.CustomIntegrationPlaceholder, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get headers
|
||||||
|
headers, err := dbclient.Client.CustomIntegrationHeaders.GetByIntegration(integrationId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't serve null
|
||||||
|
if headers == nil {
|
||||||
|
headers = make([]database.CustomIntegrationHeader, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get secrets
|
||||||
|
secrets, err := dbclient.Client.CustomIntegrationSecrets.GetByIntegration(integrationId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't serve null
|
||||||
|
if secrets == nil {
|
||||||
|
secrets = make([]database.CustomIntegrationSecret, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(200, detailedResponse{
|
||||||
|
CustomIntegration: integration,
|
||||||
|
Placeholders: placeholders,
|
||||||
|
Headers: headers,
|
||||||
|
Secrets: secrets,
|
||||||
|
})
|
||||||
|
}
|
28
app/http/endpoints/api/integrations/isactive.go
Normal file
28
app/http/endpoints/api/integrations/isactive.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsIntegrationActiveHandler(ctx *gin.Context) {
|
||||||
|
guildId := ctx.Keys["guildid"].(uint64)
|
||||||
|
|
||||||
|
integrationId, err := strconv.Atoi(ctx.Param("integrationid"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Invalid integration ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
active, err := dbclient.Client.CustomIntegrationGuilds.IsActive(integrationId, guildId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(200, gin.H{
|
||||||
|
"active": active,
|
||||||
|
})
|
||||||
|
}
|
78
app/http/endpoints/api/integrations/listintegrations.go
Normal file
78
app/http/endpoints/api/integrations/listintegrations.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const pageLimit = 20
|
||||||
|
const builtInCount = 1
|
||||||
|
|
||||||
|
type integrationWithMetadata struct {
|
||||||
|
integrationResponse
|
||||||
|
GuildCount int `json:"guild_count"`
|
||||||
|
Added bool `json:"added"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListIntegrationsHandler(ctx *gin.Context) {
|
||||||
|
userId := ctx.Keys["userid"].(uint64)
|
||||||
|
guildId := ctx.Keys["guildid"].(uint64)
|
||||||
|
|
||||||
|
page, err := strconv.Atoi(ctx.Query("page"))
|
||||||
|
if err != nil || page <= 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
page -= 1
|
||||||
|
|
||||||
|
limit := pageLimit
|
||||||
|
if page == 0 {
|
||||||
|
limit -= builtInCount
|
||||||
|
}
|
||||||
|
|
||||||
|
availableIntegrations, err := dbclient.Client.CustomIntegrationGuilds.GetAvailableIntegrationsWithActive(guildId, userId, limit, page*pageLimit)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
integrations := make([]integrationWithMetadata, len(availableIntegrations))
|
||||||
|
for i, integration := range availableIntegrations {
|
||||||
|
var proxyToken *string
|
||||||
|
if integration.ImageUrl != nil {
|
||||||
|
tmp, err := utils.GenerateImageProxyToken(*integration.ImageUrl)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyToken = &tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
integrations[i] = integrationWithMetadata{
|
||||||
|
integrationResponse: integrationResponse{
|
||||||
|
Id: integration.Id,
|
||||||
|
OwnerId: integration.OwnerId,
|
||||||
|
WebhookHost: utils.GetUrlHost(integration.WebhookUrl),
|
||||||
|
Name: integration.Name,
|
||||||
|
Description: integration.Description,
|
||||||
|
ImageUrl: integration.ImageUrl,
|
||||||
|
ProxyToken: proxyToken,
|
||||||
|
PrivacyPolicyUrl: integration.PrivacyPolicyUrl,
|
||||||
|
Public: integration.Public,
|
||||||
|
Approved: integration.Approved,
|
||||||
|
},
|
||||||
|
GuildCount: integration.GuildCount,
|
||||||
|
Added: integration.Active,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't serve null
|
||||||
|
if integrations == nil {
|
||||||
|
integrations = make([]integrationWithMetadata, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(200, integrations)
|
||||||
|
}
|
74
app/http/endpoints/api/integrations/ownedintegrations.go
Normal file
74
app/http/endpoints/api/integrations/ownedintegrations.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/TicketsBot/database"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
type integrationWithCount struct {
|
||||||
|
integrationResponse
|
||||||
|
GuildCount int `json:"guild_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOwnedIntegrationsHandler(ctx *gin.Context) {
|
||||||
|
userId := ctx.Keys["userid"].(uint64)
|
||||||
|
|
||||||
|
group, _ := errgroup.WithContext(context.Background())
|
||||||
|
|
||||||
|
var integrations []database.CustomIntegrationWithGuildCount
|
||||||
|
var placeholders map[int][]database.CustomIntegrationPlaceholder
|
||||||
|
|
||||||
|
// Retrieve integrations
|
||||||
|
group.Go(func() (err error) {
|
||||||
|
integrations, err = dbclient.Client.CustomIntegrations.GetAllOwned(userId)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
// Retrieve placeholders
|
||||||
|
group.Go(func() (err error) {
|
||||||
|
placeholders, err = dbclient.Client.CustomIntegrationPlaceholders.GetAllForOwnedIntegrations(userId)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := group.Wait(); err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]integrationWithCount, len(integrations))
|
||||||
|
for i, integration := range integrations {
|
||||||
|
var proxyToken *string
|
||||||
|
if integration.ImageUrl != nil {
|
||||||
|
tmp, err := utils.GenerateImageProxyToken(*integration.ImageUrl)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyToken = &tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
res[i] = integrationWithCount{
|
||||||
|
integrationResponse: integrationResponse{
|
||||||
|
Id: integration.Id,
|
||||||
|
OwnerId: integration.OwnerId,
|
||||||
|
WebhookHost: utils.GetUrlHost(integration.WebhookUrl),
|
||||||
|
Name: integration.Name,
|
||||||
|
Description: integration.Description,
|
||||||
|
ImageUrl: integration.ImageUrl,
|
||||||
|
ProxyToken: proxyToken,
|
||||||
|
PrivacyPolicyUrl: integration.PrivacyPolicyUrl,
|
||||||
|
Public: integration.Public,
|
||||||
|
Approved: integration.Approved,
|
||||||
|
Placeholders: placeholders[integration.Id],
|
||||||
|
},
|
||||||
|
GuildCount: integration.GuildCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(200, res)
|
||||||
|
}
|
25
app/http/endpoints/api/integrations/removeintegration.go
Normal file
25
app/http/endpoints/api/integrations/removeintegration.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RemoveIntegrationHandler(ctx *gin.Context) {
|
||||||
|
guildId := ctx.Keys["guildid"].(uint64)
|
||||||
|
|
||||||
|
integrationId, err := strconv.Atoi(ctx.Param("integrationid"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Invalid integration ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbclient.Client.CustomIntegrationGuilds.RemoveFromGuild(integrationId, guildId); err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(204)
|
||||||
|
}
|
76
app/http/endpoints/api/integrations/setpublic.go
Normal file
76
app/http/endpoints/api/integrations/setpublic.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/TicketsBot/GoPanel/botcontext"
|
||||||
|
"github.com/TicketsBot/GoPanel/config"
|
||||||
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/rxdn/gdl/objects/channel/embed"
|
||||||
|
"github.com/rxdn/gdl/rest"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetIntegrationPublicHandler(ctx *gin.Context) {
|
||||||
|
userId := ctx.Keys["userid"].(uint64)
|
||||||
|
|
||||||
|
integrationId, err := strconv.Atoi(ctx.Param("integrationid"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Invalid integration ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
integration, ok, err := dbclient.Client.CustomIntegrations.Get(integrationId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
ctx.JSON(404, utils.ErrorStr("Integration not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if integration.OwnerId != userId {
|
||||||
|
ctx.JSON(403, utils.ErrorStr("You do not have permission to manage this integration"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if integration.Public {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("You have already requested to make this integration public"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e := embed.NewEmbed().
|
||||||
|
SetTitle("Public Integration Request").
|
||||||
|
SetColor(0xfcb97d).
|
||||||
|
AddField("Integration ID", strconv.Itoa(integration.Id), true).
|
||||||
|
AddField("Integration Name", integration.Name, true).
|
||||||
|
AddField("Integration URL", fmt.Sprintf("`%s`", integration.WebhookUrl), true).
|
||||||
|
AddField("Integration Owner", fmt.Sprintf("<@%d>", integration.OwnerId), true).
|
||||||
|
AddField("Integration Description", integration.Description, false)
|
||||||
|
|
||||||
|
botCtx := botcontext.PublicContext()
|
||||||
|
_, err = rest.ExecuteWebhook(
|
||||||
|
config.Conf.Bot.PublicIntegrationRequestWebhookToken,
|
||||||
|
botCtx.RateLimiter,
|
||||||
|
config.Conf.Bot.PublicIntegrationRequestWebhookId,
|
||||||
|
true,
|
||||||
|
rest.WebhookBody{
|
||||||
|
Embeds: utils.Slice(e),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbclient.Client.CustomIntegrations.SetPublic(integration.Id); err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(204)
|
||||||
|
}
|
268
app/http/endpoints/api/integrations/updateintegration.go
Normal file
268
app/http/endpoints/api/integrations/updateintegration.go
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||||
|
"github.com/TicketsBot/GoPanel/utils"
|
||||||
|
"github.com/TicketsBot/database"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type integrationUpdateBody struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=32"`
|
||||||
|
Description string `json:"description" validate:"required,min=1,max=255"`
|
||||||
|
ImageUrl *string `json:"image_url" validate:"omitempty,url,max=255,startswith=https://"`
|
||||||
|
PrivacyPolicyUrl *string `json:"privacy_policy_url" validate:"omitempty,url,max=255,startswith=https://"`
|
||||||
|
|
||||||
|
Method string `json:"http_method" validate:"required,oneof=GET POST"`
|
||||||
|
WebhookUrl string `json:"webhook_url" validate:"required,url,max=255,startswith=https://"`
|
||||||
|
|
||||||
|
Secrets []struct {
|
||||||
|
Id int `json:"id" validate:"omitempty,min=1"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=32,excludesall=% "`
|
||||||
|
} `json:"secrets" validate:"dive,omitempty,min=0,max=5"`
|
||||||
|
|
||||||
|
Headers []struct {
|
||||||
|
Id int `json:"id" validate:"omitempty,min=1"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=32,excludes= "`
|
||||||
|
Value string `json:"value" validate:"required,min=1,max=255"`
|
||||||
|
} `json:"headers" validate:"dive,omitempty,min=0,max=5"`
|
||||||
|
|
||||||
|
Placeholders []struct {
|
||||||
|
Id int `json:"id" validate:"omitempty,min=1"`
|
||||||
|
Placeholder string `json:"name" validate:"required,min=1,max=32,excludesall=% "`
|
||||||
|
JsonPath string `json:"json_path" validate:"required,min=1,max=255"`
|
||||||
|
} `json:"placeholders" validate:"dive,omitempty,min=0,max=15"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateIntegrationHandler(ctx *gin.Context) {
|
||||||
|
userId := ctx.Keys["userid"].(uint64)
|
||||||
|
|
||||||
|
integrationId, err := strconv.Atoi(ctx.Param("integrationid"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Invalid integration ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data integrationUpdateBody
|
||||||
|
if err := ctx.BindJSON(&data); err != nil {
|
||||||
|
ctx.JSON(400, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validate.Struct(data); err != nil {
|
||||||
|
validationErrors, ok := err.(validator.ValidationErrors)
|
||||||
|
if !ok {
|
||||||
|
ctx.JSON(500, utils.ErrorStr("An error occurred while validating the integration"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted := "Your input contained the following errors:"
|
||||||
|
for _, validationError := range validationErrors {
|
||||||
|
formatted += fmt.Sprintf("\n%s", validationError.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted = strings.TrimSuffix(formatted, "\n")
|
||||||
|
ctx.JSON(400, utils.ErrorStr(formatted))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
integration, ok, err := dbclient.Client.CustomIntegrations.Get(integrationId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
ctx.JSON(404, utils.ErrorStr("Integration not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if integration.OwnerId != userId {
|
||||||
|
ctx.JSON(403, utils.ErrorStr("You do not own this integration"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update integration metadata
|
||||||
|
err = dbclient.Client.CustomIntegrations.Update(database.CustomIntegration{
|
||||||
|
Id: integration.Id,
|
||||||
|
OwnerId: integration.OwnerId,
|
||||||
|
HttpMethod: data.Method,
|
||||||
|
WebhookUrl: data.WebhookUrl,
|
||||||
|
Name: data.Name,
|
||||||
|
Description: data.Description,
|
||||||
|
ImageUrl: data.ImageUrl,
|
||||||
|
PrivacyPolicyUrl: data.PrivacyPolicyUrl,
|
||||||
|
Public: integration.Public,
|
||||||
|
Approved: integration.Approved,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store secrets
|
||||||
|
if !data.updateSecrets(ctx, integration.Id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store headers
|
||||||
|
if !data.updateHeaders(ctx, integration.Id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store placeholders
|
||||||
|
if !data.updatePlaceholders(ctx, integration.Id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(200, integration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *integrationUpdateBody) updatePlaceholders(ctx *gin.Context, integrationId int) bool {
|
||||||
|
// Verify IDs are valid for the integration
|
||||||
|
existingPlaceholders, err := dbclient.Client.CustomIntegrationPlaceholders.GetByIntegration(integrationId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, placeholder := range b.Placeholders {
|
||||||
|
if placeholder.Id != 0 {
|
||||||
|
isValid := false
|
||||||
|
inner:
|
||||||
|
for _, existingPlaceholder := range existingPlaceholders {
|
||||||
|
if existingPlaceholder.Id == placeholder.Id {
|
||||||
|
if existingPlaceholder.IntegrationId == integrationId {
|
||||||
|
isValid = true
|
||||||
|
break inner
|
||||||
|
} else {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Integration ID mismatch for placeholders"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValid {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Integration ID mismatch for placeholders"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
placeholders := make([]database.CustomIntegrationPlaceholder, len(b.Placeholders))
|
||||||
|
for i, placeholder := range b.Placeholders {
|
||||||
|
placeholders[i] = database.CustomIntegrationPlaceholder{
|
||||||
|
Id: placeholder.Id,
|
||||||
|
Name: placeholder.Placeholder,
|
||||||
|
JsonPath: placeholder.JsonPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := dbclient.Client.CustomIntegrationPlaceholders.Set(integrationId, placeholders); err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *integrationUpdateBody) updateHeaders(ctx *gin.Context, integrationId int) bool {
|
||||||
|
// Verify IDs are valid for the integration
|
||||||
|
existingHeaders, err := dbclient.Client.CustomIntegrationHeaders.GetByIntegration(integrationId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, header := range b.Headers {
|
||||||
|
if header.Id != 0 {
|
||||||
|
isValid := false
|
||||||
|
|
||||||
|
inner:
|
||||||
|
for _, existingHeader := range existingHeaders {
|
||||||
|
if existingHeader.Id == header.Id {
|
||||||
|
if existingHeader.IntegrationId == integrationId {
|
||||||
|
isValid = true
|
||||||
|
break inner
|
||||||
|
} else {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Integration ID mismatch for headers"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValid {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Integration ID mismatch for headers"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := make([]database.CustomIntegrationHeader, len(b.Headers))
|
||||||
|
for i, header := range b.Headers {
|
||||||
|
headers[i] = database.CustomIntegrationHeader{
|
||||||
|
Id: header.Id,
|
||||||
|
Name: header.Name,
|
||||||
|
Value: header.Value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := dbclient.Client.CustomIntegrationHeaders.CreateOrUpdate(integrationId, headers); err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *integrationUpdateBody) updateSecrets(ctx *gin.Context, integrationId int) bool {
|
||||||
|
// Verify IDs are valid for the integration
|
||||||
|
existingSecrets, err := dbclient.Client.CustomIntegrationSecrets.GetByIntegration(integrationId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, secret := range b.Secrets {
|
||||||
|
if secret.Id != 0 {
|
||||||
|
isValid := false
|
||||||
|
inner:
|
||||||
|
for _, existingSecret := range existingSecrets {
|
||||||
|
if existingSecret.Id == secret.Id {
|
||||||
|
if existingSecret.IntegrationId == integrationId {
|
||||||
|
isValid = true
|
||||||
|
break inner
|
||||||
|
} else {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Integration ID mismatch for secrets"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValid {
|
||||||
|
ctx.JSON(400, utils.ErrorStr("Integration ID mismatch for secrets"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets := make([]database.CustomIntegrationSecret, len(b.Secrets))
|
||||||
|
for i, secret := range b.Secrets {
|
||||||
|
secrets[i] = database.CustomIntegrationSecret{
|
||||||
|
Id: secret.Id,
|
||||||
|
Name: secret.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := dbclient.Client.CustomIntegrationSecrets.CreateOrUpdate(integrationId, secrets); err != nil {
|
||||||
|
ctx.JSON(500, utils.ErrorJson(err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
@ -7,6 +7,7 @@ import (
|
|||||||
api_blacklist "github.com/TicketsBot/GoPanel/app/http/endpoints/api/blacklist"
|
api_blacklist "github.com/TicketsBot/GoPanel/app/http/endpoints/api/blacklist"
|
||||||
api_customisation "github.com/TicketsBot/GoPanel/app/http/endpoints/api/customisation"
|
api_customisation "github.com/TicketsBot/GoPanel/app/http/endpoints/api/customisation"
|
||||||
api_forms "github.com/TicketsBot/GoPanel/app/http/endpoints/api/forms"
|
api_forms "github.com/TicketsBot/GoPanel/app/http/endpoints/api/forms"
|
||||||
|
api_integrations "github.com/TicketsBot/GoPanel/app/http/endpoints/api/integrations"
|
||||||
api_panels "github.com/TicketsBot/GoPanel/app/http/endpoints/api/panel"
|
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_override "github.com/TicketsBot/GoPanel/app/http/endpoints/api/staffoverride"
|
api_override "github.com/TicketsBot/GoPanel/app/http/endpoints/api/staffoverride"
|
||||||
@ -67,6 +68,16 @@ func StartServer() {
|
|||||||
apiGroup := router.Group("/api", middleware.VerifyXTicketsHeader, middleware.AuthenticateToken)
|
apiGroup := router.Group("/api", middleware.VerifyXTicketsHeader, middleware.AuthenticateToken)
|
||||||
{
|
{
|
||||||
apiGroup.GET("/session", api.SessionHandler)
|
apiGroup.GET("/session", api.SessionHandler)
|
||||||
|
|
||||||
|
integrationGroup := apiGroup.Group("/integrations")
|
||||||
|
|
||||||
|
integrationGroup.GET("/self", api_integrations.GetOwnedIntegrationsHandler)
|
||||||
|
integrationGroup.GET("/view/:integrationid", api_integrations.GetIntegrationHandler)
|
||||||
|
integrationGroup.GET("/view/:integrationid/detail", api_integrations.GetIntegrationDetailedHandler)
|
||||||
|
integrationGroup.POST("/:integrationid/public", api_integrations.SetIntegrationPublicHandler)
|
||||||
|
integrationGroup.PATCH("/:integrationid", api_integrations.UpdateIntegrationHandler)
|
||||||
|
integrationGroup.DELETE("/:integrationid", api_integrations.DeleteIntegrationHandler)
|
||||||
|
apiGroup.POST("/integrations", api_integrations.CreateIntegrationHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
guildAuthApiAdmin := apiGroup.Group("/:id", middleware.AuthenticateGuild(permission.Admin))
|
guildAuthApiAdmin := apiGroup.Group("/:id", middleware.AuthenticateGuild(permission.Admin))
|
||||||
@ -156,6 +167,12 @@ func StartServer() {
|
|||||||
guildAuthApiAdmin.GET("/staff-override", api_override.GetOverrideHandler)
|
guildAuthApiAdmin.GET("/staff-override", api_override.GetOverrideHandler)
|
||||||
guildAuthApiAdmin.POST("/staff-override", api_override.CreateOverrideHandler)
|
guildAuthApiAdmin.POST("/staff-override", api_override.CreateOverrideHandler)
|
||||||
guildAuthApiAdmin.DELETE("/staff-override", api_override.DeleteOverrideHandler)
|
guildAuthApiAdmin.DELETE("/staff-override", api_override.DeleteOverrideHandler)
|
||||||
|
|
||||||
|
guildAuthApiAdmin.GET("/integrations/available", api_integrations.ListIntegrationsHandler)
|
||||||
|
guildAuthApiAdmin.GET("/integrations/:integrationid", api_integrations.IsIntegrationActiveHandler)
|
||||||
|
guildAuthApiAdmin.POST("/integrations/:integrationid", api_integrations.ActivateIntegrationHandler)
|
||||||
|
guildAuthApiAdmin.PATCH("/integrations/:integrationid", api_integrations.UpdateIntegrationSecretsHandler)
|
||||||
|
guildAuthApiAdmin.DELETE("/integrations/:integrationid", api_integrations.RemoveIntegrationHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
userGroup := router.Group("/user", middleware.AuthenticateToken)
|
userGroup := router.Group("/user", middleware.AuthenticateToken)
|
||||||
|
@ -14,25 +14,28 @@ func ContextForGuild(guildId uint64) (ctx BotContext, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyPrefix string
|
|
||||||
|
|
||||||
if isWhitelabel {
|
if isWhitelabel {
|
||||||
res, err := dbclient.Client.Whitelabel.GetByBotId(whitelabelBotId)
|
res, err := dbclient.Client.Whitelabel.GetByBotId(whitelabelBotId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx, err
|
return ctx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.BotId = res.BotId
|
rateLimiter := ratelimit.NewRateLimiter(ratelimit.NewRedisStore(redis.Client.Client, fmt.Sprintf("ratelimiter:%d", whitelabelBotId)), 1)
|
||||||
ctx.Token = res.Token
|
|
||||||
keyPrefix = fmt.Sprintf("ratelimiter:%d", whitelabelBotId)
|
return BotContext{
|
||||||
|
BotId: res.BotId,
|
||||||
|
Token: res.Token,
|
||||||
|
RateLimiter: rateLimiter,
|
||||||
|
}, nil
|
||||||
} else {
|
} else {
|
||||||
ctx.BotId = config.Conf.Bot.Id
|
return PublicContext(), nil
|
||||||
ctx.Token = config.Conf.Bot.Token
|
}
|
||||||
keyPrefix = "ratelimiter:public"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Large sharding buckets
|
func PublicContext() BotContext {
|
||||||
ctx.RateLimiter = ratelimit.NewRateLimiter(ratelimit.NewRedisStore(redis.Client.Client, keyPrefix), 1)
|
return BotContext{
|
||||||
|
BotId: config.Conf.Bot.Id,
|
||||||
return
|
Token: config.Conf.Bot.Token,
|
||||||
|
RateLimiter: ratelimit.NewRateLimiter(ratelimit.NewRedisStore(redis.Client.Client, "ratelimiter:public"), 1),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,9 @@ type (
|
|||||||
AesKey string `toml:"aes-key"`
|
AesKey string `toml:"aes-key"`
|
||||||
ProxyUrl string `toml:"discord-proxy-url"`
|
ProxyUrl string `toml:"discord-proxy-url"`
|
||||||
RenderServiceUrl string `toml:"render-service-url"`
|
RenderServiceUrl string `toml:"render-service-url"`
|
||||||
|
ImageProxySecret string `toml:"image-proxy-secret"`
|
||||||
|
PublicIntegrationRequestWebhookId uint64 `toml:"public-integration-request-webhook-id"`
|
||||||
|
PublicIntegrationRequestWebhookToken string `toml:"public-integration-request-webhook-token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
Redis struct {
|
Redis struct {
|
||||||
@ -74,11 +77,6 @@ type (
|
|||||||
Cache struct {
|
Cache struct {
|
||||||
Uri string
|
Uri string
|
||||||
}
|
}
|
||||||
|
|
||||||
Referral struct {
|
|
||||||
Show bool
|
|
||||||
Link string
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -126,6 +124,7 @@ func fromEnvvar() {
|
|||||||
botId, _ := strconv.ParseUint(os.Getenv("BOT_ID"), 10, 64)
|
botId, _ := strconv.ParseUint(os.Getenv("BOT_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"))
|
||||||
|
publicIntegrationRequestWebhookId, _ := strconv.ParseUint(os.Getenv("PUBLIC_INTEGRATION_REQUEST_WEBHOOK_ID"), 10, 64)
|
||||||
|
|
||||||
Conf = Config{
|
Conf = Config{
|
||||||
Admins: admins,
|
Admins: admins,
|
||||||
@ -165,6 +164,9 @@ func fromEnvvar() {
|
|||||||
AesKey: os.Getenv("LOG_AES_KEY"),
|
AesKey: os.Getenv("LOG_AES_KEY"),
|
||||||
ProxyUrl: os.Getenv("DISCORD_PROXY_URL"),
|
ProxyUrl: os.Getenv("DISCORD_PROXY_URL"),
|
||||||
RenderServiceUrl: os.Getenv("RENDER_SERVICE_URL"),
|
RenderServiceUrl: os.Getenv("RENDER_SERVICE_URL"),
|
||||||
|
ImageProxySecret: os.Getenv("IMAGE_PROXY_SECRET"),
|
||||||
|
PublicIntegrationRequestWebhookId: publicIntegrationRequestWebhookId,
|
||||||
|
PublicIntegrationRequestWebhookToken: os.Getenv("PUBLIC_INTEGRATION_REQUEST_WEBHOOK_TOKEN"),
|
||||||
},
|
},
|
||||||
Redis: Redis{
|
Redis: Redis{
|
||||||
Host: os.Getenv("REDIS_HOST"),
|
Host: os.Getenv("REDIS_HOST"),
|
||||||
|
@ -13,7 +13,8 @@ import (
|
|||||||
var Client *database.Database
|
var Client *database.Database
|
||||||
|
|
||||||
func ConnectToDatabase() {
|
func ConnectToDatabase() {
|
||||||
config, err := pgxpool.ParseConfig(config.Conf.Database.Uri); if err != nil {
|
config, err := pgxpool.ParseConfig(config.Conf.Database.Uri)
|
||||||
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
frontend/public/assets/img/grey.png
Normal file
BIN
frontend/public/assets/img/grey.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
@ -24,7 +24,6 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
/*font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
@ -62,3 +61,7 @@ button:not(:disabled):active {
|
|||||||
button:focus {
|
button:focus {
|
||||||
border-color: #666;
|
border-color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: #f9a8d4;
|
||||||
|
}
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
<div class="badge">
|
<div class="badge" style="--badge-background-color: {colour}">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let colour = '#3472f7';
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.badge {
|
.badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
background-color: #3472f7;
|
background-color: var(--badge-background-color, #3472f7);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
margin-left: 4px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
@ -1,10 +1,12 @@
|
|||||||
<button on:click isTrigger="1" class:fullWidth class:danger {disabled} {type}>
|
<button on:click isTrigger="1" class:fullWidth class:danger class:iconOnly {disabled} {type}>
|
||||||
{#if icon !== undefined}
|
{#if icon !== undefined}
|
||||||
<i class="{icon}"></i>
|
<i class="{icon}"></i>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if !iconOnly}
|
||||||
<span class="content">
|
<span class="content">
|
||||||
<slot/>
|
<slot/>
|
||||||
</span>
|
</span>
|
||||||
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -13,6 +15,7 @@
|
|||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
export let type = "submit";
|
export let type = "submit";
|
||||||
export let danger = false;
|
export let danger = false;
|
||||||
|
export let iconOnly = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -65,4 +68,9 @@
|
|||||||
background-color: #c32232 !important;
|
background-color: #c32232 !important;
|
||||||
border-color: #c32232 !important;
|
border-color: #c32232 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iconOnly {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
73
frontend/src/components/ConfirmationModal.svelte
Normal file
73
frontend/src/components/ConfirmationModal.svelte
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<div class="modal" transition:fade>
|
||||||
|
<div class="modal-wrapper">
|
||||||
|
<Card footer="{true}" footerRight="{true}" fill="{false}">
|
||||||
|
<span slot="title">Embed Builder</span>
|
||||||
|
|
||||||
|
<div slot="body" class="body-wrapper">
|
||||||
|
<slot name="body"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div slot="footer" style="gap: 12px">
|
||||||
|
<Button danger={!isDangerous} on:click={() => dispatch("cancel", {})}>Cancel</Button>
|
||||||
|
<Button danger={isDangerous} {icon} on:click={() => dispatch("confirm", {})}>
|
||||||
|
<slot name="confirm"></slot>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-backdrop" transition:fade>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Card from "./Card.svelte";
|
||||||
|
import {fade} from "svelte/transition";
|
||||||
|
|
||||||
|
import {createEventDispatcher} from "svelte";
|
||||||
|
import Button from "./Button.svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let icon;
|
||||||
|
export let isDangerous = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 999;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 60%;
|
||||||
|
margin: 10% auto auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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>
|
501
frontend/src/components/IntegrationEditor.svelte
Normal file
501
frontend/src/components/IntegrationEditor.svelte
Normal file
@ -0,0 +1,501 @@
|
|||||||
|
{#if deleteConfirmationOpen}
|
||||||
|
<ConfirmationModal icon="fas fa-globe" isDangerous on:cancel={() => publicConfirmationOpen = false}
|
||||||
|
on:confirm={dispatchDelete}>
|
||||||
|
<span slot="body">Are you sure you want to delete your integration {data.name}?</span>
|
||||||
|
<span slot="confirm">Delete</span>
|
||||||
|
</ConfirmationModal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if publicConfirmationOpen}
|
||||||
|
<ConfirmationModal icon="fas fa-trash-icon" on:cancel={() => deleteConfirmationOpen = false}
|
||||||
|
on:confirm={dispatchMakePublic}>
|
||||||
|
<p slot="body">Are you sure you want to make your integration <i>{data.name}</i> public? Everyone will be able to
|
||||||
|
add it to their servers.</p>
|
||||||
|
<span slot="confirm">Confirm</span>
|
||||||
|
</ConfirmationModal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="parent">
|
||||||
|
<div class="content">
|
||||||
|
{#if editingMetadata || editMode}
|
||||||
|
<div class="row outer-row" bind:this={metadataRow} transition:fade>
|
||||||
|
<div class="col-metadata">
|
||||||
|
<Card footer footerRight>
|
||||||
|
<span slot="title">Integration Metadata</span>
|
||||||
|
<div slot="body" class="body-wrapper">
|
||||||
|
<p>Let people know what your integration does. A preview will be generated as you type.</p>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<Input col2 label="Name" placeholder="My Integration" bind:value={data.name}/>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<Input col2 label="Image URL" placeholder="https://example.com/logo.png" bind:value={data.image_url}
|
||||||
|
on:change={ensureNullIfBlank}/>
|
||||||
|
<Input col2 label="Privacy Policy URL" placeholder="https://example.com/privacy"
|
||||||
|
bind:value={data.privacy_policy_url} on:change={ensureNullIfBlank}/>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<Textarea col1 label="Description" placeholder="Let people know what your integration does"
|
||||||
|
bind:value={data.description}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div slot="footer" style="gap: 12px">
|
||||||
|
{#if editMode}
|
||||||
|
<Button icon="fas fa-globe" disabled={data.public} on:click={confirmMakePublic}>Make Public</Button>
|
||||||
|
|
||||||
|
<Button danger icon="fas fa-trash-can" on:click={requestDeleteConfirmation}>
|
||||||
|
Delete Integration
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button icon="fas fa-arrow-right" disabled={data.name.length === 0 || data.description.length === 0}
|
||||||
|
on:click={nextStage}>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div class="col-preview">
|
||||||
|
<div class="preview">
|
||||||
|
<Integration hideLinks name={data.name} imageUrl={data.image_url}>
|
||||||
|
<span slot="description">{data.description}</span>
|
||||||
|
</Integration>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !editingMetadata || editMode}
|
||||||
|
<div class="row outer-row" transition:fade>
|
||||||
|
<div class="col-left">
|
||||||
|
<Card footer footerRight>
|
||||||
|
<span slot="title">HTTP Request</span>
|
||||||
|
<div slot="body" class="body-wrapper">
|
||||||
|
<div>
|
||||||
|
<h3>API Endpoint</h3>
|
||||||
|
<div class="section">
|
||||||
|
<p>When a user opens a ticket, a HTTP <code>{data.http_method}</code> request will be sent to the
|
||||||
|
provided
|
||||||
|
request
|
||||||
|
URL. The URL must respond with a valid JSON payload.</p>
|
||||||
|
<div class="row">
|
||||||
|
<Dropdown col4 bind:value={data.http_method} label="Request Method">
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
</Dropdown>
|
||||||
|
<div class="col-3-4">
|
||||||
|
<Input col1 label="Request URL" bind:value={data.webhook_url}
|
||||||
|
placeholder="https://api.example.com/users/find?discord=%user_id%"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>Secrets</h3>
|
||||||
|
<div class="section">
|
||||||
|
<p>
|
||||||
|
If creating a public integration, you may wish to let users provide secret values, e.g. API keys,
|
||||||
|
instead of sending all requests through your own.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Note: Do not include the <code>%</code> symbols in secret names, they will be automatically
|
||||||
|
included
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
{#each data.secrets as secret, i}
|
||||||
|
<div class="row">
|
||||||
|
{#if i === 0}
|
||||||
|
<Input col1 label="Secret Name" placeholder="api_key" bind:value={secret.name}/>
|
||||||
|
<div class="button-anchor">
|
||||||
|
<Button danger iconOnly icon="fas fa-trash-can" on:click={() => deleteSecret(i)}/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Input col1 placeholder="api_key" bind:value={secret.name}/>
|
||||||
|
<div class="button-anchor">
|
||||||
|
<Button danger iconOnly icon="fas fa-trash-can" on:click={() => deleteSecret(i)}/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button fullWidth icon="fas fa-plus" on:click={addSecret} disabled={data.secrets.length >= 5}>
|
||||||
|
Add Additional Secret
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>Request Headers</h3>
|
||||||
|
<div class="section">
|
||||||
|
<p>You can specify up to 5 HTTP headers that will be sent with the request, for example, containing
|
||||||
|
authentication
|
||||||
|
keys. You may specify the user's ID in a header, via <code>%user_id%</code>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
You may also include the values of secrets you have created, via <code>%secret_name%</code>.
|
||||||
|
{#if data.secrets.length > 0}
|
||||||
|
For example, <code>%{data.secrets[0].name}%</code>.
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
{#each data.headers as header, i}
|
||||||
|
<div class="row">
|
||||||
|
{#if i === 0}
|
||||||
|
<Input col2 label="Header Name" placeholder="x-auth-key" bind:value={header.name}/>
|
||||||
|
<Input col2 label="Header Value" placeholder="super secret key" bind:value={header.value}/>
|
||||||
|
<div class="button-anchor">
|
||||||
|
<Button danger iconOnly icon="fas fa-trash-can" on:click={() => deleteHeader(i)}/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Input col2 placeholder="x-auth-key" bind:value={header.name}/>
|
||||||
|
<Input col2 placeholder="super secret key" bind:value={header.value}/>
|
||||||
|
<div class="button-anchor">
|
||||||
|
<Button danger iconOnly icon="fas fa-trash-can" on:click={() => deleteHeader(i)}/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button fullWidth icon="fas fa-plus" on:click={addHeader} disabled={data.headers.length >= 5}>
|
||||||
|
Add Additional Header
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div slot="footer">
|
||||||
|
{#if editMode}
|
||||||
|
<Button icon="fas fa-floppy-disk" on:click={dispatchSubmit}>Save</Button>
|
||||||
|
{:else }
|
||||||
|
<Button icon="fas fa-floppy-disk" on:click={dispatchSubmit}>Create</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div class="col-right">
|
||||||
|
<Card footer={false} fill={false}>
|
||||||
|
<span slot="title">Placeholders</span>
|
||||||
|
<div slot="body" class="body-wrapper">
|
||||||
|
<div class="section">
|
||||||
|
<p>
|
||||||
|
The response must contain a valid JSON payload. This payload will be parsed, and values can be
|
||||||
|
extracted
|
||||||
|
to use as placeholders in your welcome message.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Do <b>not</b> include the % symbols in the placeholder names. They will be included automatically.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The JSON path is the key path to access a field in the response JSON. You can use a period
|
||||||
|
(e.g. <code>user.username</code>) to access nested objects.
|
||||||
|
You will be presented with an example JSON payload as you type.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
{#each data.placeholders as placeholder, i}
|
||||||
|
<div class="row">
|
||||||
|
{#if i === 0}
|
||||||
|
<Input col2 label="Placeholder" placeholder="ingame_username"
|
||||||
|
bind:value={placeholder.name}/>
|
||||||
|
<Input col2 label="JSON Path" placeholder="user.username" bind:value={placeholder.json_path}/>
|
||||||
|
<div class="button-anchor">
|
||||||
|
<Button danger iconOnly icon="fas fa-trash-can" on:click={() => deletePlaceholder(i)}/>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Input col2 placeholder="ingame_username" bind:value={placeholder.name}/>
|
||||||
|
<Input col2 placeholder="user.username" bind:value={placeholder.json_path}/>
|
||||||
|
<div class="button-anchor">
|
||||||
|
<Button danger iconOnly icon="fas fa-trash-can" on:click={() => deletePlaceholder(i)}/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button fullWidth icon="fas fa-plus" on:click={addPlaceholder}
|
||||||
|
disabled={data.placeholders.length >= 15}>
|
||||||
|
Add Additional Placeholder
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>Example Response</h3>
|
||||||
|
<div class="section">
|
||||||
|
<p>The request must be responded to with a JSON payload in the following form:</p>
|
||||||
|
<code class="codeblock">
|
||||||
|
{exampleJson}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {fade} from 'svelte/transition';
|
||||||
|
import Card from "./Card.svelte";
|
||||||
|
import Button from "./Button.svelte";
|
||||||
|
import Dropdown from "./form/Dropdown.svelte";
|
||||||
|
import Input from "./form/Input.svelte";
|
||||||
|
import Integration from "./manage/Integration.svelte";
|
||||||
|
import Textarea from "./form/Textarea.svelte";
|
||||||
|
import {createEventDispatcher, onMount} from "svelte";
|
||||||
|
import ConfirmationModal from "./ConfirmationModal.svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let guildId;
|
||||||
|
|
||||||
|
let metadataRow;
|
||||||
|
let editingMetadata = true;
|
||||||
|
|
||||||
|
let exampleJson = "{}";
|
||||||
|
|
||||||
|
export let data = {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
image_url: "",
|
||||||
|
privacy_policy_url: "",
|
||||||
|
http_method: "GET",
|
||||||
|
placeholders: [],
|
||||||
|
headers: [],
|
||||||
|
secrets: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export let editMode = false;
|
||||||
|
let deleteConfirmationOpen = false;
|
||||||
|
let publicConfirmationOpen = false;
|
||||||
|
|
||||||
|
function requestDeleteConfirmation() {
|
||||||
|
deleteConfirmationOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmMakePublic() {
|
||||||
|
publicConfirmationOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// on:input uses the old value!
|
||||||
|
$: data.placeholders, normalisePlaceholders();
|
||||||
|
$: data.placeholders, updateExampleJson();
|
||||||
|
$: data.secrets, normaliseSecrets();
|
||||||
|
$: data.headers, normaliseHeaders();
|
||||||
|
$: data.name, data.name = data.name.substring(0, 32);
|
||||||
|
$: data.description, data.description = data.description.substring(0, 255);
|
||||||
|
|
||||||
|
function addPlaceholder() {
|
||||||
|
data.placeholders.push({name: "", json_path: ""});
|
||||||
|
data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deletePlaceholder(i) {
|
||||||
|
data.placeholders.splice(i, 1);
|
||||||
|
data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalisePlaceholders() {
|
||||||
|
data.placeholders = data.placeholders.map((placeholder) => {
|
||||||
|
placeholder.name = placeholder.name.replaceAll(' ', '_').replaceAll('%', '');
|
||||||
|
return placeholder;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addHeader() {
|
||||||
|
data.headers.push({name: "", value: ""});
|
||||||
|
data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteHeader(i) {
|
||||||
|
data.headers.splice(i, 1);
|
||||||
|
data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normaliseHeaders() {
|
||||||
|
data.headers = data.headers.map((header) => {
|
||||||
|
header.name = header.name.replaceAll(' ', '-');
|
||||||
|
return header;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSecret() {
|
||||||
|
data.secrets.push({name: ""});
|
||||||
|
data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSecret(i) {
|
||||||
|
data.secrets.splice(i, 1);
|
||||||
|
data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normaliseSecrets() {
|
||||||
|
data.secrets = data.secrets.map((secret) => {
|
||||||
|
secret.name = secret.name.replaceAll(' ', '_').replaceAll('%', '');
|
||||||
|
return secret;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextStage() {
|
||||||
|
editingMetadata = false;
|
||||||
|
metadataRow.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNullIfBlank() {
|
||||||
|
if (data.image_url !== undefined && data.image_url !== null && data.image_url.length === 0) {
|
||||||
|
data.image_url = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.privacy_policy_url !== undefined && data.privacy_policy_url !== null && data.privacy_policy_url.length === 0) {
|
||||||
|
data.privacy_policy_url = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateExampleJson() {
|
||||||
|
try {
|
||||||
|
let obj = {};
|
||||||
|
for (const placeholder of data.placeholders) {
|
||||||
|
let split = placeholder.json_path.split(".");
|
||||||
|
let current = obj;
|
||||||
|
for (const [index, part] of split.entries()) {
|
||||||
|
if (index === split.length - 1) {
|
||||||
|
current[part] = "...";
|
||||||
|
} else {
|
||||||
|
if (current[part]) {
|
||||||
|
current = current[part];
|
||||||
|
} else {
|
||||||
|
current[part] = {};
|
||||||
|
current = current[part];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exampleJson = JSON.stringify(obj, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
exampleJson = "Invalid JSON";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchSubmit() {
|
||||||
|
ensureNullIfBlank();
|
||||||
|
dispatch("submit", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchMakePublic() {
|
||||||
|
publicConfirmationOpen = false;
|
||||||
|
dispatch("makePublic", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchDelete() {
|
||||||
|
dispatch("delete", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
updateExampleJson();
|
||||||
|
|
||||||
|
if (!editMode) {
|
||||||
|
addPlaceholder();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.parent {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
row-gap: 5vh;
|
||||||
|
width: 96%;
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-bottom: 5vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 1vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-left, .col-right {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-metadata {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-preview {
|
||||||
|
display: flex;
|
||||||
|
width: 33%;
|
||||||
|
max-width: 33%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-anchor {
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeblock {
|
||||||
|
border-color: #2e3136;
|
||||||
|
background-color: #2e3136;
|
||||||
|
color: white;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 1vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 950px) {
|
||||||
|
.outer-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-left, .col-right {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -26,17 +26,24 @@
|
|||||||
padding: 20px 0 20px 15px;
|
padding: 20px 0 20px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
:global(.link) {
|
:global(.link) {
|
||||||
display: flex;
|
display: flex;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.link-blue) {
|
:global(.link-blue) {
|
||||||
color: #3472f7;
|
color: #3472f7;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
141
frontend/src/components/manage/Integration.svelte
Normal file
141
frontend/src/components/manage/Integration.svelte
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<div class="wrapper">
|
||||||
|
{#if imageUrl !== null}
|
||||||
|
<img src={imageUrl} class="logo" bind:this={logo} on:error={useDefaultLogo}/>
|
||||||
|
{:else}
|
||||||
|
<img src="/assets/img/grey.png" class="logo"/>
|
||||||
|
{/if}
|
||||||
|
<div class="details">
|
||||||
|
<div class="title-row">
|
||||||
|
<span class="title">{name}</span>
|
||||||
|
{#if builtIn}
|
||||||
|
<Badge colour="#0c8f43">Built-In</Badge>
|
||||||
|
{/if}
|
||||||
|
{#if added}
|
||||||
|
<Badge>Active</Badge>
|
||||||
|
{/if}
|
||||||
|
{#if guildCount !== undefined}
|
||||||
|
<Badge>
|
||||||
|
<div class="guild-count">
|
||||||
|
<i class="fas fa-server"></i>
|
||||||
|
{guildCount}
|
||||||
|
</div>
|
||||||
|
</Badge>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<span class="description">
|
||||||
|
<slot name="description"></slot>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{#if !hideLinks}
|
||||||
|
<div class="links">
|
||||||
|
{#if builtIn}
|
||||||
|
<a href="{viewLink}" target="_blank" class="link-blue">View</a>
|
||||||
|
{:else if added}
|
||||||
|
<Navigate to="/manage/{guildId}/integrations/view/{integrationId}" styles="link-blue">View</Navigate>
|
||||||
|
<Navigate to="/manage/{guildId}/integrations/manage/{integrationId}" styles="link-blue">Configure</Navigate>
|
||||||
|
<a href="#" class="link-blue" on:click={() => dispatch("remove", {})}>Remove</a>
|
||||||
|
{:else}
|
||||||
|
{#if owned}
|
||||||
|
<Navigate to="/manage/{guildId}/integrations/view/{integrationId}" styles="link-blue">Preview</Navigate>
|
||||||
|
<Navigate to="/manage/{guildId}/integrations/configure/{integrationId}" styles="link-blue">Configure
|
||||||
|
</Navigate>
|
||||||
|
{:else}
|
||||||
|
<Navigate to="/manage/{guildId}/integrations/view/{integrationId}" styles="link-blue">View</Navigate>
|
||||||
|
{/if}
|
||||||
|
<Navigate to="/manage/{guildId}/integrations/activate/{integrationId}" styles="link-blue">Add to server
|
||||||
|
</Navigate>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Badge from "../Badge.svelte";
|
||||||
|
import {Navigate} from "svelte-router-spa";
|
||||||
|
import {createEventDispatcher} from "svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let guildId;
|
||||||
|
|
||||||
|
export let integrationId;
|
||||||
|
export let name;
|
||||||
|
export let imageUrl;
|
||||||
|
export let owned = false;
|
||||||
|
export let guildCount;
|
||||||
|
|
||||||
|
export let added = false;
|
||||||
|
export let builtIn = false;
|
||||||
|
export let hideLinks = false;
|
||||||
|
export let viewLink;
|
||||||
|
|
||||||
|
let logo;
|
||||||
|
|
||||||
|
function useDefaultLogo() {
|
||||||
|
logo.src = "/assets/img/grey.png";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
background-color: #272727 !important;
|
||||||
|
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
|
||||||
|
transition: all .3s ease-in-out;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
min-height: 150px;
|
||||||
|
height: 150px;
|
||||||
|
max-height: 150px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px 20px 10px 20px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bolder;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #b9bbbe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guild-count {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
</style>
|
@ -8,15 +8,23 @@
|
|||||||
<!-- on:click required to close dropdown again -->
|
<!-- on:click required to close dropdown again -->
|
||||||
|
|
||||||
{#if isAdmin}
|
{#if isAdmin}
|
||||||
<NavElement icon="fas fa-cogs" link="/manage/{guildId}/settings" on:click={closeDropdown}>Settings</NavElement>
|
<NavElement icon="fas fa-cogs" link="/manage/{guildId}/settings" on:click={closeDropdown}>Settings
|
||||||
|
</NavElement>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<NavElement icon="fas fa-copy" link="/manage/{guildId}/transcripts" on:click={closeDropdown}>Transcripts</NavElement>
|
<NavElement icon="fas fa-copy" link="/manage/{guildId}/transcripts" on:click={closeDropdown}>Transcripts
|
||||||
|
</NavElement>
|
||||||
|
|
||||||
{#if isAdmin}
|
{#if isAdmin}
|
||||||
<NavElement icon="fas fa-mouse-pointer" link="/manage/{guildId}/panels" on:click={closeDropdown}>Reaction Panels</NavElement>
|
<NavElement icon="fas fa-mouse-pointer" link="/manage/{guildId}/panels" on:click={closeDropdown}>Reaction Panels</NavElement>
|
||||||
<NavElement icon="fas fa-poll-h" link="/manage/{guildId}/forms" on:click={closeDropdown}>Forms</NavElement>
|
<NavElement icon="fas fa-poll-h" link="/manage/{guildId}/forms" on:click={closeDropdown}>Forms</NavElement>
|
||||||
<NavElement icon="fas fa-users" link="/manage/{guildId}/teams" on:click={closeDropdown}>Staff Teams</NavElement>
|
<NavElement icon="fas fa-users" link="/manage/{guildId}/teams" on:click={closeDropdown}>Staff Teams</NavElement>
|
||||||
|
<NavElement icon="fas fa-robot" link="/manage/{guildId}/integrations" on:click={closeDropdown}>
|
||||||
|
<div style="display: flex; gap:4px">
|
||||||
|
Integrations
|
||||||
|
<Badge>New!</Badge>
|
||||||
|
</div>
|
||||||
|
</NavElement>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<NavElement icon="fas fa-ticket-alt" link="/manage/{guildId}/tickets" on:click={closeDropdown}>Tickets</NavElement>
|
<NavElement icon="fas fa-ticket-alt" link="/manage/{guildId}/tickets" on:click={closeDropdown}>Tickets</NavElement>
|
||||||
@ -24,7 +32,7 @@
|
|||||||
<NavElement icon="fas fa-tags" link="/manage/{guildId}/tags" on:click={closeDropdown}>Tags</NavElement>
|
<NavElement icon="fas fa-tags" link="/manage/{guildId}/tags" on:click={closeDropdown}>Tags</NavElement>
|
||||||
|
|
||||||
{#if isAdmin}
|
{#if isAdmin}
|
||||||
<NavElement icon="fas fa-paint-brush" link="/manage/{guildId}/appearance" on:click={closeDropdown}>Customise Appearance</NavElement>
|
<NavElement icon="fas fa-paint-brush" link="/manage/{guildId}/appearance" on:click={closeDropdown}>Appearance</NavElement>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -39,6 +47,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import NavElement from "../components/NavElement.svelte";
|
import NavElement from "../components/NavElement.svelte";
|
||||||
|
import Badge from "../components/Badge.svelte";
|
||||||
|
|
||||||
export let guildId;
|
export let guildId;
|
||||||
export let dropdown;
|
export let dropdown;
|
||||||
|
@ -24,6 +24,12 @@ import Appearance from './views/Appearance.svelte';
|
|||||||
import Forms from './views/Forms.svelte';
|
import Forms from './views/Forms.svelte';
|
||||||
import StaffOverride from './views/StaffOverride.svelte';
|
import StaffOverride from './views/StaffOverride.svelte';
|
||||||
import BotStaff from './views/admin/BotStaff.svelte';
|
import BotStaff from './views/admin/BotStaff.svelte';
|
||||||
|
import Integrations from "./views/integrations/Integrations.svelte";
|
||||||
|
import IntegrationView from "./views/integrations/View.svelte";
|
||||||
|
import IntegrationCreate from "./views/integrations/Create.svelte";
|
||||||
|
import IntegrationConfigure from "./views/integrations/Configure.svelte";
|
||||||
|
import IntegrationActivate from "./views/integrations/Activate.svelte";
|
||||||
|
import IntegrationManage from "./views/integrations/Manage.svelte";
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
{name: '/', component: Index, layout: IndexLayout},
|
{name: '/', component: Index, layout: IndexLayout},
|
||||||
@ -92,6 +98,41 @@ export const routes = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'integrations',
|
||||||
|
nestedRoutes: [
|
||||||
|
{
|
||||||
|
name: 'index',
|
||||||
|
component: Integrations,
|
||||||
|
layout: ManageLayout,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'create',
|
||||||
|
component: IntegrationCreate,
|
||||||
|
layout: ManageLayout,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '/view/:integration',
|
||||||
|
component: IntegrationView,
|
||||||
|
layout: ManageLayout,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '/configure/:integration',
|
||||||
|
component: IntegrationConfigure,
|
||||||
|
layout: ManageLayout,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '/activate/:integration',
|
||||||
|
component: IntegrationActivate,
|
||||||
|
layout: ManageLayout,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '/manage/:integration',
|
||||||
|
component: IntegrationManage,
|
||||||
|
layout: ManageLayout,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
160
frontend/src/views/integrations/Activate.svelte
Normal file
160
frontend/src/views/integrations/Activate.svelte
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
<div class="parent">
|
||||||
|
<div class="content">
|
||||||
|
<div class="col-left">
|
||||||
|
<Card footer footerRight>
|
||||||
|
<span slot="title">Add {integration.name} To Your Server</span>
|
||||||
|
<div slot="body" class="body-wrapper">
|
||||||
|
<h3>Secrets</h3>
|
||||||
|
{#if integration.secrets !== undefined}
|
||||||
|
{#if integration.secrets.length === 0}
|
||||||
|
<p>This integration does not require any secrets.</p>
|
||||||
|
{:else}
|
||||||
|
<p>This integration requires you to provide some secrets. These will be sent to the server controlled by
|
||||||
|
the creator of {integration.name}, at: <code>{integration.webhook_url}</code></p>
|
||||||
|
<p>Note, the integration creator may change the server at any time.</p>
|
||||||
|
|
||||||
|
<div class="secret-container">
|
||||||
|
{#each integration.secrets as secret}
|
||||||
|
<div class="secret-input">
|
||||||
|
<Input col1 label="{secret.name}" placeholder="{secret.name}" bind:value={secretValues[secret.name]}/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div slot="footer">
|
||||||
|
<Button disabled={!allValuesFilled} on:click={activateIntegration}>Add to server</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {notifyError, withLoadingScreen} from '../../js/util'
|
||||||
|
import axios from "axios";
|
||||||
|
import {API_URL} from "../../js/constants";
|
||||||
|
import {setDefaultHeaders} from '../../includes/Auth.svelte'
|
||||||
|
import Card from "../../components/Card.svelte";
|
||||||
|
import Button from "../../components/Button.svelte";
|
||||||
|
import Input from "../../components/form/Input.svelte";
|
||||||
|
import {navigateTo} from "svelte-router-spa";
|
||||||
|
|
||||||
|
export let currentRoute;
|
||||||
|
let guildId = currentRoute.namedParams.id;
|
||||||
|
let integrationId = currentRoute.namedParams.integration;
|
||||||
|
|
||||||
|
let integration = {};
|
||||||
|
let secretValues = {};
|
||||||
|
|
||||||
|
let allValuesFilled = true;
|
||||||
|
$: secretValues, updateAllValuesFilled();
|
||||||
|
|
||||||
|
function updateAllValuesFilled() {
|
||||||
|
if (integration.secrets === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(secretValues).length !== integration.secrets.length) {
|
||||||
|
allValuesFilled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key in secretValues) {
|
||||||
|
if (secretValues[key] === '') {
|
||||||
|
allValuesFilled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allValuesFilled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function activateIntegration() {
|
||||||
|
let data = {
|
||||||
|
secrets: secretValues
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = await axios.post(`${API_URL}/api/${guildId}/integrations/${integrationId}`, data);
|
||||||
|
if (res.status !== 204) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateTo(`/manage/${guildId}/integrations?added=true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadIntegration() {
|
||||||
|
let res = await axios.get(`${API_URL}/api/integrations/view/${integrationId}`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
integration = res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
withLoadingScreen(async () => {
|
||||||
|
setDefaultHeaders();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
loadIntegration()
|
||||||
|
]);
|
||||||
|
|
||||||
|
updateAllValuesFilled();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.parent {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
width: 96%;
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-bottom: 5vh;
|
||||||
|
|
||||||
|
gap: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-left {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 1vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-input {
|
||||||
|
flex: 0 0 49%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 950px) {
|
||||||
|
.content {
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 2vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-left {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
71
frontend/src/views/integrations/Configure.svelte
Normal file
71
frontend/src/views/integrations/Configure.svelte
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
{#if data !== undefined}
|
||||||
|
<IntegrationEditor editMode {guildId} {data} on:delete={deleteIntegration}
|
||||||
|
on:submit={(e) => editIntegration(e.detail)} on:makePublic={(e) => makePublic(e.detail)}/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import IntegrationEditor from "../../components/IntegrationEditor.svelte";
|
||||||
|
import {setDefaultHeaders} from '../../includes/Auth.svelte'
|
||||||
|
import {notifyError, notifySuccess, withLoadingScreen} from "../../js/util";
|
||||||
|
import axios from "axios";
|
||||||
|
import {navigateTo} from "svelte-router-spa";
|
||||||
|
import {API_URL} from "../../js/constants";
|
||||||
|
|
||||||
|
export let currentRoute;
|
||||||
|
let guildId = currentRoute.namedParams.id;
|
||||||
|
let integrationId = currentRoute.namedParams.integration;
|
||||||
|
let freshlyCreated = currentRoute.queryParams.created === "true";
|
||||||
|
|
||||||
|
let data;
|
||||||
|
|
||||||
|
async function makePublic(data) {
|
||||||
|
const res = await axios.post(`${API_URL}/api/integrations/${integrationId}/public`, data);
|
||||||
|
if (res.status !== 204) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifySuccess("Your request to make this integration public has been submitted! It will be reviewed over the next few days.");
|
||||||
|
data.public = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editIntegration(data) {
|
||||||
|
const res = await axios.patch(`${API_URL}/api/integrations/${integrationId}`, data);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifySuccess("Integration updated");
|
||||||
|
await loadIntegration();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteIntegration() {
|
||||||
|
const res = await axios.delete(`${API_URL}/api/integrations/${integrationId}`);
|
||||||
|
if (res.status !== 204) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateTo(`/manage/${guildId}/integrations`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadIntegration() {
|
||||||
|
const res = await axios.get(`${API_URL}/api/integrations/view/${integrationId}/detail`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
withLoadingScreen(async () => {
|
||||||
|
setDefaultHeaders();
|
||||||
|
await loadIntegration();
|
||||||
|
|
||||||
|
if (freshlyCreated) {
|
||||||
|
notifySuccess("Your integration has been created successfully!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
27
frontend/src/views/integrations/Create.svelte
Normal file
27
frontend/src/views/integrations/Create.svelte
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<IntegrationEditor {guildId} on:submit={(e) => createIntegration(e.detail)} />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import IntegrationEditor from "../../components/IntegrationEditor.svelte";
|
||||||
|
import {setDefaultHeaders} from '../../includes/Auth.svelte'
|
||||||
|
import {notifyError, withLoadingScreen} from "../../js/util";
|
||||||
|
import axios from "axios";
|
||||||
|
import {navigateTo} from "svelte-router-spa";
|
||||||
|
import {API_URL} from "../../js/constants";
|
||||||
|
|
||||||
|
export let currentRoute;
|
||||||
|
let guildId = currentRoute.namedParams.id;
|
||||||
|
|
||||||
|
async function createIntegration(data) {
|
||||||
|
const res = await axios.post(`${API_URL}/api/integrations`, data);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateTo(`/manage/${guildId}/integrations/configure/${res.data.id}?created=true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
withLoadingScreen(async () => {
|
||||||
|
setDefaultHeaders();
|
||||||
|
});
|
||||||
|
</script>
|
249
frontend/src/views/integrations/Integrations.svelte
Normal file
249
frontend/src/views/integrations/Integrations.svelte
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
<div class="parent">
|
||||||
|
<div class="content">
|
||||||
|
<div class="container">
|
||||||
|
<div class="spread">
|
||||||
|
<h4 class="title">My Integrations</h4>
|
||||||
|
<Button icon="fas fa-server" on:click={() => navigateTo(`/manage/${guildId}/integrations/create`)}>Create
|
||||||
|
Integration
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="integrations my-integrations">
|
||||||
|
{#each ownedIntegrations as integration}
|
||||||
|
<div class="integration">
|
||||||
|
<Integration owned name={integration.name} {guildId} integrationId={integration.id}
|
||||||
|
imageUrl={generateProxyUrl(integration)} guildCount={integration.guild_count}>
|
||||||
|
<span slot="description">
|
||||||
|
{integration.description}
|
||||||
|
</span>
|
||||||
|
</Integration>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 class="title">Available Integrations</h4>
|
||||||
|
<div class="integrations">
|
||||||
|
<!-- Built in -->
|
||||||
|
{#if page === 1}
|
||||||
|
<div class="integration">
|
||||||
|
<Integration builtIn name="Bloxlink"
|
||||||
|
imageUrl="https://dbl-static.b-cdn.net/9bbd1f9504ddefc89606b19b290e9a0f.png"
|
||||||
|
viewLink="https://docs.ticketsbot.net/setup/placeholders#bloxlink">
|
||||||
|
<span slot="description">
|
||||||
|
Our Bloxlink integration inserts the Roblox usernames, profile URLs and more of your users into
|
||||||
|
ticket welcome messages automatically! This integration is automatically enabled in all servers, press the
|
||||||
|
View button below to check out the full list of placeholders you can use!
|
||||||
|
</span>
|
||||||
|
</Integration>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each availableIntegrations as integration}
|
||||||
|
<div class="integration">
|
||||||
|
<Integration name={integration.name} {guildId} integrationId={integration.id} imageUrl={generateProxyUrl(integration)}
|
||||||
|
added={integration.added} guildCount={integration.guild_count} on:remove={() => removeIntegration(integration.id)}>
|
||||||
|
<span slot="description">
|
||||||
|
{integration.description}
|
||||||
|
</span>
|
||||||
|
</Integration>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination">
|
||||||
|
<i class="fas fa-chevron-left pagination-chevron" class:disabled-chevron={page === 1} on:click={previousPage}></i>
|
||||||
|
<p>Page {page}</p>
|
||||||
|
<i class="fas fa-chevron-right pagination-chevron" class:disabled-chevron={!hasNextPage} on:click={nextPage}></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {notifyError, notifySuccess, withLoadingScreen} from '../../js/util'
|
||||||
|
import axios from "axios";
|
||||||
|
import {API_URL} from "../../js/constants";
|
||||||
|
import {setDefaultHeaders} from '../../includes/Auth.svelte'
|
||||||
|
import Integration from "../../components/manage/Integration.svelte";
|
||||||
|
import Button from "../../components/Button.svelte";
|
||||||
|
import {navigateTo} from "svelte-router-spa";
|
||||||
|
|
||||||
|
export let currentRoute;
|
||||||
|
let guildId = currentRoute.namedParams.id;
|
||||||
|
|
||||||
|
let freshlyRemoved = currentRoute.queryParams.removed === "true";
|
||||||
|
let freshlyAdded = currentRoute.queryParams.added === "true";
|
||||||
|
|
||||||
|
let ownedIntegrations = [];
|
||||||
|
let availableIntegrations = [];
|
||||||
|
|
||||||
|
const pageLimit = 20;
|
||||||
|
const builtInIntegrationCount = 1;
|
||||||
|
let page = 1;
|
||||||
|
|
||||||
|
function previousPage() {
|
||||||
|
if (page > 1) {
|
||||||
|
page--;
|
||||||
|
loadAvailableIntegrations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextPage() {
|
||||||
|
if (hasNextPage) {
|
||||||
|
page++;
|
||||||
|
loadAvailableIntegrations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasNextPage = true;
|
||||||
|
$: if (page === 1) {
|
||||||
|
hasNextPage = availableIntegrations.length + builtInIntegrationCount >= pageLimit;
|
||||||
|
} else {
|
||||||
|
hasNextPage = availableIntegrations.length >= pageLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateProxyUrl(integration) {
|
||||||
|
if (integration.image_url === null || integration.proxy_token === undefined || integration.proxy_token === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `https://image-cdn.ticketsbot.net/proxy?token=${integration.proxy_token}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeIntegration(integrationId) {
|
||||||
|
const res = await axios.delete(`${API_URL}/api/${guildId}/integrations/${integrationId}`);
|
||||||
|
if (res.status !== 204) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadAvailableIntegrations();
|
||||||
|
notifySuccess("Integration removed from server successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAvailableIntegrations() {
|
||||||
|
const res = await axios.get(`${API_URL}/api/${guildId}/integrations/available?page=${page}`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
availableIntegrations = res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadOwnedIntegrations() {
|
||||||
|
let res = await axios.get(`${API_URL}/api/integrations/self`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ownedIntegrations = res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
withLoadingScreen(async () => {
|
||||||
|
setDefaultHeaders();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
loadOwnedIntegrations(),
|
||||||
|
loadAvailableIntegrations()
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (freshlyAdded) {
|
||||||
|
notifySuccess("Integration added to server successfully!");
|
||||||
|
} else if (freshlyRemoved) {
|
||||||
|
notifySuccess("Integration removed from server successfully!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.parent {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 96%;
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-bottom: 5vh;
|
||||||
|
row-gap: 4vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integrations {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2%;
|
||||||
|
row-gap: 4vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration {
|
||||||
|
flex: 0 0 23.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-integrations {
|
||||||
|
margin-top: 2vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: white;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spread {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-chevron {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #3472f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-chevron {
|
||||||
|
color: #777 !important;
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 1180px) {
|
||||||
|
.integration {
|
||||||
|
flex: 0 0 32%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 930px) {
|
||||||
|
.integration {
|
||||||
|
flex: 0 0 49%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 576px) {
|
||||||
|
.integration {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
174
frontend/src/views/integrations/Manage.svelte
Normal file
174
frontend/src/views/integrations/Manage.svelte
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<div class="parent">
|
||||||
|
<div class="content">
|
||||||
|
<div class="col-left">
|
||||||
|
<Card footer footerRight>
|
||||||
|
<span slot="title">Edit {integration.name} Settings</span>
|
||||||
|
<div slot="body" class="body-wrapper">
|
||||||
|
<h3>Secrets</h3>
|
||||||
|
You cannot view previously submitted secrets for security reasons. The secret fields will show as empty,
|
||||||
|
even if you have previously submitted them.
|
||||||
|
|
||||||
|
{#if integration.secrets !== undefined}
|
||||||
|
{#if integration.secrets.length === 0}
|
||||||
|
<p>This integration does not require any secrets.</p>
|
||||||
|
{:else}
|
||||||
|
<p>This integration requires you to provide some secrets. These will be sent to the server controlled by
|
||||||
|
the creator of {integration.name}, at: <code>{integration.webhook_url}</code></p>
|
||||||
|
<p>Note, the integration creator may change the server at any time.</p>
|
||||||
|
|
||||||
|
<div class="secret-container">
|
||||||
|
{#each integration.secrets as secret}
|
||||||
|
<div class="secret-input">
|
||||||
|
<Input col1 label="{secret.name}" placeholder="{secret.name}" bind:value={secretValues[secret.name]}/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div slot="footer" style="gap: 12px">
|
||||||
|
<Button danger icon="fas fa-trash-can" on:click={removeIntegration}>Remove from server</Button>
|
||||||
|
<Button icon="fas fa-floppy-disk" disabled={!allValuesFilled} on:click={saveSecrets}>Save Integration</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {notifyError, notifySuccess, withLoadingScreen} from '../../js/util'
|
||||||
|
import axios from "axios";
|
||||||
|
import {API_URL} from "../../js/constants";
|
||||||
|
import {setDefaultHeaders} from '../../includes/Auth.svelte'
|
||||||
|
import Card from "../../components/Card.svelte";
|
||||||
|
import Button from "../../components/Button.svelte";
|
||||||
|
import Input from "../../components/form/Input.svelte";
|
||||||
|
import {navigateTo} from "svelte-router-spa";
|
||||||
|
|
||||||
|
export let currentRoute;
|
||||||
|
let guildId = currentRoute.namedParams.id;
|
||||||
|
let integrationId = currentRoute.namedParams.integration;
|
||||||
|
|
||||||
|
let integration = {};
|
||||||
|
let secretValues = {};
|
||||||
|
|
||||||
|
let allValuesFilled = true;
|
||||||
|
$: secretValues, updateAllValuesFilled();
|
||||||
|
|
||||||
|
function updateAllValuesFilled() {
|
||||||
|
if (integration.secrets === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(secretValues).length !== integration.secrets.length) {
|
||||||
|
allValuesFilled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let key in secretValues) {
|
||||||
|
if (secretValues[key] === '') {
|
||||||
|
allValuesFilled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allValuesFilled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSecrets() {
|
||||||
|
let data = {
|
||||||
|
secrets: secretValues
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await axios.patch(`${API_URL}/api/${guildId}/integrations/${integrationId}`, data);
|
||||||
|
if (res.status !== 204) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifySuccess('Integration settings saved successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeIntegration() {
|
||||||
|
const res = await axios.delete(`${API_URL}/api/${guildId}/integrations/${integrationId}`);
|
||||||
|
if (res.status !== 204) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateTo(`/manage/${guildId}/integrations?removed=true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadIntegration() {
|
||||||
|
let res = await axios.get(`${API_URL}/api/integrations/view/${integrationId}`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
integration = res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
withLoadingScreen(async () => {
|
||||||
|
setDefaultHeaders();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
loadIntegration()
|
||||||
|
]);
|
||||||
|
|
||||||
|
updateAllValuesFilled();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.parent {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
width: 96%;
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-bottom: 5vh;
|
||||||
|
|
||||||
|
gap: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-left {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 1vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-input {
|
||||||
|
flex: 0 0 49%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 950px) {
|
||||||
|
.content {
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 2vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-left {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
174
frontend/src/views/integrations/View.svelte
Normal file
174
frontend/src/views/integrations/View.svelte
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<div class="parent">
|
||||||
|
<div class="content">
|
||||||
|
<div class="col-left">
|
||||||
|
<Card footer footerRight>
|
||||||
|
<span slot="title">About {integration.name}</span>
|
||||||
|
<div slot="body" class="body-wrapper">
|
||||||
|
<span class="description">{integration.description}</span>
|
||||||
|
|
||||||
|
<p style="padding-top: 5px">When a user opens a ticket, a request containing the ticket opener's user ID will
|
||||||
|
be sent to the following URL, controlled by the integration author:</p>
|
||||||
|
<input readonly value={integration.webhook_url} class="form-input"/>
|
||||||
|
|
||||||
|
|
||||||
|
{#if privacy_policy_url === null}
|
||||||
|
<p>The integration author has not provided a privacy policy.</p>
|
||||||
|
{:else}
|
||||||
|
<p>The integration author has provided a privacy policy, accessible at
|
||||||
|
<a href="{privacy_policy_url}" class="link-blue">{privacy_policy_url}</a>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div slot="footer">
|
||||||
|
{#if isActive}
|
||||||
|
<Button on:click={removeIntegration} danger>Remove from server</Button>
|
||||||
|
{:else}
|
||||||
|
<Button on:click={() => navigateTo(`/manage/${guildId}/integrations/activate/${integrationId}`)}>
|
||||||
|
Add to server
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div class="col-right">
|
||||||
|
<Card footer={false} fill={false}>
|
||||||
|
<span slot="title">Placeholders</span>
|
||||||
|
<div slot="body">
|
||||||
|
<p>The following placeholders are available to user in welcome messages through the <i>{integration.name}</i>
|
||||||
|
integration:</p>
|
||||||
|
|
||||||
|
<div class="placeholders">
|
||||||
|
{#if integration.placeholders}
|
||||||
|
{#each integration.placeholders as placeholder}
|
||||||
|
<Badge>%{placeholder.name}%</Badge>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {notifyError, notifySuccess, withLoadingScreen} from '../../js/util'
|
||||||
|
import axios from "axios";
|
||||||
|
import {API_URL} from "../../js/constants";
|
||||||
|
import {setDefaultHeaders} from '../../includes/Auth.svelte'
|
||||||
|
import Card from "../../components/Card.svelte";
|
||||||
|
import Button from "../../components/Button.svelte";
|
||||||
|
import Badge from "../../components/Badge.svelte";
|
||||||
|
import {Navigate, navigateTo} from "svelte-router-spa";
|
||||||
|
|
||||||
|
export let currentRoute;
|
||||||
|
let guildId = currentRoute.namedParams.id;
|
||||||
|
let integrationId = currentRoute.namedParams.integration;
|
||||||
|
|
||||||
|
let integration = {};
|
||||||
|
let isActive = false;
|
||||||
|
let privacy_policy_url = null;
|
||||||
|
|
||||||
|
async function removeIntegration() {
|
||||||
|
const res = await axios.delete(`${API_URL}/api/${guildId}/integrations/${integrationId}`);
|
||||||
|
if (res.status !== 204) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateTo(`/manage/${guildId}/integrations?removed=true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadIntegration() {
|
||||||
|
let res = await axios.get(`${API_URL}/api/integrations/view/${integrationId}`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
integration = res.data;
|
||||||
|
|
||||||
|
if (integration.privacy_policy_url !== null) {
|
||||||
|
let tmp = new URL(integration.privacy_policy_url);
|
||||||
|
if (tmp.protocol === "http:" || tmp.protocol === "https:") {
|
||||||
|
privacy_policy_url = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadIsActive() {
|
||||||
|
let res = await axios.get(`${API_URL}/api/${guildId}/integrations/${integrationId}`);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
notifyError(res.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive = res.data.active;
|
||||||
|
}
|
||||||
|
|
||||||
|
withLoadingScreen(async () => {
|
||||||
|
setDefaultHeaders();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
loadIntegration(),
|
||||||
|
loadIsActive()
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.parent {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 96%;
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-bottom: 5vh;
|
||||||
|
|
||||||
|
gap: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-left {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-right {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholders {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
border-bottom: 1px solid #777;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 950px) {
|
||||||
|
.content {
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: 2vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-left, .col-right {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
9
go.mod
9
go.mod
@ -5,10 +5,10 @@ go 1.18
|
|||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v0.3.1
|
github.com/BurntSushi/toml v0.3.1
|
||||||
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc
|
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc
|
||||||
github.com/TicketsBot/common v0.0.0-20220615205931-a6a31e73b52a
|
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42
|
||||||
github.com/TicketsBot/database v0.0.0-20220627203133-dd19fe34f094
|
github.com/TicketsBot/database v0.0.0-20220710120946-a97157c1ca8c
|
||||||
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
|
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
|
||||||
github.com/TicketsBot/worker v0.0.0-20220627203254-f37bdb40b39a
|
github.com/TicketsBot/worker v0.0.0-20220710121124-cd5ec72739f9
|
||||||
github.com/apex/log v1.1.2
|
github.com/apex/log v1.1.2
|
||||||
github.com/getsentry/sentry-go v0.13.0
|
github.com/getsentry/sentry-go v0.13.0
|
||||||
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
|
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
|
||||||
@ -19,12 +19,13 @@ require (
|
|||||||
github.com/go-redis/redis/v8 v8.11.3
|
github.com/go-redis/redis/v8 v8.11.3
|
||||||
github.com/go-redis/redis_rate/v9 v9.1.1
|
github.com/go-redis/redis_rate/v9 v9.1.1
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||||
|
github.com/google/uuid v1.1.1
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/jackc/pgtype v1.4.0
|
github.com/jackc/pgtype v1.4.0
|
||||||
github.com/jackc/pgx/v4 v4.7.1
|
github.com/jackc/pgx/v4 v4.7.1
|
||||||
github.com/pasztorpisti/qs v0.0.0-20171216220353-8d6c33ee906c
|
github.com/pasztorpisti/qs v0.0.0-20171216220353-8d6c33ee906c
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/rxdn/gdl v0.0.0-20220621165443-28e214d254c1
|
github.com/rxdn/gdl v0.0.0-20220702190021-560b2ab99d25
|
||||||
github.com/sirupsen/logrus v1.5.0
|
github.com/sirupsen/logrus v1.5.0
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||||
)
|
)
|
||||||
|
10
go.sum
10
go.sum
@ -5,14 +5,17 @@ github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc h1:n15W8
|
|||||||
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc/go.mod h1:2KcfHS0JnSsgcxZBs3NyWMXNQzEo67mBSGOyzHPWOCc=
|
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc/go.mod h1:2KcfHS0JnSsgcxZBs3NyWMXNQzEo67mBSGOyzHPWOCc=
|
||||||
github.com/TicketsBot/common v0.0.0-20220615205931-a6a31e73b52a h1:SwA18cDURmnXSrKBdetNVanSsyJBMtyosDzvgYMpKP4=
|
github.com/TicketsBot/common v0.0.0-20220615205931-a6a31e73b52a h1:SwA18cDURmnXSrKBdetNVanSsyJBMtyosDzvgYMpKP4=
|
||||||
github.com/TicketsBot/common v0.0.0-20220615205931-a6a31e73b52a/go.mod h1:ZAoYcDD7SQLTsZT7dbo/X0J256+pogVRAReunCGng+U=
|
github.com/TicketsBot/common v0.0.0-20220615205931-a6a31e73b52a/go.mod h1:ZAoYcDD7SQLTsZT7dbo/X0J256+pogVRAReunCGng+U=
|
||||||
github.com/TicketsBot/database v0.0.0-20220627203133-dd19fe34f094 h1:a272Wj4At5XjIjLoRAdn4PwfZ288+A4QzwTvFAR6fho=
|
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42/go.mod h1:WxHh6bY7KhIqdayeOp5f0Zj2NNi/7QqCQfMEqHnpdAM=
|
||||||
github.com/TicketsBot/database v0.0.0-20220627203133-dd19fe34f094/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
|
github.com/TicketsBot/database v0.0.0-20220710120946-a97157c1ca8c h1:WD3W0cUkIpMI5XUoGDQ88uYJnljZXxqv0e9TWCt6MqI=
|
||||||
|
github.com/TicketsBot/database v0.0.0-20220710120946-a97157c1ca8c/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
|
||||||
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=
|
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s=
|
||||||
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c=
|
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c=
|
||||||
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=
|
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=
|
||||||
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261/go.mod h1:2zPxDAN2TAPpxUPjxszjs3QFKreKrQh5al/R3cMXmYk=
|
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261/go.mod h1:2zPxDAN2TAPpxUPjxszjs3QFKreKrQh5al/R3cMXmYk=
|
||||||
github.com/TicketsBot/worker v0.0.0-20220627203254-f37bdb40b39a h1:cx2YtngcJ7KpOBm/QotWKHb2XZbvUDJwD9azBl31e/k=
|
github.com/TicketsBot/worker v0.0.0-20220627203254-f37bdb40b39a h1:cx2YtngcJ7KpOBm/QotWKHb2XZbvUDJwD9azBl31e/k=
|
||||||
github.com/TicketsBot/worker v0.0.0-20220627203254-f37bdb40b39a/go.mod h1:R70+F86Z+UlretKMxOX1jRqCwvVlvuem9UAyAL4EiG8=
|
github.com/TicketsBot/worker v0.0.0-20220627203254-f37bdb40b39a/go.mod h1:R70+F86Z+UlretKMxOX1jRqCwvVlvuem9UAyAL4EiG8=
|
||||||
|
github.com/TicketsBot/worker v0.0.0-20220710121124-cd5ec72739f9 h1:kxjeQ0OtHMh55y0xXuPmq1F24pAuH8V6Y4txk0ZJkAM=
|
||||||
|
github.com/TicketsBot/worker v0.0.0-20220710121124-cd5ec72739f9/go.mod h1:QOawjBdVtnwfMkIaCCjYw/WleX2Rf5asNHk4OdbRJfs=
|
||||||
github.com/apex/log v1.1.2 h1:bnDuVoi+o98wOdVqfEzNDlY0tcmBia7r4YkjS9EqGYk=
|
github.com/apex/log v1.1.2 h1:bnDuVoi+o98wOdVqfEzNDlY0tcmBia7r4YkjS9EqGYk=
|
||||||
github.com/apex/log v1.1.2/go.mod h1:SyfRweFO+TlkIJ3DVizTSeI1xk7jOIIqOnUPZQTTsww=
|
github.com/apex/log v1.1.2/go.mod h1:SyfRweFO+TlkIJ3DVizTSeI1xk7jOIIqOnUPZQTTsww=
|
||||||
github.com/apex/logs v0.0.3/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
|
github.com/apex/logs v0.0.3/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
|
||||||
@ -118,6 +121,7 @@ github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
|||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
@ -273,6 +277,8 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK
|
|||||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||||
github.com/rxdn/gdl v0.0.0-20220621165443-28e214d254c1 h1:1/q9ohADLOrMQwhTfxRplPiMQ6EmVnJB+pWb+SLHd0c=
|
github.com/rxdn/gdl v0.0.0-20220621165443-28e214d254c1 h1:1/q9ohADLOrMQwhTfxRplPiMQ6EmVnJB+pWb+SLHd0c=
|
||||||
github.com/rxdn/gdl v0.0.0-20220621165443-28e214d254c1/go.mod h1:HtxfLp4OaoPoDJHQ4JOx/QeLH2d40VgT3wNOf7ETsRE=
|
github.com/rxdn/gdl v0.0.0-20220621165443-28e214d254c1/go.mod h1:HtxfLp4OaoPoDJHQ4JOx/QeLH2d40VgT3wNOf7ETsRE=
|
||||||
|
github.com/rxdn/gdl v0.0.0-20220702190021-560b2ab99d25 h1:9G1HcBG9hLFT7+FKiiVECF+Z2Y0yt3BrSNnuDXcRb7A=
|
||||||
|
github.com/rxdn/gdl v0.0.0-20220702190021-560b2ab99d25/go.mod h1:HtxfLp4OaoPoDJHQ4JOx/QeLH2d40VgT3wNOf7ETsRE=
|
||||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
github.com/schollz/progressbar/v3 v3.8.2 h1:2kZJwZCpb+E/V79kGO7daeq+hUwUJW0A5QD1Wv455dA=
|
github.com/schollz/progressbar/v3 v3.8.2 h1:2kZJwZCpb+E/V79kGO7daeq+hUwUJW0A5QD1Wv455dA=
|
||||||
github.com/schollz/progressbar/v3 v3.8.2/go.mod h1:9KHLdyuXczIsyStQwzvW8xiELskmX7fQMaZdN23nAv8=
|
github.com/schollz/progressbar/v3 v3.8.2/go.mod h1:9KHLdyuXczIsyStQwzvW8xiELskmX7fQMaZdN23nAv8=
|
||||||
|
19
utils/imageproxy.go
Normal file
19
utils/imageproxy.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TicketsBot/GoPanel/config"
|
||||||
|
"github.com/golang-jwt/jwt"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateImageProxyToken(imageUrl string) (string, error) {
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"url": imageUrl,
|
||||||
|
"request_id": uuid.New().String(),
|
||||||
|
"exp": strconv.FormatInt(time.Now().Add(time.Second*30).Unix(), 10),
|
||||||
|
})
|
||||||
|
|
||||||
|
return token.SignedString([]byte(config.Conf.Bot.ImageProxySecret))
|
||||||
|
}
|
12
utils/netutils.go
Normal file
12
utils/netutils.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "net/url"
|
||||||
|
|
||||||
|
func GetUrlHost(rawUrl string) string {
|
||||||
|
parsed, err := url.Parse(rawUrl)
|
||||||
|
if err != nil {
|
||||||
|
return "Invalid URL"
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.Hostname()
|
||||||
|
}
|
@ -11,3 +11,7 @@ func ValueOrZero[T any](v *T) T {
|
|||||||
return *v
|
return *v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Slice[T any](v ...T) []T {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user