This commit is contained in:
rxdn 2023-08-21 19:27:00 +01:00
parent 263b5a3252
commit c600b91109
13 changed files with 425 additions and 100 deletions

View File

@ -1,6 +1,7 @@
package api
import (
"context"
"errors"
"github.com/TicketsBot/GoPanel/app/http/validation"
"github.com/TicketsBot/GoPanel/botcontext"
@ -8,11 +9,11 @@ import (
"github.com/TicketsBot/GoPanel/rpc"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/GoPanel/utils/types"
"github.com/TicketsBot/common/collections"
"github.com/TicketsBot/common/premium"
"github.com/TicketsBot/database"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/jackc/pgx/v4"
"github.com/rxdn/gdl/objects/guild/emoji"
"github.com/rxdn/gdl/objects/interaction/component"
"github.com/rxdn/gdl/rest/request"
@ -22,25 +23,26 @@ import (
const freePanelLimit = 3
type panelBody struct {
ChannelId uint64 `json:"channel_id,string"`
MessageId uint64 `json:"message_id,string"`
Title string `json:"title"`
Content string `json:"content"`
Colour uint32 `json:"colour"`
CategoryId uint64 `json:"category_id,string"`
Emoji types.Emoji `json:"emote"`
WelcomeMessage *types.CustomEmbed `json:"welcome_message" validate:"omitempty,dive"`
Mentions []string `json:"mentions"`
WithDefaultTeam bool `json:"default_team"`
Teams []int `json:"teams"`
ImageUrl *string `json:"image_url,omitempty"`
ThumbnailUrl *string `json:"thumbnail_url,omitempty"`
ButtonStyle component.ButtonStyle `json:"button_style,string"`
ButtonLabel string `json:"button_label"`
FormId *int `json:"form_id"`
NamingScheme *string `json:"naming_scheme"`
Disabled bool `json:"disabled"`
ExitSurveyFormId *int `json:"exit_survey_form_id"`
ChannelId uint64 `json:"channel_id,string"`
MessageId uint64 `json:"message_id,string"`
Title string `json:"title"`
Content string `json:"content"`
Colour uint32 `json:"colour"`
CategoryId uint64 `json:"category_id,string"`
Emoji types.Emoji `json:"emote"`
WelcomeMessage *types.CustomEmbed `json:"welcome_message" validate:"omitempty,dive"`
Mentions []string `json:"mentions"`
WithDefaultTeam bool `json:"default_team"`
Teams []int `json:"teams"`
ImageUrl *string `json:"image_url,omitempty"`
ThumbnailUrl *string `json:"thumbnail_url,omitempty"`
ButtonStyle component.ButtonStyle `json:"button_style,string"`
ButtonLabel string `json:"button_label"`
FormId *int `json:"form_id"`
NamingScheme *string `json:"naming_scheme"`
Disabled bool `json:"disabled"`
ExitSurveyFormId *int `json:"exit_survey_form_id"`
AccessControlList []database.PanelAccessControlRule `json:"access_control_list"`
}
func (p *panelBody) IntoPanelMessageData(customId string, isPremium bool) panelMessageData {
@ -109,6 +111,12 @@ func CreatePanel(ctx *gin.Context) {
return
}
roles, err := botContext.GetGuildRoles(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// Do custom validation
validationContext := PanelValidationContext{
Data: data,
@ -116,6 +124,7 @@ func CreatePanel(ctx *gin.Context) {
IsPremium: premiumTier > premium.None,
BotContext: botContext,
Channels: channels,
Roles: roles,
}
if err := ValidatePanelBody(validationContext); err != nil {
@ -215,30 +224,19 @@ func CreatePanel(ctx *gin.Context) {
ExitSurveyFormId: data.ExitSurveyFormId,
}
panelId, err := dbclient.Client.Panel.Create(panel)
if err != nil {
ctx.AbortWithStatusJSON(500, gin.H{
"success": false,
"error": err.Error(),
})
return
createOptions := panelCreateOptions{
TeamIds: data.Teams, // Already validated
AccessControlRules: data.AccessControlList, // Already validated
}
// insert role mention data
// string is role ID or "user" to mention the ticket opener
validRoles, err := getRoleHashSet(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
validRoles := utils.ToSet(utils.Map(roles, utils.RoleToId))
var roleMentions []uint64
for _, mention := range data.Mentions {
if mention == "user" {
if err = dbclient.Client.PanelUserMention.Set(panelId, true); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
createOptions.ShouldMentionUser = true
} else {
roleId, err := strconv.ParseUint(mention, 10, 64)
if err != nil {
@ -247,18 +245,13 @@ func CreatePanel(ctx *gin.Context) {
}
if validRoles.Contains(roleId) {
roleMentions = append(roleMentions, roleId)
createOptions.RoleMentions = append(roleMentions, roleId)
}
}
}
if err := dbclient.Client.PanelRoleMentions.Replace(panelId, roleMentions); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// Already validated, we are safe to insert
if err := dbclient.Client.PanelTeams.Replace(panelId, data.Teams); err != nil {
panelId, err := storePanel(ctx, panel, createOptions)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
@ -269,27 +262,52 @@ func CreatePanel(ctx *gin.Context) {
})
}
// DB functions
type panelCreateOptions struct {
ShouldMentionUser bool
RoleMentions []uint64
TeamIds []int
AccessControlRules []database.PanelAccessControlRule
}
func storePanel(ctx context.Context, panel database.Panel, options panelCreateOptions) (int, error) {
var panelId int
err := dbclient.Client.Panel.BeginFunc(ctx, func(tx pgx.Tx) error {
var err error
panelId, err = dbclient.Client.Panel.CreateWithTx(tx, panel)
if err != nil {
return err
}
if err := dbclient.Client.PanelUserMention.SetWithTx(tx, panelId, options.ShouldMentionUser); err != nil {
return err
}
if err := dbclient.Client.PanelRoleMentions.ReplaceWithTx(tx, panelId, options.RoleMentions); err != nil {
return err
}
// Already validated, we are safe to insert
if err := dbclient.Client.PanelTeams.ReplaceWithTx(tx, panelId, options.TeamIds); err != nil {
return err
}
if err := dbclient.Client.PanelAccessControlRules.ReplaceWithTx(ctx, tx, panelId, options.AccessControlRules); err != nil {
return err
}
return nil
})
if err != nil {
return 0, err
}
return panelId, nil
}
// Data must be validated before calling this function
func (p *panelBody) getEmoji() *emoji.Emoji {
return p.Emoji.IntoGdl()
}
func getRoleHashSet(guildId uint64) (*collections.Set[uint64], error) {
ctx, err := botcontext.ContextForGuild(guildId)
if err != nil {
return nil, err
}
roles, err := ctx.GetGuildRoles(guildId)
if err != nil {
return nil, err
}
set := collections.NewSet[uint64]()
for _, role := range roles {
set.Add(role.Id)
}
return set, nil
}

View File

@ -14,12 +14,13 @@ import (
func ListPanels(ctx *gin.Context) {
type panelResponse struct {
database.Panel
WelcomeMessage *types.CustomEmbed `json:"welcome_message"`
UseCustomEmoji bool `json:"use_custom_emoji"`
Emoji types.Emoji `json:"emote"`
Mentions []string `json:"mentions"`
Teams []int `json:"teams"`
UseServerDefaultNamingScheme bool `json:"use_server_default_naming_scheme"`
WelcomeMessage *types.CustomEmbed `json:"welcome_message"`
UseCustomEmoji bool `json:"use_custom_emoji"`
Emoji types.Emoji `json:"emote"`
Mentions []string `json:"mentions"`
Teams []int `json:"teams"`
UseServerDefaultNamingScheme bool `json:"use_server_default_naming_scheme"`
AccessControlList []database.PanelAccessControlRule `json:"access_control_list"`
}
guildId := ctx.Keys["guildid"].(uint64)
@ -30,6 +31,12 @@ func ListPanels(ctx *gin.Context) {
return
}
accessControlLists, err := dbclient.Client.PanelAccessControlRules.GetAllForGuild(ctx, guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
allFields, err := dbclient.Client.EmbedFields.GetAllFieldsForPanels(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
@ -85,6 +92,11 @@ func ListPanels(ctx *gin.Context) {
welcomeMessage = types.NewCustomEmbed(p.WelcomeMessage, fields)
}
accessControlList := accessControlLists[p.PanelId]
if accessControlList == nil {
accessControlList = make([]database.PanelAccessControlRule, 0)
}
wrapped[i] = panelResponse{
Panel: p.Panel,
WelcomeMessage: welcomeMessage,
@ -93,6 +105,7 @@ func ListPanels(ctx *gin.Context) {
Mentions: mentions,
Teams: teamIds,
UseServerDefaultNamingScheme: p.NamingScheme == nil,
AccessControlList: accessControlList,
}
return nil

View File

@ -11,6 +11,7 @@ import (
"github.com/TicketsBot/database"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/jackc/pgx/v4"
"github.com/rxdn/gdl/objects/interaction/component"
"github.com/rxdn/gdl/rest"
"github.com/rxdn/gdl/rest/request"
@ -71,6 +72,12 @@ func UpdatePanel(ctx *gin.Context) {
return
}
roles, err := botContext.GetGuildRoles(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// Do custom validation
validationContext := PanelValidationContext{
Data: data,
@ -78,6 +85,7 @@ func UpdatePanel(ctx *gin.Context) {
IsPremium: premiumTier > premium.None,
BotContext: botContext,
Channels: channels,
Roles: roles,
}
if err := ValidatePanelBody(validationContext); err != nil {
@ -213,17 +221,8 @@ func UpdatePanel(ctx *gin.Context) {
ExitSurveyFormId: data.ExitSurveyFormId,
}
if err = dbclient.Client.Panel.Update(panel); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// insert mention data
validRoles, err := getRoleHashSet(guildId)
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
validRoles := utils.ToSet(utils.Map(roles, utils.RoleToId))
// string is role ID or "user" to mention the ticket opener
var shouldMentionUser bool
@ -244,22 +243,37 @@ func UpdatePanel(ctx *gin.Context) {
}
}
if err := dbclient.Client.PanelUserMention.Set(panel.PanelId, shouldMentionUser); err != nil {
ctx.AbortWithStatusJSON(500, utils.ErrorJson(err))
return
}
err = dbclient.Client.Panel.BeginFunc(ctx, func(tx pgx.Tx) error {
if err := dbclient.Client.Panel.UpdateWithTx(tx, panel); err != nil {
return err
}
if err := dbclient.Client.PanelRoleMentions.Replace(panel.PanelId, roleMentions); err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// We are safe to insert, team IDs already validated
if err := dbclient.Client.PanelTeams.Replace(panel.PanelId, data.Teams); err != nil {
if err := dbclient.Client.PanelUserMention.SetWithTx(tx, panel.PanelId, shouldMentionUser); err != nil {
return err
}
if err := dbclient.Client.PanelRoleMentions.ReplaceWithTx(tx, panel.PanelId, roleMentions); err != nil {
return err
}
// We are safe to insert, team IDs already validated
if err := dbclient.Client.PanelTeams.ReplaceWithTx(tx, panel.PanelId, data.Teams); err != nil {
return err
}
if err := dbclient.Client.PanelAccessControlRules.ReplaceWithTx(ctx, tx, panel.PanelId, data.AccessControlList); err != nil {
return err
}
return nil
})
if err != nil {
ctx.JSON(500, utils.ErrorJson(err))
return
}
// This doesn't need to be done in a transaction
// Update multi panels
// check if this will break a multi-panel;

View File

@ -9,14 +9,24 @@ import (
"github.com/TicketsBot/GoPanel/botcontext"
dbclient "github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/database"
"github.com/rxdn/gdl/objects/channel"
"github.com/rxdn/gdl/objects/guild"
"github.com/rxdn/gdl/objects/interaction/component"
"regexp"
"strings"
"time"
)
func ApplyPanelDefaults(data *panelBody) []defaults.DefaultApplicator {
func ApplyPanelDefaults(data *panelBody) {
for _, applicator := range DefaultApplicators(data) {
if applicator.ShouldApply() {
applicator.Apply()
}
}
}
func DefaultApplicators(data *panelBody) []defaults.DefaultApplicator {
return []defaults.DefaultApplicator{
defaults.NewDefaultApplicator(defaults.EmptyStringCheck, &data.Title, "Open a ticket!"),
defaults.NewDefaultApplicator(defaults.EmptyStringCheck, &data.Content, "By clicking the button, a ticket will be opened for you."),
@ -34,6 +44,7 @@ type PanelValidationContext struct {
IsPremium bool
BotContext botcontext.BotContext
Channels []channel.Channel
Roles []guild.Role
}
func ValidatePanelBody(validationContext PanelValidationContext) error {
@ -59,6 +70,7 @@ func panelValidators() []validation.Validator[PanelValidationContext] {
validateTeams,
validateNamingScheme,
validateWelcomeMessage,
validateAccessControlList,
}
}
@ -282,3 +294,44 @@ func validateWelcomeMessage(ctx PanelValidationContext) validation.ValidationFun
return validation.NewInvalidInputError("Welcome message has no content")
}
}
func validateAccessControlList(ctx PanelValidationContext) validation.ValidationFunc {
return func() error {
acl := ctx.Data.AccessControlList
if len(acl) == 0 {
return validation.NewInvalidInputError("Access control list is empty")
}
if len(acl) > 10 {
return validation.NewInvalidInputError("Access control list cannot have more than 10 roles")
}
roles := utils.ToSet(utils.Map(ctx.Roles, utils.RoleToId))
if roles.Size() != len(ctx.Roles) {
return validation.NewInvalidInputError("Duplicate roles in access control list")
}
everyoneRoleFound := false
for _, rule := range acl {
if rule.RoleId == ctx.GuildId {
everyoneRoleFound = true
}
if rule.Action != database.AccessControlActionDeny && rule.Action != database.AccessControlActionAllow {
return validation.NewInvalidInputErrorf("Invalid access control action \"%s\"", rule.Action)
}
if !roles.Contains(rule.RoleId) {
return validation.NewInvalidInputErrorf("Invalid role %d in access control list not found in the guild", rule.RoleId)
}
}
if !everyoneRoleFound {
return validation.NewInvalidInputError("Access control list does not contain @everyone rule")
}
return nil
}
}

View File

@ -1,5 +1,7 @@
package validation
import "fmt"
type InvalidInputError struct {
Message string
}
@ -11,3 +13,7 @@ func (e *InvalidInputError) Error() string {
func NewInvalidInputError(message string) *InvalidInputError {
return &InvalidInputError{Message: message}
}
func NewInvalidInputErrorf(message string, args ...any) *InvalidInputError {
return &InvalidInputError{Message: fmt.Sprintf(message, args...)}
}

View File

@ -10,6 +10,7 @@
{loadOptions}
{placeholder}
{placeholderAlwaysShow}
{disabled}
multiple={isMulti}
--background="#2e3136"
--border="#2e3136"
@ -46,6 +47,7 @@
export let placeholderAlwaysShow = false;
export let loadOptions;
export let loadOptionsInterval;
export let disabled = false;
export let optionIdentifier;
export let nameMapper = (x) => x;

View File

@ -2,8 +2,8 @@
<label class="form-label">{label}</label>
{/if}
<WrappedSelect placeholder="Search..." optionIdentifier="id" items={roles}
bind:selectedValue={value} nameMapper={labelMapper}/>
<WrappedSelect {placeholder} optionIdentifier="id" items={roles} {disabled}
bind:selectedValue={value} nameMapper={labelMapper} on:change />
<script>
import {onMount} from 'svelte'
@ -11,8 +11,10 @@
import WrappedSelect from "../WrappedSelect.svelte";
export let label;
export let placeholder = "Search...";
export let roles = [];
export let guildId;
export let disabled = false;
export let value;

View File

@ -0,0 +1,174 @@
<div class="super">
<RoleSelect {guildId} placeholder="Add another role..."
roles={roles.filter((r) => !acl.find((s) => s.role_id === r.id))} disabled={acl.length >= maxAclSize}
on:change={(e) => addToACL(e.detail)} bind:value={roleSelectorValue}/>
<div class="container">
{#each acl as subject, i}
{@const role = roles.find(r => r.id === subject.role_id)}
<div class="subject">
<div class="inner-left">
<div class="row" style="gap: 10px">
<div class="arrow-container">
<i class="fa-solid fa-arrow-up position-arrow" class:disabled={i<=0} on:click={() => moveUp(i)}></i>
<i class="fa-solid fa-arrow-down position-arrow" class:disabled={i>=acl.length-1}
on:click={() => moveDown(i)}></i>
</div>
<span>{role.name}</span>
</div>
{#key rerender}
<Toggle on="Allow" off="Deny"
hideLabel
toggledColor="#66bb6a"
untoggledColor="#e84141"
toggled={subject.action === "allow"}
on:toggle={(e) => handleToggle(subject.role_id, e.detail)}/>
{/key}
</div>
<div class="inner-right">
{#if subject.role_id !== guildId}
<div class="delete-button">
<i class="fas fa-x" on:click={() => removeFromACL(subject)}></i>
</div>
{/if}
</div>
</div>
{/each}
</div>
</div>
<script>
import Toggle from "svelte-toggle";
import RoleSelect from "../form/RoleSelect.svelte";
export let guildId;
export let roles;
export let maxAclSize = 10;
export let acl = [
{
role_id: guildId,
action: "allow"
}
];
let rerender = 0;
let roleSelectorValue;
function handleToggle(roleId, enabled) {
const subject = acl.find(s => s.role_id === roleId);
subject.action = enabled ? "allow" : "deny";
}
function addToACL(role) {
acl = [
{
role_id: role.id,
action: "allow"
},
...acl
];
roleSelectorValue = null;
rerender++;
}
function removeFromACL(subject) {
if (subject.role_id === guildId) return;
acl = acl.filter(s => s.role_id !== subject.role_id);
rerender++;
}
function moveUp(index) {
if (index <= 0) {
return;
}
const tmp = acl[index];
acl[index] = acl[index - 1];
acl[index - 1] = tmp;
rerender++;
}
function moveDown(index) {
if (index >= acl.length - 1) {
return;
}
const tmp = acl[index];
acl[index] = acl[index + 1];
acl[index + 1] = tmp
rerender++;
}
</script>
<style>
.super {
display: flex;
flex-direction: column;
width: 100%;
gap: 10px;
}
.container {
display: flex;
flex-direction: column;
gap: 5px;
padding: 10px;
border-radius: 5px;
background-color: #121212;
}
.subject {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 5px 20px;
border-radius: 5px;
background-color: #2e3136;
}
.subject > .inner-left {
display: flex;
flex-direction: row;
flex: 1;
gap: 20%;
}
.subject > .inner-right {
display: flex;
flex-direction: row;
}
.delete-button {
cursor: pointer;
transform: scale(1.33333333333, 1);
}
.row {
display: flex;
flex-direction: row;
}
.arrow-container {
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
}
.position-arrow {
cursor: pointer;
}
.position-arrow.disabled {
cursor: unset !important;
color: #777;
}
</style>

View File

@ -154,28 +154,38 @@
</div>
</div>
</Collapsible>
<Collapsible>
<span slot="header">Access Control</span>
<div slot="content" class="col-1">
<div class="row">
<p>Control who can open tickets with from this panel. Rules are evaluated from <em>top to bottom</em>,
stopping after the first match.</p>
</div>
<div class="row">
<AccessControlList {guildId} {roles} bind:acl={data.access_control_list} />
</div>
</div>
</Collapsible>
</form>
<script>
import Input from "../form/Input.svelte";
import Textarea from "../form/Textarea.svelte";
import Colour from "../form/Colour.svelte";
import Button from "../Button.svelte";
import ChannelDropdown from "../ChannelDropdown.svelte";
import EmbedBuilder from "../EmbedBuilder.svelte";
import {createEventDispatcher, onMount} from 'svelte';
import {colourToInt, intToColour} from "../../js/util";
import CategoryDropdown from "../CategoryDropdown.svelte";
import EmojiInput from "../form/EmojiInput.svelte";
import EmojiItem from "../EmojiItem.svelte";
import Select from 'svelte-select';
import Dropdown from "../form/Dropdown.svelte";
import Toggle from "svelte-toggle";
import Checkbox from "../form/Checkbox.svelte";
import Collapsible from "../Collapsible.svelte";
import EmbedForm from "../EmbedForm.svelte";
import WrappedSelect from "../WrappedSelect.svelte";
import AccessControlList from "./AccessControlList.svelte";
export let guildId;
export let seedDefault = true;
@ -328,6 +338,12 @@
footer: {},
description: 'Thank you for contacting support.\nPlease describe your issue and wait for a response.'
},
access_control_list: [
{
role_id: guildId,
action: "allow"
}
]
};
} else {
applyOverrides();

2
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/BurntSushi/toml v1.2.1
github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc
github.com/TicketsBot/common v0.0.0-20230702161316-9b2fa80535aa
github.com/TicketsBot/database v0.0.0-20230717191327-c63f2e0e2e27
github.com/TicketsBot/database v0.0.0-20230821182620-0130c7c2c5ad
github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c
github.com/TicketsBot/worker v0.0.0-20230731124103-99c6834d9134
github.com/apex/log v1.1.2

2
go.sum
View File

@ -49,6 +49,8 @@ github.com/TicketsBot/common v0.0.0-20230702161316-9b2fa80535aa h1:6lMp2fzZvLpIq
github.com/TicketsBot/common v0.0.0-20230702161316-9b2fa80535aa/go.mod h1:zN6qXS5AYkt4JTHtq7mHT3eBHomUWZoZ29dZ/CPMjHQ=
github.com/TicketsBot/database v0.0.0-20230717191327-c63f2e0e2e27 h1:VbMTyqRPza4PRv/mRqJstLRD0tXTttPNeO2nLW/APfU=
github.com/TicketsBot/database v0.0.0-20230717191327-c63f2e0e2e27/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
github.com/TicketsBot/database v0.0.0-20230821182620-0130c7c2c5ad h1:tg/KYNLExb8MJbrxxlVgSjIzSEOibduE3ZV1S0uz+7Y=
github.com/TicketsBot/database v0.0.0-20230821182620-0130c7c2c5ad/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI=
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=

2
locale

@ -1 +1 @@
Subproject commit e21b73209a122b7feb5c8672c34b73d0a7f6ab03
Subproject commit b6540fa03b4f32ce126aa1d31830bbd4a0c2c6c7

View File

@ -1,7 +1,9 @@
package utils
import (
"github.com/TicketsBot/common/collections"
"github.com/rxdn/gdl/objects/channel/message"
"github.com/rxdn/gdl/objects/guild"
)
func Contains[T comparable](slice []T, value T) bool {
@ -21,3 +23,26 @@ func Reverse(slice []message.Message) []message.Message {
}
return slice
}
func Map[T comparable, U any](slice []T, f func(T) U) []U {
result := make([]U, len(slice))
for i, elem := range slice {
result[i] = f(elem)
}
return result
}
func ToSet[T comparable](slice []T) *collections.Set[T] {
set := collections.NewSet[T]()
for _, el := range slice {
set.Add(el)
}
return set
}
func RoleToId(role guild.Role) uint64 {
return role.Id
}