Start work on panel

This commit is contained in:
Dot-Rar 2019-07-04 20:30:19 +01:00
parent 8f41184f74
commit 9b56c488ac
31 changed files with 690 additions and 49 deletions

View File

@ -56,44 +56,42 @@ func BlacklistHandler(ctx *gin.Context) {
blacklistedIds = append(blacklistedIds, user.User)
}
usernames := table.GetUsernames(blacklistedIds)
nodes := table.GetUserNodes(blacklistedIds)
var blacklisted []map[string]interface{}
for _, node := range blacklistedUsers {
for _, node := range nodes {
blacklisted = append(blacklisted, map[string]interface{}{
"userId": node.User,
"username": usernames[node.User],
"userId": node.Id,
"username": utils.Base64Decode(node.Name),
"discrim": node.Discriminator,
})
}
userNotFound := false
isStaff := false
if store.Get("csrf").(string) == ctx.Query("csrf") { // CSRF is correct *and* set
targetIdStr := ctx.Query("userid")
targetId, err := strconv.ParseInt(targetIdStr, 10, 64)
username := ctx.Query("username")
discrim := ctx.Query("discrim")
if err != nil {
userNotFound = true
} else {
// Verify that the user ID is real and in a shared guild
username := table.GetUsername(targetId)
exists := username != ""
// Verify that the user ID is real and in a shared guild
targetId := table.GetUserId(username, discrim)
exists := targetId != 0
if exists {
if guild.OwnerId == targetIdStr || table.IsSupport(guildId, targetId) { // Prevent users from blacklisting staff
isStaff = true
} else {
if !utils.Contains(blacklistedIds, targetId) { // Prevent duplicates
table.AddBlacklist(guildId, targetId)
blacklisted = append(blacklisted, map[string]interface{}{
"userId": targetIdStr,
"username": username,
})
}
}
if exists {
if guild.OwnerId == strconv.Itoa(int(targetId)) || table.IsStaff(guildId, targetId) { // Prevent users from blacklisting staff
isStaff = true
} else {
userNotFound = true
if !utils.Contains(blacklistedIds, targetId) { // Prevent duplicates
table.AddBlacklist(guildId, targetId)
blacklisted = append(blacklisted, map[string]interface{}{
"userId": targetId,
"username": username,
"discrim": discrim,
})
}
}
} else {
userNotFound = true
}
}

View File

@ -210,6 +210,39 @@ func SettingsHandler(ctx *gin.Context) {
}
}
panelSettings := table.GetPanelSettings(guildId)
panelUpdated := false
// Get panel title
panelTitle := ctx.Query("paneltitle")
if panelTitle == "" || len(panelTitle) > 255 || !csrfCorrect {
panelTitle = panelSettings.Title
} else {
panelUpdated = true
}
// Get panel content
panelContent := ctx.Query("panelcontent")
if panelContent == "" || len(panelContent) > 255 || !csrfCorrect {
panelContent = panelSettings.Content
} else {
panelUpdated = true
}
// Get panel colour
var panelColour uint64
panelColourHex := ctx.Query("panelcolour")
if panelColourHex == "" || len(panelColourHex) > 255 || !csrfCorrect {
panelColour = uint64(panelSettings.Colour)
} else {
panelUpdated = true
panelColour, err = strconv.ParseUint(panelColourHex, 16, 32)
}
if panelUpdated {
go table.UpdatePanelSettings(guildId, panelTitle, panelContent, int(panelColour))
}
utils.Respond(ctx, template.TemplateSettings.Render(map[string]interface{}{
"name": store.Get("name").(string),
"guildId": guildIdStr,
@ -224,6 +257,9 @@ func SettingsHandler(ctx *gin.Context) {
"invalidTicketLimit": invalidTicketLimit,
"csrf": store.Get("csrf").(string),
"pingEveryone": pingEveryone,
"paneltitle": panelTitle,
"panelcontent": panelContent,
"panelcolour": strconv.FormatInt(int64(panelColour), 16),
}))
} else {
ctx.Redirect(302, "/login")

View File

@ -0,0 +1,110 @@
package manage
import (
"github.com/TicketsBot/GoPanel/app/http/template"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/GoPanel/utils/discord/objects"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
"strconv"
"strings"
)
func TicketListHandler(ctx *gin.Context) {
store := sessions.Default(ctx)
if store == nil {
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
userIdStr := store.Get("userid").(string)
userId, err := utils.GetUserId(store)
if err != nil {
ctx.String(500, err.Error())
return
}
// Verify the guild exists
guildIdStr := ctx.Param("id")
guildId, err := strconv.ParseInt(guildIdStr, 10, 64)
if err != nil {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
return
}
// Get object for selected guild
var guild objects.Guild
for _, g := range table.GetGuilds(userIdStr) {
if g.Id == guildIdStr {
guild = g
break
}
}
// Verify the user has permissions to be here
if !guild.Owner && !table.IsAdmin(guildId, userId) {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
return
}
tickets := table.GetOpenTickets(guildId)
var toFetch []int64
for _, ticket := range tickets {
toFetch = append(toFetch, ticket.Owner)
for _, idStr := range strings.Split(ticket.Members, ",") {
if memberId, err := strconv.ParseInt(idStr, 10, 64); err == nil {
toFetch = append(toFetch, memberId)
}
}
}
nodes := make(map[int64]table.UsernameNode)
for _, node := range table.GetUserNodes(toFetch) {
nodes[node.Id] = node
}
var ticketsFormatted []map[string]interface{}
for _, ticket := range tickets {
var membersFormatted []map[string]interface{}
for index, memberIdStr := range strings.Split(ticket.Members, ",") {
if memberId, err := strconv.ParseInt(memberIdStr, 10, 64); err == nil {
if memberId != 0 {
var separator string
if index != len(strings.Split(ticket.Members, ",")) - 1 {
separator = ", "
}
membersFormatted = append(membersFormatted, map[string]interface{}{
"username": utils.Base64Decode(nodes[memberId].Name),
"discrim": nodes[memberId].Discriminator,
"sep": separator,
})
}
}
}
ticketsFormatted = append(ticketsFormatted, map[string]interface{}{
"uuid": ticket.Uuid,
"ticketId": ticket.TicketId,
"username": utils.Base64Decode(nodes[ticket.Owner].Name),
"discrim": nodes[ticket.Owner].Discriminator,
"members": membersFormatted,
})
}
utils.Respond(ctx, template.TemplateTicketList.Render(map[string]interface{}{
"name": store.Get("name").(string),
"guildId": guildIdStr,
"csrf": store.Get("csrf").(string),
"avatar": store.Get("avatar").(string),
"baseUrl": config.Conf.Server.BaseUrl,
"tickets": ticketsFormatted,
}))
}
}

View File

@ -0,0 +1,98 @@
package manage
import (
"fmt"
"github.com/TicketsBot/GoPanel/app/http/template"
"github.com/TicketsBot/GoPanel/config"
"github.com/TicketsBot/GoPanel/database/table"
"github.com/TicketsBot/GoPanel/utils"
"github.com/TicketsBot/GoPanel/utils/discord/endpoints/channel"
"github.com/TicketsBot/GoPanel/utils/discord/objects"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
"strconv"
)
func TicketViewHandler(ctx *gin.Context) {
store := sessions.Default(ctx)
if store == nil {
return
}
defer store.Save()
if utils.IsLoggedIn(store) {
userIdStr := store.Get("userid").(string)
userId, err := utils.GetUserId(store)
if err != nil {
ctx.String(500, err.Error())
return
}
// Verify the guild exists
guildIdStr := ctx.Param("id")
guildId, err := strconv.ParseInt(guildIdStr, 10, 64)
if err != nil {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 404 Page
return
}
// Get object for selected guild
var guild objects.Guild
for _, g := range table.GetGuilds(userIdStr) {
if g.Id == guildIdStr {
guild = g
break
}
}
// Verify the user has permissions to be here
if !guild.Owner && !table.IsAdmin(guildId, userId) {
ctx.Redirect(302, config.Conf.Server.BaseUrl) // TODO: 403 Page
return
}
// Get ticket UUID from URL and verify it exists
uuid := ctx.Param("uuid")
ticket := table.GetTicket(uuid)
exists := ticket != table.Ticket{}
// If invalid ticket UUID, take user to ticket list
if !exists {
ctx.Redirect(302, fmt.Sprintf("/manage/%s/tickets", guildIdStr))
return
}
// Get messages
var messages []objects.Message
// We want to show users error messages so they can report them
isError := false
var errorMessage string
endpoint := channel.GetChannelMessages(int(ticket.Channel))
if err = endpoint.Request(store, nil, nil, &messages); err != nil {
isError = true
errorMessage = err.Error()
}
// Format messages, exclude unneeded data
var messagesFormatted []map[string]interface{}
for _, message := range utils.Reverse(messages) {
messagesFormatted = append(messagesFormatted, map[string]interface{}{
"username": message.Author.Username,
"content": message.Content,
})
}
utils.Respond(ctx, template.TemplateTicketView.Render(map[string]interface{}{
"name": store.Get("name").(string),
"guildId": guildIdStr,
"csrf": store.Get("csrf").(string),
"avatar": store.Get("avatar").(string),
"baseUrl": config.Conf.Server.BaseUrl,
"isError": isError,
"error": errorMessage,
"messages": messagesFormatted,
"ticketId": ticket.TicketId,
}))
}
}

View File

@ -62,6 +62,12 @@ func StartServer() {
// /manage/:id/blacklist/remove/:user
router.GET("/manage/:id/blacklist/remove/:user", manage.BlacklistRemoveHandler)
// /manage/:id/tickets
router.GET("/manage/:id/tickets", manage.TicketListHandler)
// /manage/:id/tickets/view/:uuid
//router.GET("/manage/:id/tickets/view/:uuid", manage.TicketViewHandler)
if err := router.Run(config.Conf.Server.Host); err != nil {
panic(err)
}

View File

@ -22,6 +22,8 @@ var (
TemplateLogs Template
TemplateSettings Template
TemplateBlacklist Template
TemplateTicketList Template
TemplateTicketView Template
)
func (t *Template) Render(context ...interface{}) string {
@ -54,6 +56,14 @@ func LoadTemplates() {
compiled: loadTemplate("blacklist"),
Layout: LayoutManage,
}
TemplateTicketList = Template{
compiled: loadTemplate("ticketlist"),
Layout: LayoutManage,
}
TemplateTicketView = Template{
compiled: loadTemplate("ticketview"),
Layout: LayoutManage,
}
}
func loadLayout(name string) *mustache.Template {

View File

@ -0,0 +1,37 @@
package table
import (
"github.com/TicketsBot/GoPanel/database"
)
type PanelSettings struct {
GuildId int64 `gorm:"column:GUILDID"`
Title string `gorm:"column:TITLE;type:VARCHAR(255)"`
Content string `gorm:"column:CONTENT;type:TEXT"`
Colour int `gorm:"column:COLOUR`
}
func (PanelSettings) TableName() string {
return "panelsettings"
}
func UpdatePanelSettings(guildId int64, title string, content string, colour int) {
settings := PanelSettings{
Title: title,
Content: content,
Colour: colour,
}
database.Database.Where(&PanelSettings{GuildId: guildId}).Assign(&settings).FirstOrCreate(&PanelSettings{})
}
func GetPanelSettings(guildId int64) PanelSettings {
settings := PanelSettings{
Title: "Open A Ticket",
Content: "React with :envelope_with_arrow: to open a ticket",
Colour: 2335514,
}
database.Database.Where(PanelSettings{GuildId: guildId}).First(&settings)
return settings
}

36
database/table/tickets.go Normal file
View File

@ -0,0 +1,36 @@
package table
import "github.com/TicketsBot/GoPanel/database"
type Ticket struct {
Uuid string `gorm:"column:UUID;type:varchar(36);primary_key"`
TicketId int `gorm:"column:ID"`
Guild int64 `gorm:"column:GUILDID"`
Channel int64 `gorm:"column:CHANNELID"`
Owner int64 `gorm:"column:OWNERID"`
Members string `gorm:"column:MEMBERS;type:text"`
IsOpen bool `gorm:"column:OPEN"`
OpenTime int64 `gorm:"column:OPENTIME"`
}
func (Ticket) TableName() string {
return "tickets"
}
func GetTickets(guild int64) []Ticket {
var tickets []Ticket
database.Database.Where(&Ticket{Guild: guild}).Order("ID asc").Find(&tickets)
return tickets
}
func GetOpenTickets(guild int64) []Ticket {
var tickets []Ticket
database.Database.Where(&Ticket{Guild: guild, IsOpen: true}).Order("ID asc").Find(&tickets)
return tickets
}
func GetTicket(uuid string) Ticket {
var ticket Ticket
database.Database.Where(&Ticket{Uuid: uuid}).First(&ticket)
return ticket
}

View File

@ -1,13 +1,15 @@
package table
import (
"encoding/base64"
"github.com/TicketsBot/GoPanel/database"
"github.com/TicketsBot/GoPanel/utils"
)
type UsernameNode struct {
Id int64 `gorm:"column:USERID;primary_key"`
Name string `gorm:"column:USERNAME;type:text"` // Base 64 encoded
Discriminator string `gorm:"column:DISCRIM;type:varchar(4)"`
Avatar string `gorm:"column:AVATARHASH;type:varchar(100)"`
}
func (UsernameNode) TableName() string {
@ -17,24 +19,17 @@ func (UsernameNode) TableName() string {
func GetUsername(id int64) string {
node := UsernameNode{Name: "Unknown"}
database.Database.Where(&UsernameNode{Id: id}).First(&node)
return base64Decode(node.Name)
return utils.Base64Decode(node.Name)
}
func GetUsernames(ids []int64) map[int64]string {
func GetUserNodes(ids []int64) []UsernameNode {
var nodes []UsernameNode
database.Database.Where(ids).Find(&nodes)
m := make(map[int64]string)
for _, node := range nodes {
m[node.Id] = base64Decode(node.Name)
}
return m
return nodes
}
func base64Decode(s string) string {
b, err := base64.StdEncoding.DecodeString(s); if err != nil {
return ""
}
return string(b)
func GetUserId(name, discrim string) int64 {
var node UsernameNode
database.Database.Where(&UsernameNode{Name: utils.Base64Encode(name), Discriminator: discrim}).First(&node)
return node.Id
}

View File

@ -0,0 +1,31 @@
@font-face{font-family:Whitney;font-style:light;font-weight:300;src:url('https://discordapp.com/assets/6c6374bad0b0b6d204d8d6dc4a18d820.woff') format('woff')}
@font-face{font-family:Whitney;font-style:normal;font-weight:500;src:url('https://discordapp.com/assets/e8acd7d9bf6207f99350ca9f9e23b168.woff') format('woff')}
@font-face{font-family:Whitney;font-style:medium;font-weight:600;src:url('https://discordapp.com/assets/3bdef1251a424500c1b3a78dea9b7e57.woff') format('woff')}
@font-face{font-family:WhitneyMedium;font-style:medium;font-weight:600;src:url('https://discordapp.com/assets/be0060dafb7a0e31d2a1ca17c0708636.woff') format('woff')}
@font-face{font-family:Whitney;font-style:bold;font-weight:700;src:url('https://discordapp.com/assets/8e12fb4f14d9c4592eb8ec9f22337b04.woff') format('woff')}
.discord-container {
background-color: #2e3136;
border-radius: 25px;
height: 100vh;
margin: 0;
padding: 0;
font-family: 'Whitney', sans-serif !important;
}
.channel-header {
background-color: #1e2124;
height: 5vh;
width: 100%;
border-radius: 25px 25px 0 0;
position: relative;
text-align: center;
display: flex;
align-items: center;
}
.channel-name {
color: white;
padding-left: 20px;
}

View File

@ -62,6 +62,10 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js" integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous"></script>
<!-- Discord theme -->
<link href="/assets/css/discordmock.css" rel="stylesheet"/>
<!-- Icons -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.2/css/all.css" integrity="sha384-oS3vJWv+0UjzBfQzYUhtDYW+Pj2yciDJxpsK1OYPAYjqT085Qq/1cq5FLXAZQ7Ay" crossorigin="anonymous">
</head>
@ -112,6 +116,9 @@
<li class="nav-item">
<a class="nav-link" href="/manage/{{guildId}}/blacklist">Blacklist</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/manage/{{guildId}}/tickets">Ticket List</a>
</li>
</ul>
</div>
</nav>

View File

@ -19,16 +19,19 @@
<div class="card-body">
<form>
<div class="row">
<div class="col-md-8 pr-1">
<div class="col-md-3 pr-1">
<div class="form-group">
<label>User ID</label>
<input name="userid" type="text" class="form-control" placeholder="User ID">
<label>Username</label>
<input name="username" type="text" class="form-control" placeholder="Username">
</div>
</div>
<div class="col-md-4 px-1">
<label></label>
<div class="alert alert-success" role="alert">
Click <a href="https://support.discordapp.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-">here</a> for a guide on how to retreive user IDs.
<div class="col-md-1 px-1">
<label>Discriminator</label>
<div class="input-group mb-3">
<div class="input-group-prepend">
<div class="input-group-text">#</div>
</div>
<input name="discrim" type="text" class="form-control" placeholder="0000">
</div>
</div>
</div>
@ -51,7 +54,7 @@
<thead>
<tr>
<th>User ID</th>
<th>Username</th>
<th>Username#Discrim</th>
<th>Remove</th>
</tr>
</thead>
@ -59,7 +62,7 @@
{{#blacklisted}}
<tr>
<td>{{userId}}</td>
<td>{{username}}</td>
<td>{{username}}#{{discrim}}</td>
<td><a href="{{baseUrl}}/manage/{{guildId}}/blacklist/remove/{{userId}}?c={{csrf}}">Remove</a></td>
</tr>
{{/blacklisted}}

View File

@ -68,6 +68,28 @@
</div>
</div>
<div class="row">
<div class="col-md-3 pr-1">
<label>Panel Title</label>
<input name="paneltitle" type="text" class="form-control" placeholder="Open A Ticket" value="{{paneltitle}}">
</div>
<div class="col-md-6 pr-1">
<label>Panel Content</label>
<input name="panelcontent" type="text" class="form-control" placeholder="React with :envelope_with_arrow: to open a ticket" value="{{panelcontent}}">
</div>
<div class="col-md-3 pr-1">
<label>Panel Colour (Hex)</label>
<div class="input-group mb-3">
<div class="input-group-prepend">
<div class="input-group-text">#</div>
</div>
<input name="panelcolour" type="text" class="form-control" placeholder="23A31A" value="{{panelcolour}}">
</div>
</div>
</div>
<input name="csrf" type="hidden" value="{{csrf}}">
<div class="row">
<div class="col-md-1 pr-1">

View File

@ -0,0 +1,42 @@
<div class="content">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">Ticket List</h4>
</div>
<div class="card-body">
<div class="card-body table-responsive">
<table class="table table-hover table-striped">
<thead>
<tr>
<th>Ticket ID</th>
<th>User</th>
<th>Additional Members</th>
<th>View</th>
</tr>
</thead>
<tbody>
{{#tickets}}
<tr>
<td>{{ticketId}}</td>
<td>{{username}}#{{discrim}}</td>
<td>{{#members}}{{username}}#{{discrim}}{{sep}}{{/members}}</td>
<!--<td><a class="btn btn-primary btn-sm" role="button" href="/manage/{{guildId}}/tickets/view/{{uuid}}">View</a></td>-->
<td>Coming soon</td>
</tr>
{{/tickets}}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
$('.toast').toast('show');
</script>
</div>

View File

@ -0,0 +1,50 @@
<div class="content">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h4 class="card-title">Ticket List</h4>
</div>
<div class="card-body">
<div class="discord-container">
<div class="channel-header">
<span class="channel-name">#ticket-{{ticketId}}</span>
</div>
<div class="message-container">
<div class="message">
</div>
</div>
<div class="input-container">
</div>
</div>
</div>
</div>
</div>
</div>
<div aria-live="polite" aria-atomic="true" style="position: relative; min-height: 200px;">
<div style="position: absolute; right: 10px; min-width: 300px">
{{#isError}}
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" data-autohide="false">
<div class="toast-header">
<strong class="mr-auto">Error</strong>
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
{{error}}
</div>
</div>
{{/isError}}
</div>
</div>
<script>
$('.toast').toast('show');
</script>
</div>
</div>

View File

@ -0,0 +1,14 @@
package channel
import (
"fmt"
"github.com/TicketsBot/GoPanel/utils/discord"
)
func GetChannelMessages(id int) discord.Endpoint {
return discord.Endpoint{
RequestType: discord.GET,
AuthorizationType: discord.BOT,
Endpoint: fmt.Sprintf("/channels/%d/messages", id),
}
}

View File

@ -0,0 +1,11 @@
package objects
type Attachment struct {
Id string
Filename string
Size int
url string
ProxyUrl string
height int
Width int
}

View File

@ -0,0 +1,17 @@
package objects
type Embed struct {
Title string
Type string
Description string
Url string
Timestamp string
Color int
Footer EmbedField
Image EmbedImage
Thumbnail EmbedThumbnail
Video EmbedVideo
Provider EmbedProvider
Author EmbedAuthor
Fields []EmbedField
}

View File

@ -0,0 +1,8 @@
package objects
type EmbedAuthor struct {
Name string
Url string
IconUrl string
ProxyIconUrl string
}

View File

@ -0,0 +1,7 @@
package objects
type EmbedField struct {
Name string
Value string
Inline bool
}

View File

@ -0,0 +1,7 @@
package objects
type EmbedFooter struct {
Text string
IconUrl string
ProxyIconUrl string
}

View File

@ -0,0 +1,8 @@
package objects
type EmbedImage struct {
Url string
ProxyUrl string
Height int
Width int
}

View File

@ -0,0 +1,6 @@
package objects
type EmbedProvider struct {
Name string
Url string
}

View File

@ -0,0 +1,8 @@
package objects
type EmbedThumbnail struct {
Url string
ProxyUrl string
Height int
Width int
}

View File

@ -0,0 +1,7 @@
package objects
type EmbedVideo struct {
Url string
Height int
Width int
}

View File

@ -0,0 +1,25 @@
package objects
type Message struct {
Id string
ChannelId string
GuildId string
Author User
Member Member
Content string
Timestamp string
EditedTimestamp string
Tts bool
MentionEveryone bool
Mentions []interface{}
MentionsRoles []int64
Attachments []Attachment
Embeds []Embed
Reactions []Reaction
Nonce string
Pinned bool
WebhookId string
Type int
Activity MessageActivity
Application MessageApplication
}

View File

@ -0,0 +1,6 @@
package objects
type MessageActivity struct {
Type int
PartyId string
}

View File

@ -0,0 +1,9 @@
package objects
type MessageApplication struct {
Id string
CoverImage string
Description string
Icon string
Name string
}

View File

@ -0,0 +1,7 @@
package objects
type Reaction struct {
Count int
Me bool
Emoji Emoji
}

View File

@ -32,3 +32,11 @@ func Insert(slice []objects.Guild, index int, value objects.Guild) []objects.Gui
// Return the result.
return slice
}
func Reverse(slice []objects.Message) []objects.Message {
for i := len(slice)/2-1; i >= 0; i-- {
opp := len(slice)-1-i
slice[i], slice[opp] = slice[opp], slice[i]
}
return slice
}

View File

@ -1,6 +1,7 @@
package utils
import (
"encoding/base64"
"math/rand"
"strconv"
)
@ -19,3 +20,14 @@ func IsInt(str string) bool {
_, err := strconv.ParseInt(str, 10, 64)
return err == nil
}
func Base64Decode(s string) string {
b, err := base64.StdEncoding.DecodeString(s); if err != nil {
return ""
}
return string(b)
}
func Base64Encode(s string) string {
return base64.StdEncoding.EncodeToString([]byte(s))
}