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_customisation "github.com/TicketsBot/GoPanel/app/http/endpoints/api/customisation"
|
||||
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_settings "github.com/TicketsBot/GoPanel/app/http/endpoints/api/settings"
|
||||
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.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))
|
||||
@ -156,6 +167,12 @@ func StartServer() {
|
||||
guildAuthApiAdmin.GET("/staff-override", api_override.GetOverrideHandler)
|
||||
guildAuthApiAdmin.POST("/staff-override", api_override.CreateOverrideHandler)
|
||||
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)
|
||||
|
@ -14,25 +14,28 @@ func ContextForGuild(guildId uint64) (ctx BotContext, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
var keyPrefix string
|
||||
|
||||
if isWhitelabel {
|
||||
res, err := dbclient.Client.Whitelabel.GetByBotId(whitelabelBotId)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
ctx.BotId = res.BotId
|
||||
ctx.Token = res.Token
|
||||
keyPrefix = fmt.Sprintf("ratelimiter:%d", whitelabelBotId)
|
||||
rateLimiter := ratelimit.NewRateLimiter(ratelimit.NewRedisStore(redis.Client.Client, fmt.Sprintf("ratelimiter:%d", whitelabelBotId)), 1)
|
||||
|
||||
return BotContext{
|
||||
BotId: res.BotId,
|
||||
Token: res.Token,
|
||||
RateLimiter: rateLimiter,
|
||||
}, nil
|
||||
} else {
|
||||
ctx.BotId = config.Conf.Bot.Id
|
||||
ctx.Token = config.Conf.Bot.Token
|
||||
keyPrefix = "ratelimiter:public"
|
||||
return PublicContext(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func PublicContext() BotContext {
|
||||
return BotContext{
|
||||
BotId: config.Conf.Bot.Id,
|
||||
Token: config.Conf.Bot.Token,
|
||||
RateLimiter: ratelimit.NewRateLimiter(ratelimit.NewRedisStore(redis.Client.Client, "ratelimiter:public"), 1),
|
||||
}
|
||||
|
||||
// TODO: Large sharding buckets
|
||||
ctx.RateLimiter = ratelimit.NewRateLimiter(ratelimit.NewRedisStore(redis.Client.Client, keyPrefix), 1)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -54,14 +54,17 @@ type (
|
||||
}
|
||||
|
||||
Bot struct {
|
||||
Id uint64
|
||||
Token string
|
||||
PremiumLookupProxyUrl string `toml:"premium-lookup-proxy-url"`
|
||||
PremiumLookupProxyKey string `toml:"premium-lookup-proxy-key"`
|
||||
ObjectStore string
|
||||
AesKey string `toml:"aes-key"`
|
||||
ProxyUrl string `toml:"discord-proxy-url"`
|
||||
RenderServiceUrl string `toml:"render-service-url"`
|
||||
Id uint64
|
||||
Token string
|
||||
PremiumLookupProxyUrl string `toml:"premium-lookup-proxy-url"`
|
||||
PremiumLookupProxyKey string `toml:"premium-lookup-proxy-key"`
|
||||
ObjectStore string
|
||||
AesKey string `toml:"aes-key"`
|
||||
ProxyUrl string `toml:"discord-proxy-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 {
|
||||
@ -74,11 +77,6 @@ type (
|
||||
Cache struct {
|
||||
Uri string
|
||||
}
|
||||
|
||||
Referral struct {
|
||||
Show bool
|
||||
Link string
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
@ -126,6 +124,7 @@ func fromEnvvar() {
|
||||
botId, _ := strconv.ParseUint(os.Getenv("BOT_ID"), 10, 64)
|
||||
redisPort, _ := strconv.Atoi(os.Getenv("REDIS_PORT"))
|
||||
redisThreads, _ := strconv.Atoi(os.Getenv("REDIS_THREADS"))
|
||||
publicIntegrationRequestWebhookId, _ := strconv.ParseUint(os.Getenv("PUBLIC_INTEGRATION_REQUEST_WEBHOOK_ID"), 10, 64)
|
||||
|
||||
Conf = Config{
|
||||
Admins: admins,
|
||||
@ -157,14 +156,17 @@ func fromEnvvar() {
|
||||
Uri: os.Getenv("DATABASE_URI"),
|
||||
},
|
||||
Bot: Bot{
|
||||
Id: botId,
|
||||
Token: os.Getenv("BOT_TOKEN"),
|
||||
PremiumLookupProxyUrl: os.Getenv("PREMIUM_PROXY_URL"),
|
||||
PremiumLookupProxyKey: os.Getenv("PREMIUM_PROXY_KEY"),
|
||||
ObjectStore: os.Getenv("LOG_ARCHIVER_URL"),
|
||||
AesKey: os.Getenv("LOG_AES_KEY"),
|
||||
ProxyUrl: os.Getenv("DISCORD_PROXY_URL"),
|
||||
RenderServiceUrl: os.Getenv("RENDER_SERVICE_URL"),
|
||||
Id: botId,
|
||||
Token: os.Getenv("BOT_TOKEN"),
|
||||
PremiumLookupProxyUrl: os.Getenv("PREMIUM_PROXY_URL"),
|
||||
PremiumLookupProxyKey: os.Getenv("PREMIUM_PROXY_KEY"),
|
||||
ObjectStore: os.Getenv("LOG_ARCHIVER_URL"),
|
||||
AesKey: os.Getenv("LOG_AES_KEY"),
|
||||
ProxyUrl: os.Getenv("DISCORD_PROXY_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{
|
||||
Host: os.Getenv("REDIS_HOST"),
|
||||
|
@ -13,7 +13,8 @@ import (
|
||||
var Client *database.Database
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
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;
|
||||
padding: 0 !important;
|
||||
box-sizing: border-box;
|
||||
/*font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;*/
|
||||
}
|
||||
|
||||
label {
|
||||
@ -62,3 +61,7 @@ button:not(:disabled):active {
|
||||
button:focus {
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
code {
|
||||
color: #f9a8d4;
|
||||
}
|
||||
|
@ -1,17 +1,19 @@
|
||||
<div class="badge">
|
||||
<div class="badge" style="--badge-background-color: {colour}">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
export let colour = '#3472f7';
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
background-color: #3472f7;
|
||||
background-color: var(--badge-background-color, #3472f7);
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
padding: 0 4px;
|
||||
margin-left: 4px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
</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}
|
||||
<i class="{icon}"></i>
|
||||
{/if}
|
||||
<span class="content">
|
||||
<slot/>
|
||||
</span>
|
||||
{#if !iconOnly}
|
||||
<span class="content">
|
||||
<slot/>
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<script>
|
||||
@ -13,6 +15,7 @@
|
||||
export let disabled = false;
|
||||
export let type = "submit";
|
||||
export let danger = false;
|
||||
export let iconOnly = false;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@ -65,4 +68,9 @@
|
||||
background-color: #c32232 !important;
|
||||
border-color: #c32232 !important;
|
||||
}
|
||||
|
||||
.iconOnly {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
</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;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
:global(.link) {
|
||||
display: flex;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:global(.link-blue) {
|
||||
color: #3472f7;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.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 -->
|
||||
|
||||
{#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}
|
||||
|
||||
<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}
|
||||
<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-users" link="/manage/{guildId}/teams" on:click={closeDropdown}>Staff Teams</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-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}
|
||||
|
||||
<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>
|
||||
|
||||
{#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}
|
||||
</div>
|
||||
</div>
|
||||
@ -39,6 +47,7 @@
|
||||
|
||||
<script>
|
||||
import NavElement from "../components/NavElement.svelte";
|
||||
import Badge from "../components/Badge.svelte";
|
||||
|
||||
export let guildId;
|
||||
export let dropdown;
|
||||
@ -47,11 +56,11 @@
|
||||
$: isAdmin = permissionLevel >= 2;
|
||||
|
||||
function dropdownNav() {
|
||||
dropdown.update(v => !v);
|
||||
dropdown.update(v => !v);
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
dropdown.set(false);
|
||||
dropdown.set(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -24,6 +24,12 @@ import Appearance from './views/Appearance.svelte';
|
||||
import Forms from './views/Forms.svelte';
|
||||
import StaffOverride from './views/StaffOverride.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 = [
|
||||
{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 (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc
|
||||
github.com/TicketsBot/common v0.0.0-20220615205931-a6a31e73b52a
|
||||
github.com/TicketsBot/database v0.0.0-20220627203133-dd19fe34f094
|
||||
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42
|
||||
github.com/TicketsBot/database v0.0.0-20220710120946-a97157c1ca8c
|
||||
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/getsentry/sentry-go v0.13.0
|
||||
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_rate/v9 v9.1.1
|
||||
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/jackc/pgtype v1.4.0
|
||||
github.com/jackc/pgx/v4 v4.7.1
|
||||
github.com/pasztorpisti/qs v0.0.0-20171216220353-8d6c33ee906c
|
||||
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
|
||||
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/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/database v0.0.0-20220627203133-dd19fe34f094 h1:a272Wj4At5XjIjLoRAdn4PwfZ288+A4QzwTvFAR6fho=
|
||||
github.com/TicketsBot/database v0.0.0-20220627203133-dd19fe34f094/go.mod h1:F57cywrZsnper1cy56Bx0c/HEsxQBLHz3Pl98WXblWw=
|
||||
github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42/go.mod h1:WxHh6bY7KhIqdayeOp5f0Zj2NNi/7QqCQfMEqHnpdAM=
|
||||
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/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/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/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/go.mod h1:SyfRweFO+TlkIJ3DVizTSeI1xk7jOIIqOnUPZQTTsww=
|
||||
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
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/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-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/schollz/progressbar/v3 v3.8.2 h1:2kZJwZCpb+E/V79kGO7daeq+hUwUJW0A5QD1Wv455dA=
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func Slice[T any](v ...T) []T {
|
||||
return v
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user