Make use of local storage
This commit is contained in:
parent
1796ec9cb5
commit
16fadb8663
@ -1,151 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||
"github.com/TicketsBot/GoPanel/rpc/cache"
|
||||
"github.com/TicketsBot/GoPanel/utils"
|
||||
"github.com/TicketsBot/common/collections"
|
||||
"github.com/TicketsBot/common/permission"
|
||||
syncutils "github.com/TicketsBot/common/utils"
|
||||
"github.com/TicketsBot/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jackc/pgtype"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type wrappedGuild struct {
|
||||
Id uint64 `json:"id,string"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
PermissionLevel permission.PermissionLevel `json:"permission_level"`
|
||||
}
|
||||
|
||||
func GetGuilds(c *gin.Context) {
|
||||
userId := c.Keys["userid"].(uint64)
|
||||
|
||||
// Get the guilds that the user is in, that the bot is also in
|
||||
userGuilds, err := getGuildIntersection(userId)
|
||||
if err != nil {
|
||||
c.JSON(500, utils.ErrorJson(err))
|
||||
return
|
||||
}
|
||||
|
||||
wg := syncutils.NewChannelWaitGroup()
|
||||
wg.Add(len(userGuilds))
|
||||
|
||||
ctx, cancel := context.WithTimeout(c, time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
group, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
var mu sync.Mutex
|
||||
guilds := make([]wrappedGuild, 0, len(userGuilds))
|
||||
for _, guild := range userGuilds {
|
||||
guild := guild
|
||||
|
||||
group.Go(func() error {
|
||||
defer wg.Done()
|
||||
|
||||
permLevel, err := utils.GetPermissionLevel(ctx, guild.GuildId, userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
guilds = append(guilds, wrappedGuild{
|
||||
Id: guild.GuildId,
|
||||
Name: guild.Name,
|
||||
Icon: guild.Icon,
|
||||
PermissionLevel: permLevel,
|
||||
})
|
||||
mu.Unlock()
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := group.Wait(); err != nil {
|
||||
c.JSON(500, utils.ErrorJson(err))
|
||||
return
|
||||
}
|
||||
|
||||
// Sort the guilds by name, but put the guilds with permission_level=0 last
|
||||
slices.SortFunc(guilds, func(a, b wrappedGuild) int {
|
||||
if a.PermissionLevel == 0 && b.PermissionLevel > 0 {
|
||||
return 1
|
||||
} else if a.PermissionLevel > 0 && b.PermissionLevel == 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
c.JSON(200, guilds)
|
||||
}
|
||||
|
||||
func getGuildIntersection(userId uint64) ([]database.UserGuild, error) {
|
||||
// Get all the guilds that the user is in
|
||||
userGuilds, err := dbclient.Client.UserGuilds.Get(context.Background(), userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
guildIds := make([]uint64, len(userGuilds))
|
||||
for i, guild := range userGuilds {
|
||||
guildIds[i] = guild.GuildId
|
||||
}
|
||||
|
||||
// Restrict the set of guilds to guilds that the bot is also in
|
||||
botGuilds, err := getExistingGuilds(guildIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
botGuildIds := collections.NewSet[uint64]()
|
||||
for _, guildId := range botGuilds {
|
||||
botGuildIds.Add(guildId)
|
||||
}
|
||||
|
||||
// Get the intersection of the two sets
|
||||
intersection := make([]database.UserGuild, 0, len(botGuilds))
|
||||
for _, guild := range userGuilds {
|
||||
if botGuildIds.Contains(guild.GuildId) {
|
||||
intersection = append(intersection, guild)
|
||||
}
|
||||
}
|
||||
|
||||
return intersection, nil
|
||||
}
|
||||
|
||||
func getExistingGuilds(userGuilds []uint64) ([]uint64, error) {
|
||||
query := `SELECT "guild_id" from guilds WHERE "guild_id" = ANY($1);`
|
||||
|
||||
userGuildsArray := &pgtype.Int8Array{}
|
||||
if err := userGuildsArray.Set(userGuilds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := cache.Instance.Query(context.Background(), query, userGuildsArray)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
var existingGuilds []uint64
|
||||
for rows.Next() {
|
||||
var guildId uint64
|
||||
if err := rows.Scan(&guildId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingGuilds = append(existingGuilds, guildId)
|
||||
}
|
||||
|
||||
return existingGuilds, nil
|
||||
}
|
@ -1,30 +1,34 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/TicketsBot/GoPanel/app/http/session"
|
||||
"github.com/TicketsBot/GoPanel/config"
|
||||
"github.com/TicketsBot/GoPanel/redis"
|
||||
wrapper "github.com/TicketsBot/GoPanel/redis"
|
||||
"github.com/TicketsBot/GoPanel/utils"
|
||||
"github.com/TicketsBot/GoPanel/utils/discord"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rxdn/gdl/rest"
|
||||
"github.com/rxdn/gdl/rest/request"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ReloadGuildsHandler(ctx *gin.Context) {
|
||||
userId := ctx.Keys["userid"].(uint64)
|
||||
func ReloadGuildsHandler(c *gin.Context) {
|
||||
userId := c.Keys["userid"].(uint64)
|
||||
|
||||
key := fmt.Sprintf("tickets:dashboard:guildreload:%d", userId)
|
||||
res, err := redis.Client.SetNX(wrapper.DefaultContext(), key, 1, time.Second*10).Result()
|
||||
if err != nil {
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !res {
|
||||
ttl, err := redis.Client.TTL(wrapper.DefaultContext(), key).Result()
|
||||
if err != nil {
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
c.JSON(500, utils.ErrorJson(err))
|
||||
return
|
||||
}
|
||||
|
||||
@ -33,19 +37,19 @@ func ReloadGuildsHandler(ctx *gin.Context) {
|
||||
ttl = 0
|
||||
}
|
||||
|
||||
ctx.JSON(429, utils.ErrorStr("You're doing this too quickly: try again in %d seconds", int(ttl.Seconds())))
|
||||
c.JSON(429, utils.ErrorStr("You're doing this too quickly: try again in %d seconds", int(ttl.Seconds())))
|
||||
return
|
||||
}
|
||||
|
||||
store, err := session.Store.Get(userId)
|
||||
if err != nil {
|
||||
if err == session.ErrNoSession {
|
||||
ctx.JSON(401, gin.H{
|
||||
c.JSON(401, gin.H{
|
||||
"success": false,
|
||||
"auth": true,
|
||||
})
|
||||
} else {
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
return
|
||||
@ -53,9 +57,9 @@ func ReloadGuildsHandler(ctx *gin.Context) {
|
||||
|
||||
// What does this do?
|
||||
if store.Expiry > time.Now().Unix() {
|
||||
res, err := discord.RefreshToken(store.RefreshToken)
|
||||
res, err := rest.RefreshToken(c, nil, config.Conf.Oauth.Id, config.Conf.Oauth.Secret, store.RefreshToken)
|
||||
if err != nil { // Tell client to re-authenticate
|
||||
ctx.JSON(200, gin.H{
|
||||
c.JSON(200, gin.H{
|
||||
"success": false,
|
||||
"reauthenticate_required": true,
|
||||
})
|
||||
@ -67,20 +71,31 @@ func ReloadGuildsHandler(ctx *gin.Context) {
|
||||
store.Expiry = time.Now().Unix() + int64(res.ExpiresIn)
|
||||
|
||||
if err := session.Store.Set(userId, store); err != nil {
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := utils.LoadGuilds(store.AccessToken, userId); err != nil {
|
||||
// TODO: Log to sentry
|
||||
guilds, err := utils.LoadGuilds(c, store.AccessToken, userId)
|
||||
if err != nil {
|
||||
var oauthError request.OAuthError
|
||||
if errors.As(err, &oauthError) {
|
||||
if oauthError.ErrorCode == "invalid_grant" {
|
||||
// Tell client to reauth, needs a 200 or client will display error
|
||||
ctx.JSON(200, gin.H{
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"reauthenticate_required": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(200, utils.SuccessResponse)
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"guilds": guilds,
|
||||
})
|
||||
}
|
||||
|
@ -2,54 +2,53 @@ package root
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/TicketsBot/GoPanel/app"
|
||||
"github.com/TicketsBot/GoPanel/app/http/session"
|
||||
"github.com/TicketsBot/GoPanel/config"
|
||||
"github.com/TicketsBot/GoPanel/utils"
|
||||
"github.com/TicketsBot/GoPanel/utils/discord"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/rxdn/gdl/rest"
|
||||
"github.com/rxdn/gdl/rest/request"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
TokenData struct {
|
||||
ClientId string `qs:"client_id"`
|
||||
ClientSecret string `qs:"client_secret"`
|
||||
GrantType string `qs:"grant_type"`
|
||||
Code string `qs:"code"`
|
||||
RedirectUri string `qs:"redirect_uri"`
|
||||
Scope string `qs:"scope"`
|
||||
}
|
||||
|
||||
TokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
)
|
||||
|
||||
func CallbackHandler(ctx *gin.Context) {
|
||||
code, ok := ctx.GetQuery("code")
|
||||
func CallbackHandler(c *gin.Context) {
|
||||
code, ok := c.GetQuery("code")
|
||||
if !ok {
|
||||
ctx.JSON(400, utils.ErrorStr("Discord provided invalid Oauth2 code"))
|
||||
c.JSON(400, utils.ErrorStr("Missing code query parameter"))
|
||||
return
|
||||
}
|
||||
|
||||
res, err := discord.AccessToken(code)
|
||||
res, err := rest.ExchangeCode(c, nil, config.Conf.Oauth.Id, config.Conf.Oauth.Secret, config.Conf.Oauth.RedirectUri, code)
|
||||
if err != nil {
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
var oauthError request.OAuthError
|
||||
if errors.As(err, &oauthError) {
|
||||
if oauthError.ErrorCode == "invalid_grant" {
|
||||
c.JSON(400, utils.ErrorStr("Invalid code: try logging in again"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, app.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
scopes := strings.Split(res.Scope, " ")
|
||||
if !utils.Contains(scopes, "identify") {
|
||||
c.JSON(400, utils.ErrorStr("Missing identify scope"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get ID + name
|
||||
currentUser, err := rest.GetCurrentUser(context.Background(), fmt.Sprintf("Bearer %s", res.AccessToken), nil)
|
||||
if err != nil {
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, app.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
@ -62,31 +61,47 @@ func CallbackHandler(ctx *gin.Context) {
|
||||
HasGuilds: false,
|
||||
}
|
||||
|
||||
if err := utils.LoadGuilds(res.AccessToken, currentUser.Id); err == nil {
|
||||
store.HasGuilds = true
|
||||
} else {
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
var guilds []utils.GuildDto
|
||||
if utils.Contains(scopes, "guilds") {
|
||||
guilds, err = utils.LoadGuilds(c, res.AccessToken, currentUser.Id)
|
||||
if err != nil {
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, app.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
store.HasGuilds = true
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"userid": strconv.FormatUint(currentUser.Id, 10),
|
||||
"timestamp": time.Now(),
|
||||
"sub": strconv.FormatUint(currentUser.Id, 10),
|
||||
"iat": time.Now().Unix(),
|
||||
})
|
||||
|
||||
str, err := token.SignedString([]byte(config.Conf.Server.Secret))
|
||||
if err != nil {
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := session.Store.Set(currentUser.Id, store); err != nil {
|
||||
ctx.JSON(500, utils.ErrorJson(err))
|
||||
_ = c.AbortWithError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(200, gin.H{
|
||||
resMap := gin.H{
|
||||
"success": true,
|
||||
"token": str,
|
||||
})
|
||||
"user_data": gin.H{
|
||||
"id": strconv.FormatUint(currentUser.Id, 10),
|
||||
"username": currentUser.Username,
|
||||
"avatar": currentUser.Avatar,
|
||||
"admin": utils.Contains(config.Conf.Admins, currentUser.Id),
|
||||
},
|
||||
}
|
||||
if len(guilds) > 0 {
|
||||
resMap["guilds"] = guilds
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resMap)
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ func ErrorHandler(c *gin.Context) {
|
||||
message = "An error occurred processing your request"
|
||||
}
|
||||
|
||||
c.Writer = cw.ResponseWriter
|
||||
c.JSON(-1, ErrorResponse{
|
||||
Error: message,
|
||||
})
|
||||
@ -44,6 +45,8 @@ func ErrorHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
if c.Writer.Status() >= 500 {
|
||||
c.Writer = cw.ResponseWriter
|
||||
|
||||
c.JSON(-1, ErrorResponse{
|
||||
Error: "An internal server error occurred",
|
||||
})
|
||||
|
@ -196,7 +196,6 @@ func StartServer(logger *zap.Logger, sm *livechat.SocketManager) {
|
||||
|
||||
userGroup := router.Group("/user", middleware.AuthenticateToken, middleware.UpdateLastSeen)
|
||||
{
|
||||
userGroup.GET("/guilds", api.GetGuilds)
|
||||
userGroup.POST("/guilds/reload", api.ReloadGuildsHandler)
|
||||
userGroup.GET("/permissionlevel", api.GetPermissionLevel)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script context="module">
|
||||
import axios from 'axios';
|
||||
import {API_URL, OAUTH} from "../js/constants";
|
||||
import {OAUTH} from "../js/constants";
|
||||
|
||||
const _tokenKey = 'token';
|
||||
|
||||
@ -31,17 +31,19 @@
|
||||
axios.defaults.headers.common['Authorization'] = getToken();
|
||||
axios.defaults.headers.common['x-tickets'] = 'true'; // arbitrary header name and value
|
||||
axios.defaults.validateStatus = (s) => true;
|
||||
|
||||
addRefreshInterceptor();
|
||||
}
|
||||
|
||||
function addRefreshInterceptor() {
|
||||
axios.interceptors.response.use(async (res) => { // we set validateStatus to false
|
||||
if (res.status === 401) {
|
||||
await _refreshToken();
|
||||
redirectLogin();
|
||||
}
|
||||
return res;
|
||||
}, async (err) => {
|
||||
if (err.response.status === 401) {
|
||||
await _refreshToken();
|
||||
redirectLogin();
|
||||
}
|
||||
return err.response;
|
||||
});
|
||||
|
@ -1,11 +1,17 @@
|
||||
<script>
|
||||
import {Navigate} from 'svelte-router-spa'
|
||||
import {getAvatarUrl, getDefaultIcon} from "../js/icons";
|
||||
|
||||
export let name;
|
||||
export let avatar;
|
||||
export let userData;
|
||||
|
||||
let hasFailed = false;
|
||||
function handleAvatarLoadError(e, userId) {
|
||||
if (!hasFailed) {
|
||||
hasFailed = true;
|
||||
e.target.src = getDefaultIcon(userId);
|
||||
}
|
||||
}
|
||||
|
||||
export let isWhitelabel = false;
|
||||
export let isAdmin = false;
|
||||
</script>
|
||||
|
||||
<div class="sidebar">
|
||||
@ -17,22 +23,13 @@
|
||||
<span class="sidebar-text">Servers</span>
|
||||
</div>
|
||||
</Navigate>
|
||||
{#if isWhitelabel}
|
||||
<Navigate to="/whitelabel" styles="sidebar-link">
|
||||
<div class="sidebar-element">
|
||||
<i class="fas fa-edit sidebar-icon"></i>
|
||||
<span class="sidebar-text">Whitelabel</span>
|
||||
</div>
|
||||
</Navigate>
|
||||
{:else}
|
||||
<a href="https://ticketsbot.net/premium" class="sidebar-link">
|
||||
<div class="sidebar-element">
|
||||
<i class="fas fa-edit sidebar-icon"></i>
|
||||
<span class="sidebar-text">Whitelabel</span>
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
{#if isAdmin}
|
||||
{#if userData.admin}
|
||||
<Navigate to="/admin/bot-staff" styles="sidebar-link">
|
||||
<div class="sidebar-element">
|
||||
<i class="fa-solid fa-user-secret sidebar-icon"></i>
|
||||
@ -51,10 +48,10 @@
|
||||
</div>
|
||||
<div class="sidebar-element user-element">
|
||||
<a class="sidebar-link">
|
||||
<i id="avatar-sidebar" style="background: url('{avatar}') center center;"></i>
|
||||
{#if name !== undefined}
|
||||
<span class="sidebar-text">{name}</span>
|
||||
{/if}
|
||||
<img class="avatar" src={getAvatarUrl(userData.id, userData.avatar)}
|
||||
on:error={(e) => handleAvatarLoadError(e, userData.id)} alt="Avatar"/>
|
||||
|
||||
<span class="sidebar-text">{userData.username}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -133,7 +130,7 @@
|
||||
margin: 0 !important
|
||||
}
|
||||
|
||||
#avatar-sidebar {
|
||||
.avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: block;
|
||||
|
@ -23,37 +23,24 @@
|
||||
import {loadingScreen} from "../js/stores"
|
||||
import {redirectLogin, setDefaultHeaders} from '../includes/Auth.svelte'
|
||||
import AdminSidebar from "../includes/AdminSidebar.svelte";
|
||||
import {onMount} from "svelte";
|
||||
|
||||
export let currentRoute;
|
||||
export let params = {};
|
||||
|
||||
setDefaultHeaders()
|
||||
setDefaultHeaders();
|
||||
|
||||
let name;
|
||||
let avatar;
|
||||
|
||||
let isWhitelabel = false;
|
||||
onMount(() => {
|
||||
let isAdmin = false;
|
||||
|
||||
async function loadData() {
|
||||
const res = await axios.get(`${API_URL}/api/session`);
|
||||
if (res.status !== 200) {
|
||||
if (res.data.auth === true) {
|
||||
redirectLogin();
|
||||
}
|
||||
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
isAdmin = res.data.admin;
|
||||
|
||||
try {
|
||||
const userData = JSON.parse(window.localStorage.getItem('user_data'));
|
||||
isAdmin = userData.admin;
|
||||
} finally {
|
||||
if (!isAdmin) {
|
||||
navigateTo(`/`);
|
||||
}
|
||||
}
|
||||
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Head/>
|
||||
|
||||
<div class="wrapper">
|
||||
<Sidebar {name} {avatar} {isWhitelabel} {isAdmin} />
|
||||
<Sidebar {userData} />
|
||||
<div class="super-container">
|
||||
<LoadingScreen/>
|
||||
<NotifyModal/>
|
||||
@ -22,36 +22,26 @@
|
||||
import {notifyError} from '../js/util'
|
||||
import {loadingScreen} from "../js/stores"
|
||||
import {redirectLogin, setDefaultHeaders} from '../includes/Auth.svelte'
|
||||
import {onMount} from "svelte";
|
||||
|
||||
export let currentRoute;
|
||||
export let params = {};
|
||||
|
||||
setDefaultHeaders()
|
||||
|
||||
let name;
|
||||
let avatar;
|
||||
let userData = {
|
||||
id: 0,
|
||||
username: 'Unknown',
|
||||
avatar: '',
|
||||
admin: false
|
||||
};
|
||||
|
||||
let isWhitelabel = false;
|
||||
let isAdmin = false;
|
||||
|
||||
async function loadData() {
|
||||
const res = await axios.get(`${API_URL}/api/session`);
|
||||
if (res.status !== 200) {
|
||||
if (res.data.auth === true) {
|
||||
redirectLogin();
|
||||
onMount(() => {
|
||||
const retrieved = window.localStorage.getItem('user_data');
|
||||
if (retrieved) {
|
||||
userData = JSON.parse(retrieved);
|
||||
}
|
||||
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
name = res.data.username;
|
||||
avatar = res.data.avatar;
|
||||
isWhitelabel = res.data.whitelabel;
|
||||
isAdmin = res.data.admin;
|
||||
}
|
||||
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -38,27 +38,7 @@
|
||||
|
||||
setDefaultHeaders();
|
||||
|
||||
export let guilds = [];
|
||||
|
||||
async function loadData() {
|
||||
const res = await axios.get(`${API_URL}/user/guilds`);
|
||||
if (res.status !== 200) {
|
||||
notifyError(res.data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
guilds = res.data;
|
||||
|
||||
permissionLevelCache.update(cache => {
|
||||
for (const guild of guilds) {
|
||||
cache[guild.id] = {
|
||||
permission_level: guild.permission_level,
|
||||
last_updated: new Date(),
|
||||
};
|
||||
}
|
||||
return cache;
|
||||
})
|
||||
}
|
||||
let guilds = window.localStorage.getItem('guilds') ? JSON.parse(window.localStorage.getItem('guilds')) : [];
|
||||
|
||||
async function refreshGuilds() {
|
||||
await withLoadingScreen(async () => {
|
||||
@ -73,13 +53,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
await loadData();
|
||||
guilds = res.data.guilds;
|
||||
window.localStorage.setItem('guilds', JSON.stringify(guilds));
|
||||
});
|
||||
}
|
||||
|
||||
withLoadingScreen(async () => {
|
||||
await loadData();
|
||||
});
|
||||
withLoadingScreen(() => {});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -23,6 +23,10 @@
|
||||
}
|
||||
|
||||
setToken(res.data.token);
|
||||
window.localStorage.setItem('user_data', JSON.stringify(res.data.user_data));
|
||||
if (res.data.guilds) {
|
||||
window.localStorage.setItem('guilds', JSON.stringify(res.data.guilds));
|
||||
}
|
||||
|
||||
let path = '/';
|
||||
|
||||
@ -33,6 +37,11 @@
|
||||
if (path === '/callback') {
|
||||
path = '/';
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(path);
|
||||
path = '/';
|
||||
} catch (e) {}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Error parsing state: ${e}`)
|
||||
|
@ -18,7 +18,7 @@
|
||||
<tbody>
|
||||
{#each staff as user}
|
||||
<tr>
|
||||
<td>{user.username}#{user.discriminator} ({user.id})</td>
|
||||
<td>{user.username} ({user.id})</td>
|
||||
<td>
|
||||
<Button type="button" danger on:click={() => removeStaff(user.id)}>
|
||||
Delete
|
||||
|
@ -141,13 +141,13 @@
|
||||
}
|
||||
|
||||
async function loadGuilds() {
|
||||
const res = await axios.get(`${API_URL}/user/guilds`)
|
||||
if (res.status !== 200) {
|
||||
notifyError(`Failed to load guilds: ${res.data.error}`)
|
||||
const fromLocalStorage = window.localStorage.getItem('guilds');
|
||||
if (!fromLocalStorage) {
|
||||
notifyError('Failed to load guilds from local storage.');
|
||||
return;
|
||||
}
|
||||
|
||||
guilds = [...guilds, ...res.data];
|
||||
guilds = [...guilds, ...JSON.parse(fromLocalStorage)];
|
||||
}
|
||||
|
||||
async function submitServers() {
|
||||
|
4
go.mod
4
go.mod
@ -8,7 +8,7 @@ require (
|
||||
github.com/BurntSushi/toml v1.2.1
|
||||
github.com/TicketsBot/archiverclient v0.0.0-20241012221057-16a920bfb454
|
||||
github.com/TicketsBot/common v0.0.0-20241104184641-e39c64bdcf3e
|
||||
github.com/TicketsBot/database v0.0.0-20241110235041-04a08e1a42a4
|
||||
github.com/TicketsBot/database v0.0.0-20241113215509-c84281e50a7e
|
||||
github.com/TicketsBot/logarchiver v0.0.0-20241012220745-5f3ba17a5138
|
||||
github.com/TicketsBot/worker v0.0.0-20241110222533-ba74e19de868
|
||||
github.com/apex/log v1.1.2
|
||||
@ -29,7 +29,7 @@ require (
|
||||
github.com/penglongli/gin-metrics v0.1.10
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.14.0
|
||||
github.com/rxdn/gdl v0.0.0-20241027214923-02dff700595b
|
||||
github.com/rxdn/gdl v0.0.0-20241113224447-d578afa35bd3
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/weppos/publicsuffix-go v0.20.0
|
||||
|
4
go.sum
4
go.sum
@ -49,6 +49,8 @@ github.com/TicketsBot/common v0.0.0-20241104184641-e39c64bdcf3e h1:cYfBjPX/FhD/M
|
||||
github.com/TicketsBot/common v0.0.0-20241104184641-e39c64bdcf3e/go.mod h1:N7zwetwx8B3RK/ZajWwMroJSyv2ZJ+bIOZWv/z8DhaM=
|
||||
github.com/TicketsBot/database v0.0.0-20241110235041-04a08e1a42a4 h1:9YMD+x2nBQN8SimrrveijYM1f7PSZ92J3h7UqgSeZfs=
|
||||
github.com/TicketsBot/database v0.0.0-20241110235041-04a08e1a42a4/go.mod h1:mpVkDO8tnnWn1pMGEphVg6YSeGIhDwLAN43lBTkpGmU=
|
||||
github.com/TicketsBot/database v0.0.0-20241113215509-c84281e50a7e h1:8nvgWvmFAJfjuAUhaGdOO9y7puPAOHRjgGWn+/DI/HA=
|
||||
github.com/TicketsBot/database v0.0.0-20241113215509-c84281e50a7e/go.mod h1:mpVkDO8tnnWn1pMGEphVg6YSeGIhDwLAN43lBTkpGmU=
|
||||
github.com/TicketsBot/logarchiver v0.0.0-20241012220745-5f3ba17a5138 h1:wsR5ESeaQKo122qsmzPcblxlJdE0GIQbp2B/7/uX+TA=
|
||||
github.com/TicketsBot/logarchiver v0.0.0-20241012220745-5f3ba17a5138/go.mod h1:4Rq0CgSCgXVW6uEyEUvWzxOmFp+L57rFfCjPDFPHFiw=
|
||||
github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM=
|
||||
@ -489,6 +491,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-20241027214923-02dff700595b h1:vSQ8iR4vrrDNchF24oxKMYbdO2D/2HKNqQkx8+v2ZMY=
|
||||
github.com/rxdn/gdl v0.0.0-20241027214923-02dff700595b/go.mod h1:hDxVWVHzvsO3Mt9d5KIjMLbm3K91Qgqw3LS0FIUxGVo=
|
||||
github.com/rxdn/gdl v0.0.0-20241113224447-d578afa35bd3 h1:ScHj6weZ5Eg4OqNH/8VT0VUxquywJ/Y8Ft/nanztbzI=
|
||||
github.com/rxdn/gdl v0.0.0-20241113224447-d578afa35bd3/go.mod h1:hDxVWVHzvsO3Mt9d5KIjMLbm3K91Qgqw3LS0FIUxGVo=
|
||||
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=
|
||||
|
@ -1,119 +0,0 @@
|
||||
package discord
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/TicketsBot/GoPanel/config"
|
||||
"github.com/pasztorpisti/qs"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
TokenData struct {
|
||||
ClientId string `qs:"client_id"`
|
||||
ClientSecret string `qs:"client_secret"`
|
||||
GrantType string `qs:"grant_type"`
|
||||
Code string `qs:"code"`
|
||||
RedirectUri string `qs:"redirect_uri"`
|
||||
Scope string `qs:"scope"`
|
||||
}
|
||||
|
||||
RefreshData struct {
|
||||
ClientId string `qs:"client_id"`
|
||||
ClientSecret string `qs:"client_secret"`
|
||||
GrantType string `qs:"grant_type"`
|
||||
RefreshToken string `qs:"refresh_token"`
|
||||
RedirectUri string `qs:"redirect_uri"`
|
||||
Scope string `qs:"scope"`
|
||||
}
|
||||
|
||||
TokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
)
|
||||
|
||||
const TokenEndpoint = "https://discord.com/api/oauth2/token"
|
||||
|
||||
func AccessToken(code string) (TokenResponse, error) {
|
||||
data := TokenData{
|
||||
ClientId: strconv.FormatUint(config.Conf.Oauth.Id, 10),
|
||||
ClientSecret: config.Conf.Oauth.Secret,
|
||||
GrantType: "authorization_code",
|
||||
Code: code,
|
||||
RedirectUri: config.Conf.Oauth.RedirectUri,
|
||||
Scope: "identify guilds",
|
||||
}
|
||||
|
||||
res, err := tokenPost(data)
|
||||
if err != nil {
|
||||
return TokenResponse{}, err
|
||||
}
|
||||
|
||||
var unmarshalled TokenResponse
|
||||
if err = json.Unmarshal(res, &unmarshalled); err != nil {
|
||||
return TokenResponse{}, err
|
||||
}
|
||||
|
||||
return unmarshalled, nil
|
||||
}
|
||||
|
||||
func RefreshToken(refreshToken string) (TokenResponse, error) {
|
||||
data := RefreshData{
|
||||
ClientId: strconv.FormatUint(config.Conf.Oauth.Id, 10),
|
||||
ClientSecret: config.Conf.Oauth.Secret,
|
||||
GrantType: "refresh_token",
|
||||
RefreshToken: refreshToken,
|
||||
RedirectUri: config.Conf.Oauth.RedirectUri,
|
||||
Scope: "identify guilds",
|
||||
}
|
||||
|
||||
res, err := tokenPost(data)
|
||||
if err != nil {
|
||||
return TokenResponse{}, err
|
||||
}
|
||||
|
||||
var unmarshalled TokenResponse
|
||||
if err = json.Unmarshal(res, &unmarshalled); err != nil {
|
||||
return TokenResponse{}, err
|
||||
}
|
||||
|
||||
return unmarshalled, nil
|
||||
}
|
||||
|
||||
func tokenPost(body ...interface{}) ([]byte, error) {
|
||||
str, err := qs.Marshal(body[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encoded := []byte(str)
|
||||
|
||||
req, err := http.NewRequest("POST", TokenEndpoint, bytes.NewBuffer([]byte(encoded)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", string(ApplicationFormUrlEncoded))
|
||||
|
||||
client := &http.Client{}
|
||||
client.Timeout = 3 * time.Second
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
content, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
package discord
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/TicketsBot/GoPanel/config"
|
||||
"github.com/gin-gonic/contrib/sessions"
|
||||
"github.com/pasztorpisti/qs"
|
||||
"github.com/pkg/errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RequestType string
|
||||
type ContentType string
|
||||
type AuthorizationType string
|
||||
|
||||
const (
|
||||
GET RequestType = "GET"
|
||||
POST RequestType = "POST"
|
||||
PATCH RequestType = "PATCH"
|
||||
|
||||
BEARER AuthorizationType = "Bearer"
|
||||
BOT AuthorizationType = "BOT"
|
||||
NONE AuthorizationType = "NONE"
|
||||
|
||||
ApplicationJson ContentType = "application/json"
|
||||
ApplicationFormUrlEncoded ContentType = "application/x-www-form-urlencoded"
|
||||
|
||||
BASE_URL = "https://discordapp.com/api/v8"
|
||||
)
|
||||
|
||||
type Endpoint struct {
|
||||
RequestType RequestType
|
||||
AuthorizationType AuthorizationType
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
func (e *Endpoint) Request(store sessions.Session, contentType *ContentType, body interface{}, response interface{}) (error, *http.Response) {
|
||||
url := BASE_URL + e.Endpoint
|
||||
// Create req
|
||||
var req *http.Request
|
||||
var err error
|
||||
if body == nil || contentType == nil {
|
||||
req, err = http.NewRequest(string(e.RequestType), url, nil)
|
||||
} else {
|
||||
// Encode body
|
||||
var encoded []byte
|
||||
if *contentType == ApplicationJson {
|
||||
raw, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
encoded = raw
|
||||
} else if *contentType == ApplicationFormUrlEncoded {
|
||||
str, err := qs.Marshal(body)
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
encoded = []byte(str)
|
||||
}
|
||||
|
||||
// Create req
|
||||
req, err = http.NewRequest(string(e.RequestType), url, bytes.NewBuffer(encoded))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
|
||||
// Set content type and user agent
|
||||
if contentType != nil {
|
||||
req.Header.Set("Content-Type", string(*contentType))
|
||||
}
|
||||
req.Header.Set("User-Agent", "DiscordBot (https://github.com/TicketsBot/GoPanel, 1.0.0)")
|
||||
|
||||
// Auth
|
||||
accessToken := store.Get("access_token").(string)
|
||||
expiry := store.Get("expiry").(int64)
|
||||
refreshToken := store.Get("refresh_token").(string)
|
||||
|
||||
// Check if needs refresh
|
||||
if (time.Now().UnixNano() / int64(time.Second)) > expiry {
|
||||
res, err := RefreshToken(refreshToken)
|
||||
if err != nil {
|
||||
store.Clear()
|
||||
_ = store.Save()
|
||||
return errors.New("Please login again!"), nil
|
||||
}
|
||||
|
||||
store.Set("access_token", res.AccessToken)
|
||||
store.Set("expiry", (time.Now().UnixNano()/int64(time.Second))+int64(res.ExpiresIn))
|
||||
store.Set("refresh_token", res.RefreshToken)
|
||||
|
||||
accessToken = res.AccessToken
|
||||
}
|
||||
|
||||
switch e.AuthorizationType {
|
||||
case BEARER:
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
case BOT:
|
||||
req.Header.Set("Authorization", "Bot "+config.Conf.Bot.Token)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
client.Timeout = 3 * time.Second
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
content, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
|
||||
return json.Unmarshal(content, response), res
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package user
|
||||
|
||||
import "github.com/TicketsBot/GoPanel/utils/discord"
|
||||
|
||||
var CurrentUser = discord.Endpoint{
|
||||
RequestType: discord.GET,
|
||||
AuthorizationType: discord.BEARER,
|
||||
Endpoint: "/users/@me",
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package user
|
||||
|
||||
import "github.com/TicketsBot/GoPanel/utils/discord"
|
||||
|
||||
var CurrentUserGuilds = discord.Endpoint{
|
||||
RequestType: discord.GET,
|
||||
AuthorizationType: discord.BEARER,
|
||||
Endpoint: "/users/@me/guilds",
|
||||
}
|
@ -1,29 +1,96 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/TicketsBot/GoPanel/app"
|
||||
dbclient "github.com/TicketsBot/GoPanel/database"
|
||||
"github.com/TicketsBot/GoPanel/rpc/cache"
|
||||
"github.com/TicketsBot/common/collections"
|
||||
"github.com/TicketsBot/common/permission"
|
||||
"github.com/TicketsBot/database"
|
||||
"github.com/jackc/pgtype"
|
||||
"github.com/rxdn/gdl/objects/guild"
|
||||
"github.com/rxdn/gdl/rest"
|
||||
errgroup "golang.org/x/sync/errgroup"
|
||||
"slices"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func LoadGuilds(accessToken string, userId uint64) error {
|
||||
type GuildDto struct {
|
||||
Id uint64 `json:"id,string"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
PermissionLevel permission.PermissionLevel `json:"permission_level"`
|
||||
}
|
||||
|
||||
func LoadGuilds(ctx context.Context, accessToken string, userId uint64) ([]GuildDto, error) {
|
||||
authHeader := fmt.Sprintf("Bearer %s", accessToken)
|
||||
|
||||
data := rest.CurrentUserGuildsData{
|
||||
Limit: 200,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), app.DefaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
guilds, err := rest.GetCurrentUserGuilds(ctx, authHeader, nil, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := storeGuildsInDb(ctx, userId, guilds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userGuilds, err := getGuildIntersection(ctx, userId, guilds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
group, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
var mu sync.Mutex
|
||||
dtos := make([]GuildDto, 0, len(userGuilds))
|
||||
for _, guild := range userGuilds {
|
||||
guild := guild
|
||||
|
||||
group.Go(func() error {
|
||||
permLevel, err := GetPermissionLevel(ctx, guild.Id, userId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
dtos = append(dtos, GuildDto{
|
||||
Id: guild.Id,
|
||||
Name: guild.Name,
|
||||
Icon: guild.Icon,
|
||||
PermissionLevel: permLevel,
|
||||
})
|
||||
mu.Unlock()
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := group.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sort the guilds by name, but put the guilds with permission_level=0 last
|
||||
slices.SortFunc(dtos, func(a, b GuildDto) int {
|
||||
if a.PermissionLevel == 0 && b.PermissionLevel > 0 {
|
||||
return 1
|
||||
} else if a.PermissionLevel > 0 && b.PermissionLevel == 0 {
|
||||
return -1
|
||||
}
|
||||
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
return dtos, nil
|
||||
}
|
||||
|
||||
// TODO: Remove this function!
|
||||
func storeGuildsInDb(ctx context.Context, userId uint64, guilds []guild.Guild) error {
|
||||
var wrappedGuilds []database.UserGuild
|
||||
|
||||
// endpoint's partial guild doesn't includes ownerid
|
||||
@ -39,5 +106,61 @@ func LoadGuilds(accessToken string, userId uint64) error {
|
||||
})
|
||||
}
|
||||
|
||||
return dbclient.Client.UserGuilds.Set(context.Background(), userId, wrappedGuilds)
|
||||
return dbclient.Client.UserGuilds.Set(ctx, userId, wrappedGuilds)
|
||||
}
|
||||
|
||||
func getGuildIntersection(ctx context.Context, userId uint64, userGuilds []guild.Guild) ([]guild.Guild, error) {
|
||||
guildIds := make([]uint64, len(userGuilds))
|
||||
for i, guild := range userGuilds {
|
||||
guildIds[i] = guild.Id
|
||||
}
|
||||
|
||||
// Restrict the set of guilds to guilds that the bot is also in
|
||||
botGuilds, err := getExistingGuilds(ctx, guildIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
botGuildIds := collections.NewSet[uint64]()
|
||||
for _, guildId := range botGuilds {
|
||||
botGuildIds.Add(guildId)
|
||||
}
|
||||
|
||||
// Get the intersection of the two sets
|
||||
intersection := make([]guild.Guild, 0, len(botGuilds))
|
||||
for _, guild := range userGuilds {
|
||||
if botGuildIds.Contains(guild.Id) {
|
||||
intersection = append(intersection, guild)
|
||||
}
|
||||
}
|
||||
|
||||
return intersection, nil
|
||||
}
|
||||
|
||||
func getExistingGuilds(ctx context.Context, userGuilds []uint64) ([]uint64, error) {
|
||||
query := `SELECT "guild_id" from guilds WHERE "guild_id" = ANY($1);`
|
||||
|
||||
userGuildsArray := &pgtype.Int8Array{}
|
||||
if err := userGuildsArray.Set(userGuilds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := cache.Instance.Query(ctx, query, userGuildsArray)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
var existingGuilds []uint64
|
||||
for rows.Next() {
|
||||
var guildId uint64
|
||||
if err := rows.Scan(&guildId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingGuilds = append(existingGuilds, guildId)
|
||||
}
|
||||
|
||||
return existingGuilds, nil
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user