Whitelabel slash commands

This commit is contained in:
rxdn 2020-12-24 20:42:26 +00:00
parent a8d14bf971
commit 61621906c0
16 changed files with 397 additions and 163 deletions

View File

@ -1,10 +1,12 @@
package api
import (
"context"
"fmt"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/common/permission"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
"strconv"
"strings"
)
@ -21,10 +23,11 @@ func GetPermissionLevel(ctx *gin.Context) {
return
}
// TODO: Check whether the bot is in the guild to prevent us getting maliciously 429'd
// TODO: This is insanely inefficient
levels := make(map[string]permission.PermissionLevel)
group, _ := errgroup.WithContext(context.Background())
for _, raw := range guilds {
guildId, err := strconv.ParseUint(raw, 10, 64)
if err != nil {
@ -35,8 +38,16 @@ func GetPermissionLevel(ctx *gin.Context) {
return
}
level := utils.GetPermissionLevel(guildId, userId)
group.Go(func() error {
level, err := utils.GetPermissionLevel(guildId, userId)
levels[strconv.FormatUint(guildId, 10)] = level
return err
})
}
if err := group.Wait(); err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
ctx.JSON(200, gin.H{

View File

@ -3,6 +3,7 @@ package api
import (
"context"
"github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/common/permission"
"github.com/gin-gonic/gin"
@ -39,6 +40,12 @@ func GetGuilds(ctx *gin.Context) {
g := g
group.Go(func() error {
// verify bot is in guild
_, ok := cache.Instance.GetGuild(g.GuildId, false)
if !ok {
return nil
}
fakeGuild := guild.Guild{
Id: g.GuildId,
Owner: g.Owner,
@ -49,7 +56,12 @@ func GetGuilds(ctx *gin.Context) {
fakeGuild.OwnerId = userId
}
if utils.GetPermissionLevel(g.GuildId, userId) >= permission.Support {
permLevel, err := utils.GetPermissionLevel(g.GuildId, userId)
if err != nil {
return err
}
if permLevel >= permission.Support {
lock.Lock()
adminGuilds = append(adminGuilds, wrappedGuild{
Id: g.GuildId,
@ -58,6 +70,7 @@ func GetGuilds(ctx *gin.Context) {
})
lock.Unlock()
}
return nil
})
}

View File

@ -0,0 +1,72 @@
package api
import (
"github.com/TicketsBot/GoPanel/botcontext"
"github.com/TicketsBot/GoPanel/database"
command "github.com/TicketsBot/worker/bot/command/impl"
"github.com/gin-gonic/gin"
"github.com/rxdn/gdl/rest"
"time"
)
func WhitelabelCreateInteractions(ctx *gin.Context) {
userId := ctx.Keys["userid"].(uint64)
// Get bot
bot, err := database.Client.Whitelabel.GetByUserId(userId)
if err != nil {
ctx.JSON(500, gin.H{
"success": false,
"error": err.Error(),
})
return
}
// Ensure bot exists
if bot.BotId == 0 {
ctx.JSON(404, gin.H{
"success": false,
"error": "No bot found",
})
return
}
botContext, err := botcontext.ContextForGuild(0)
if err != nil {
ctx.JSON(500, gin.H{
"success": false,
"error": err.Error(),
})
return
}
for _, cmd := range command.Commands {
properties := cmd.Properties()
if properties.MessageOnly || properties.AdminOnly || properties.HelperOnly {
continue
}
option := command.BuildOption(cmd)
data := rest.CreateCommandData{
Name: option.Name,
Description: option.Description,
Options: option.Options,
}
if _, err := rest.CreateGlobalCommand(bot.Token, botContext.RateLimiter, bot.BotId, data); err != nil {
ctx.JSON(500, gin.H{
"success": false,
"error": err.Error(),
})
return
}
time.Sleep(time.Second)
}
ctx.JSON(200, gin.H{
"success": true,
})
}

View File

@ -0,0 +1,53 @@
package api
import (
"github.com/TicketsBot/GoPanel/database"
"github.com/gin-gonic/gin"
)
func WhitelabelGetPublicKey(ctx *gin.Context) {
type data struct {
PublicKey string `json:"public_key"`
}
userId := ctx.Keys["userid"].(uint64)
// Get bot
bot, err := database.Client.Whitelabel.GetByUserId(userId)
if err != nil {
ctx.JSON(500, gin.H{
"success": false,
"error": err.Error(),
})
return
}
// Ensure bot exists
if bot.BotId == 0 {
ctx.JSON(404, gin.H{
"success": false,
"error": "No bot found",
})
return
}
key, err := database.Client.WhitelabelKeys.Get(bot.BotId)
if err != nil {
ctx.JSON(500, gin.H{
"success": false,
"error": err.Error(),
})
return
}
if key == "" {
ctx.JSON(404, gin.H{
"success": false,
})
} else {
ctx.JSON(200, gin.H{
"success": true,
"key": key,
})
}
}

View File

@ -0,0 +1,65 @@
package api
import (
"encoding/hex"
"github.com/TicketsBot/GoPanel/database"
"github.com/gin-gonic/gin"
)
func WhitelabelPostPublicKey(ctx *gin.Context) {
type data struct {
PublicKey string `json:"public_key"`
}
userId := ctx.Keys["userid"].(uint64)
// Get bot
bot, err := database.Client.Whitelabel.GetByUserId(userId)
if err != nil {
ctx.JSON(500, gin.H{
"success": false,
"error": err.Error(),
})
return
}
// Ensure bot exists
if bot.BotId == 0 {
ctx.JSON(404, gin.H{
"success": false,
"error": "No bot found",
})
return
}
// Parse status
var body data
if err := ctx.BindJSON(&body); err != nil {
ctx.JSON(400, gin.H{
"success": false,
"error": "No public key provided",
})
return
}
bytes, err := hex.DecodeString(body.PublicKey)
if err != nil || len(bytes) != 32 {
ctx.JSON(400, gin.H{
"success": false,
"error": "Invalid public key",
})
return
}
if err := database.Client.WhitelabelKeys.Set(bot.BotId, body.PublicKey); err != nil {
ctx.JSON(500, gin.H{
"success": false,
"error": err.Error(),
})
return
}
ctx.JSON(200, gin.H{
"success": true,
})
}

View File

@ -59,7 +59,13 @@ func LogViewHandler(ctx *gin.Context) {
}
// Verify the user has permissions to be here
if utils.GetPermissionLevel(guildId, userId) < permission.Support && ticket.UserId != userId {
permLevel, err := utils.GetPermissionLevel(guildId, userId)
if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
if permLevel < permission.Support && ticket.UserId != userId {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
return
}

View File

@ -56,7 +56,13 @@ func ModmailLogViewHandler(ctx *gin.Context) {
}
// Verify the user has permissions to be here
if utils.GetPermissionLevel(guildId, userId) < permission.Support && archive.UserId != userId {
permLevel, err := utils.GetPermissionLevel(guildId, userId)
if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
if permLevel < permission.Support && archive.UserId != userId {
utils.ErrorPage(ctx, 403, "You do not have permission to view this archive")
return
}

View File

@ -4,7 +4,6 @@ import (
"fmt"
"github.com/TicketsBot/GoPanel/botcontext"
"github.com/TicketsBot/GoPanel/rpc"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/common/permission"
"github.com/TicketsBot/common/premium"
@ -113,11 +112,15 @@ func WebChatWs(ctx *gin.Context) {
return
}
// Get object for selected guild
guild, _ := cache.Instance.GetGuild(guildIdParsed, false)
// Verify the user has permissions to be here
if utils.GetPermissionLevel(guild.Id, userId) < permission.Admin {
permLevel, err := utils.GetPermissionLevel(guildIdParsed, userId)
if err != nil {
fmt.Println(err.Error())
conn.Close()
return
}
if permLevel < permission.Admin {
fmt.Println(err.Error())
conn.Close()
return

View File

@ -46,7 +46,14 @@ func AuthenticateGuild(isApiMethod bool, requiredPermissionLevel permission.Perm
// Verify the user has permissions to be here
userId := ctx.Keys["userid"].(uint64)
if utils.GetPermissionLevel(guild.Id, userId) < requiredPermissionLevel {
permLevel, err := utils.GetPermissionLevel(guild.Id, userId)
if err != nil {
ctx.JSON(500, utils.ErrorToResponse(err))
return
}
if permLevel < requiredPermissionLevel {
if isApiMethod {
ctx.AbortWithStatusJSON(403, gin.H{
"success": false,

View File

@ -47,7 +47,7 @@ func StartServer() {
router.Use(static.Serve("/assets/", static.LocalFile("./public/static", false)))
router.Use(gin.Recovery())
router.Use(createLimiter(600, time.Minute * 10))
router.Use(createLimiter(600, time.Minute*10))
// Register templates
router.HTMLRender = createRenderer()
@ -56,7 +56,6 @@ func StartServer() {
router.GET("/callback", root.CallbackHandler)
router.GET("/manage/:id/logs/view/:ticket", manage.LogViewHandler) // we check in the actual handler bc of a custom redirect
router.GET("/manage/:id/logs/modmail/view/:uuid", manage.ModmailLogViewHandler) // we check in the actual handler bc of a custom redirect
authorized := router.Group("/", middleware.AuthenticateCookie)
{
@ -71,7 +70,6 @@ func StartServer() {
authenticateGuildAdmin.GET("/manage/:id/settings", manage.SettingsHandler)
authenticateGuildSupport.GET("/manage/:id/logs", manage.LogsHandler)
authenticateGuildSupport.GET("/manage/:id/logs/modmail", manage.ModmailLogsHandler)
authenticateGuildSupport.GET("/manage/:id/blacklist", manage.BlacklistHandler)
authenticateGuildAdmin.GET("/manage/:id/panels", manage.PanelHandler)
authenticateGuildSupport.GET("/manage/:id/tags", manage.TagsHandler)
@ -139,10 +137,12 @@ func StartServer() {
whitelabelGroup.GET("/", api_whitelabel.WhitelabelGet)
whitelabelApiGroup.GET("/errors", api_whitelabel.WhitelabelGetErrors)
whitelabelApiGroup.GET("/guilds", api_whitelabel.WhitelabelGetGuilds)
whitelabelApiGroup.POST("/modmail", api_whitelabel.WhitelabelModmailPost)
whitelabelApiGroup.GET("/public-key", api_whitelabel.WhitelabelGetPublicKey)
whitelabelApiGroup.POST("/public-key", api_whitelabel.WhitelabelPostPublicKey)
whitelabelApiGroup.POST("/create-interactions", api_whitelabel.WhitelabelCreateInteractions)
whitelabelApiGroup.Group("/").Use(createLimiter(10, time.Minute)).POST("/", api_whitelabel.WhitelabelPost)
whitelabelApiGroup.Group("/").Use(createLimiter(1, time.Second * 5)).POST("/status", api_whitelabel.WhitelabelStatusPost)
whitelabelApiGroup.Group("/").Use(createLimiter(1, time.Second*5)).POST("/status", api_whitelabel.WhitelabelStatusPost)
}
}
@ -177,6 +177,7 @@ func addMainTemplate(renderer multitemplate.Renderer, name string, extra ...stri
"./public/templates/includes/head.tmpl",
"./public/templates/includes/sidebar.tmpl",
"./public/templates/includes/loadingscreen.tmpl",
"./public/templates/includes/notifymodal.tmpl",
fmt.Sprintf("./public/templates/views/%s.tmpl", name),
}

View File

@ -5,8 +5,8 @@ import (
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/messagequeue"
"github.com/TicketsBot/GoPanel/rpc/cache"
"github.com/TicketsBot/common/permission"
"github.com/TicketsBot/database"
"github.com/go-redis/redis"
"github.com/rxdn/gdl/objects/channel"
"github.com/rxdn/gdl/objects/guild"
"github.com/rxdn/gdl/objects/member"
@ -23,8 +23,8 @@ func (ctx BotContext) Db() *database.Database {
return dbclient.Client
}
func (ctx BotContext) Redis() *redis.Client {
return messagequeue.Client.Client
func (ctx BotContext) Cache() permission.PermissionCache {
return permission.NewRedisCache(messagequeue.Client.Client)
}
func (ctx BotContext) IsBotAdmin(userId uint64) bool {

11
go.mod
View File

@ -5,9 +5,9 @@ go 1.14
require (
github.com/BurntSushi/toml v0.3.1
github.com/TicketsBot/archiverclient v0.0.0-20200704164621-09d42dd941e0
github.com/TicketsBot/common v0.0.0-20200925115036-a1bbe85f45bb
github.com/TicketsBot/database v0.0.0-20200723134637-72f4cd31eef6
github.com/TicketsBot/logarchiver v0.0.0-20200425163447-199b93429026 // indirect
github.com/TicketsBot/common v0.0.0-20201222195753-3dd751ebabf8
github.com/TicketsBot/database v0.0.0-20201224193659-c89391f44b57
github.com/TicketsBot/worker v0.0.0-20201224203453-0c8f9a415306
github.com/apex/log v1.1.2
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
@ -19,11 +19,10 @@ require (
github.com/gofrs/uuid v3.3.0+incompatible
github.com/gorilla/sessions v1.2.0 // indirect
github.com/gorilla/websocket v1.4.2
github.com/jackc/pgx/v4 v4.6.0
github.com/klauspost/compress v1.10.10 // indirect
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-20201123164345-0469e0a3cea3
github.com/rxdn/gdl v0.0.0-20201214225805-4ae598a98327
github.com/sirupsen/logrus v1.5.0
github.com/ulule/limiter/v3 v3.5.0
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a

View File

@ -5,10 +5,7 @@
<a class="nav-link" href="/manage/{{.guildId}}/settings"><i class="fas fa-cogs icon"></i>Settings</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/manage/{{.guildId}}/logs"><i class="fas fa-copy icon"></i>Logs</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/manage/{{.guildId}}/logs/modmail"><i class="fas fa-copy icon"></i>Modmail Logs</a>
<a class="nav-link" href="/manage/{{.guildId}}/logs"><i class="fas fa-copy icon"></i>Transcripts</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/manage/{{.guildId}}/blacklist"><i class="fas fa-ban icon"></i>Blacklist</a>

View File

@ -8,6 +8,8 @@
{{template "sidebar" .}}
<div class="main-panel">
{{template "loadingscreen" .}}
<script src="/assets/js/modalbackdrop.js"></script>
{{template "notifymodal" .}}
{{template "content" .}}
</div>
</div>

View File

@ -14,7 +14,7 @@
<div class="form-group">
<label>Bot Token</label>
<input name="token" type="text" class="form-control"
placeholder="9ViiGeUZlFJKIfSzodnzZT6W.bX8IAh.p9gG0tElMXg1EqwAChqaYz3swFY" id="token">
placeholder="xxxxxxxxxxxxxxxxxxxxxxxx.xxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxx" id="token">
</div>
</div>
</div>
@ -49,22 +49,19 @@
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="card-title">Modmail</h4>
<h4 class="card-title">Slash Commands</h4>
</div>
<div class="card-body" id="card">
<form onsubmit="updateForcedModmail(); return false;">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label>Server</label>
<select class="form-control" id="forced_modmail">
<option value="0">Allow user to select</option>
</select>
</div>
</div>
</div>
<form onsubmit="updatePublicKey(); return false;">
<div class="row">
<div class="col-md-9">
<div class="form-group">
<input name="public-key" type="text" class="form-control" placeholder="Public Key" id="public-key">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<button class="btn btn-primary btn-fill" type="submit">
@ -77,6 +74,17 @@
</form>
</div>
</div>
<div class="row">
<div class="col-md-12">
<button class="btn btn-primary btn-fill" style="width: 100%" onclick="createSlashCommands()">
<i class="fas fa-paper-plane"></i>
Create Slash Commands
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@ -147,7 +155,7 @@
// get bot ID
const res = await axios.get('/user/whitelabel');
if (res.status !== 200 || !res.data.success) {
showToast('Error', res.data.error);
notifyError(res.data.error);
return;
}
@ -165,26 +173,25 @@
const res = await axios.post('/user/whitelabel', data);
if (res.status !== 200 || !res.data.success) {
showToast('Error', res.data.error);
notifyError(res.data.error);
return;
}
showToast('Success', `Started tickets whitelabel on ${res.data.bot.username}#${res.data.bot.discriminator}`);
notifySuccess(`Started tickets whitelabel on ${res.data.bot.username}#${res.data.bot.discriminator}`);
}
async function updateForcedModmail() {
const select = document.getElementById('forced_modmail');
async function updatePublicKey() {
const data = {
guild: select.options[select.selectedIndex].value
public_key: document.getElementById('public-key').value,
};
const res = await axios.post('/user/whitelabel/modmail', data);
const res = await axios.post('/user/whitelabel/public-key', data);
if (res.status !== 200 || !res.data.success) {
showToast('Error', res.data.error);
notifyError(res.data.error);
return;
}
showToast('Success', 'Updated modmail settings successfully')
notifySuccess('Updated slash command settings successfully')
}
async function updateStatus() {
@ -194,18 +201,18 @@
const res = await axios.post('/user/whitelabel/status', data);
if (res.status !== 200 || !res.data.success) {
showToast('Error', res.data.error);
notifyError(res.data.error);
return;
}
showToast('Success', 'Updated status successfully')
notifySuccess('Updated status successfully')
}
async function loadStatus() {
const res = await axios.get('/user/whitelabel');
if (res.status !== 200 || !res.data.success) {
if (res.status !== 404) {
showToast('Error', res.data.error);
notifyError(res.data.error);
}
return;
}
@ -217,7 +224,7 @@
async function loadErrors() {
const res = await axios.get('/user/whitelabel/errors');
if (res.status !== 200 || !res.data.success) {
showToast('Error', res.data.error);
notifyError(res.data.error);
return;
}
@ -235,44 +242,41 @@
}
}
async function loadGuilds() {
// get selected guild
const settingsRes = await axios.get('/user/whitelabel');
if (settingsRes.status !== 200 || !settingsRes.data.success) {
showToast('Error', settingsRes.data.error);
async function loadPublicKey() {
const res = await axios.get('/user/whitelabel/public-key');
if (res.status === 404) {
return;
}
const guildId = settingsRes.data.modmail_forced_guild;
// get guild list
const guildsRes = await axios.get('/user/whitelabel/guilds');
if (guildsRes.status !== 200 || !guildsRes.data.success) {
if (guildsRes.status !== 404) {
showToast('Error', guildsRes.data.error);
}
if ((res.status !== 200 || !res.data.success)) {
notifyError(res.data.error);
return;
}
// append guilds to dropdown
const select = document.getElementById('forced_modmail');
for (let [id, name] of Object.entries(guildsRes.data.guilds)) {
const option = document.createElement('option');
option.value = id;
option.text = name;
if (id === guildId) {
option.selected = true;
const key = res.data.key;
document.getElementById('public-key').value = key;
}
select.add(option);
async function createSlashCommands() {
notify('Slash Commands', 'Creating slash commands, please note this may take up to 120 seconds to complete');
const opts = {
timeout: 120 * 1000
};
const res = await axios.post('/user/whitelabel/create-interactions', {}, opts);
if (res.status !== 200 || !res.data.success) {
notifyError(res.data.error);
return;
}
notifySuccess('Slash commands have been created. Please note, Discord may take up to an hour to show them in your client');
}
withLoadingScreen(async () => {
await loadStatus();
await loadErrors();
await loadGuilds();
await loadPublicKey();
});
</script>
{{end}}

View File

@ -5,20 +5,15 @@ import (
"github.com/TicketsBot/common/permission"
)
func GetPermissionLevel(guildId, userId uint64) permission.PermissionLevel {
func GetPermissionLevel(guildId, userId uint64) (permission.PermissionLevel, error) {
botContext, err := botcontext.ContextForGuild(guildId)
if err != nil {
return permission.Everyone
return permission.Everyone, err
}
if botContext.IsBotAdmin(userId) {
return permission.Admin
}
// get member
member, err := botContext.GetGuildMember(guildId, userId)
if err != nil {
return permission.Everyone
return permission.Everyone, err
}
return permission.GetPermissionLevel(botContext, member, guildId)