1
0
mirror of https://github.com/mgerb/go-discord-bot synced 2026-01-11 09:32:50 +00:00

expanded permissions functionality - added packr for bundling static assets

This commit is contained in:
2018-04-15 15:28:12 -05:00
parent e36b168a23
commit 89b6d89890
9 changed files with 196 additions and 122 deletions

View File

@@ -4,15 +4,9 @@ import jwt_decode from 'jwt-decode';
import { StorageService } from '../../services'; import { StorageService } from '../../services';
import './Navbar.scss'; import './Navbar.scss';
let oauthUrl: string; const baseUrl = window.location.origin + '/oauth';
if (!process.env.NODE_ENV) { const oauthUrl = `https://discordapp.com/api/oauth2/authorize?client_id=410818759746650140&redirect_uri=${baseUrl}&response_type=code&scope=identify%20guilds`;
// dev
oauthUrl = `https://discordapp.com/api/oauth2/authorize?client_id=410818759746650140&redirect_uri=https%3A%2F%2Flocalhost%2Foauth&response_type=code&scope=identify%20guilds`;
} else {
// prod
oauthUrl = `https://discordapp.com/api/oauth2/authorize?client_id=271998875802402816&redirect_uri=https%3A%2F%2Fcashdiscord.com%2Foauth&response_type=code&scope=identify%20guilds%20email`;
}
interface Props {} interface Props {}
@@ -47,7 +41,7 @@ export class Navbar extends React.Component<Props, State> {
render() { render() {
return ( return (
<div className="Navbar"> <div className="Navbar">
<div className="Navbar__header">Cash</div> <div className="Navbar__header">Sound Bot</div>
<NavLink exact to="/" className="Navbar__item" activeClassName="Navbar__item--active"> <NavLink exact to="/" className="Navbar__item" activeClassName="Navbar__item--active">
Home Home
</NavLink> </NavLink>

View File

@@ -34,6 +34,7 @@ export class Stats extends Component<any, IState> {
return { username: k, count: v }; return { username: k, count: v };
}) })
.orderBy(v => v.count, 'desc') .orderBy(v => v.count, 'desc')
.slice(0, 10)
.value(); .value();
this.setState({ data }); this.setState({ data });
@@ -61,8 +62,8 @@ export class Stats extends Component<any, IState> {
return ( return (
<div className="content"> <div className="content">
<div className="card" style={{ maxWidth: '1000px' }}> <div className="card" style={{ maxWidth: '1000px' }}>
<div className="card__header">Shitposts</div> <div className="card__header">Posts containing links</div>
<HorizontalBar data={data} height={500} /> <HorizontalBar data={data} />
</div> </div>
</div> </div>
); );

View File

@@ -7,6 +7,7 @@
"bot_prefix": "#", "bot_prefix": "#",
"admin_emails": ["mail@example.com"], "admin_emails": ["mail@example.com"],
"mod_emails": ["mail@example.com"],
"jwt_key": "", "jwt_key": "",
"server_addr": "0.0.0.0:80", "server_addr": "0.0.0.0:80",

View File

@@ -5,7 +5,7 @@ install:
go get && cd client && npm install go get && cd client && npm install
build-server: build-server:
go build -o bot ./main.go packr build -o bot ./main.go && packr install
build-client: build-client:
cd client && npm run build cd client && npm run build
@@ -13,4 +13,4 @@ build-client:
clean: clean:
rm -rf bot ./dist rm -rf bot ./dist
all: install build-server build-client all: install build-client build-server

View File

@@ -1,51 +1,88 @@
# Discord Sound Bot # Discord Sound Bot
This is a soundboard bot for discord. The back end is in GoLang and the front end uses React. A soundboard bot for discord with a Go back end and React front end.
<img src="http://i.imgur.com/jtAyJZ1.png"/> ![Image](https://i.imgur.com/BCoLAuK.png)
## How to use ## How to use
NOTE: Currently the binaries in the release package only run on linux. Check them out [here](https://github.com/mgerb/go-discord-bot/releases) * [Download latest release here](https://github.com/mgerb/go-discord-bot/releases)
* Install [youtube-dl](https://github.com/rg3/youtube-dl/blob/master/README.md#installation)
* Install [ffmpeg](https://www.ffmpeg.org/download.html)
* edit your config.json file
* run the executable
- download bot.zip and extract everything ### Commands
- rename config.template.json to config.json
- add your bot token and preferred upload password (leave as is for no password)
- run the bot with `./bot` (you may need to use sudo if you leave it on port 80)
## Flags * `clip` - clips the past minute of audio
* `summon` - summons the bot to your current channel
* `dismiss` - dismisses the bot from the server
* `<audio clip>` - play a named audio clip
> -p, run in production mode ### Uploading files
> -tls, run with auto tls Discord oauth is used to authenticate users in order to upload files.
To get oauth working you must set up your bot client secret/id in the config.
You must also set up the redirect URI. This is needed so discord can redirect
back to your site after authentication. Discord doesn't like insecure redirects
so you will have to use a proxy for this. I prefer using [caddy](https://github.com/mholt/caddy)
with the following config.
## Setting up Youtube downloader ```
https://localhost {
tls self_signed
proxy / http://localhost:8080 {
transparent
}
}
```
- Install [youtube-dl](https://github.com/rg3/youtube-dl/blob/master/README.md#installation) For public hosting you will want to use something like this.
### NOTE ```
https://<your domain name> {
tls <your email>
proxy / http://localhost:8080 {
transparent
}
}
```
If you get a permissions error with ffmpeg on mac or linux: ### Clipping audio
`sudo chmod +x dist/ffmpeg_linux`
Sounds are stored in the `sounds` directory. You may copy files directly to this folder rather than uploading through the site. If the bot is in a channel it listens to all audio. Use the `clip` command
to record the past minute of conversation. Access all clips in the "Clips"
section of the site.
### Stats
If logging is enabled the bot will log all messages and store in a database file. Currently the bot keeps track of
all messages that contain links in them. I added this because it's something we use in my discord.
Check it out in the "Stats" page on the site.
## Building from Source ## Building from Source
### Dependencies ### Dependencies
- Go
- node/npm * Go
- make * node/npm
* make
### Compiling ### Compiling
- Make sure dependencies are installed
- `make all` * Make sure dependencies are installed
- Rename the `config.template.json` to `config.json` * Rename the `config.template.json` to `config.json`
- add configurations to `config.json` * add configurations to `config.json`
- run the executable * `cd client && npm run dev`
- open a browser `localhost:<port>` * `go run main.go`
- upload files * open a browser `localhost:<config_port>`
- success!
[Packr](https://github.com/gobuffalo/packr) is used to bundle the static web assets into the binary.
Use these commands to compile the project. The client must be built first.
* `packr build`
* `packr install`
### Windows ### Windows
I've only compiled and run this on linux so far, but I've recently added cross platform support.
I only run this on linux. I'm not sure if it will work on windows, but it should without too much work.

View File

@@ -33,22 +33,23 @@ const (
) )
// store our connection objects in a map tied to a guild id // store our connection objects in a map tied to a guild id
var activeConnections = make(map[string]*audioConnection) var activeConnections = make(map[string]*AudioConnection)
type audioConnection struct { // AudioConnection -
guild *discordgo.Guild type AudioConnection struct {
session *discordgo.Session Guild *discordgo.Guild `json:"guild"`
voiceConnection *discordgo.VoiceConnection Session *discordgo.Session `json:"-"`
currentChannel *discordgo.Channel VoiceConnection *discordgo.VoiceConnection `json:"-"`
sounds map[string]*audioClip CurrentChannel *discordgo.Channel `json:"current_channel"`
soundQueue chan string Sounds map[string]*AudioClip `json:"-"`
voiceClipQueue chan *discordgo.Packet SoundQueue chan string `json:"-"`
soundPlayingLock bool VoiceClipQueue chan *discordgo.Packet `json:"-"`
audioListenerLock bool SoundPlayingLock bool `json:"-"`
mutex *sync.Mutex // mutex for single audio connection AudioListenerLock bool `json:"-"`
Mutex *sync.Mutex `json:"-"` // mutex for single audio connection
} }
type audioClip struct { type AudioClip struct {
Name string Name string
Extension string Extension string
Content [][]byte Content [][]byte
@@ -76,13 +77,13 @@ func SoundsHandler(s *discordgo.Session, m *discordgo.MessageCreate) {
} }
// create new connection instance // create new connection instance
newInstance := &audioConnection{ newInstance := &AudioConnection{
guild: newGuild, Guild: newGuild,
session: s, Session: s,
sounds: make(map[string]*audioClip, 0), Sounds: make(map[string]*AudioClip, 0),
soundQueue: make(chan string, maxSoundQueue), SoundQueue: make(chan string, maxSoundQueue),
mutex: &sync.Mutex{}, Mutex: &sync.Mutex{},
audioListenerLock: false, AudioListenerLock: false,
} }
activeConnections[c.GuildID] = newInstance activeConnections[c.GuildID] = newInstance
@@ -95,7 +96,7 @@ func SoundsHandler(s *discordgo.Session, m *discordgo.MessageCreate) {
go activeConnections[c.GuildID].handleMessage(m) go activeConnections[c.GuildID].handleMessage(m)
} }
func (conn *audioConnection) handleMessage(m *discordgo.MessageCreate) { func (conn *AudioConnection) handleMessage(m *discordgo.MessageCreate) {
// check if valid command // check if valid command
if strings.HasPrefix(m.Content, config.Config.BotPrefix) { if strings.HasPrefix(m.Content, config.Config.BotPrefix) {
@@ -120,22 +121,22 @@ func (conn *audioConnection) handleMessage(m *discordgo.MessageCreate) {
} }
// dismiss bot from currnet channel if it's in one // dismiss bot from currnet channel if it's in one
func (conn *audioConnection) dismiss() { func (conn *AudioConnection) dismiss() {
if conn.voiceConnection != nil && !conn.soundPlayingLock && len(conn.soundQueue) == 0 { if conn.VoiceConnection != nil && !conn.SoundPlayingLock && len(conn.SoundQueue) == 0 {
conn.voiceConnection.Disconnect() conn.VoiceConnection.Disconnect()
} }
} }
// summon bot to channel that user is currently in // summon bot to channel that user is currently in
func (conn *audioConnection) summon(m *discordgo.MessageCreate) { func (conn *AudioConnection) summon(m *discordgo.MessageCreate) {
// Join the channel the user issued the command from if not in it // Join the channel the user issued the command from if not in it
if conn.voiceConnection == nil || conn.voiceConnection.ChannelID != m.ChannelID { if conn.VoiceConnection == nil || conn.VoiceConnection.ChannelID != m.ChannelID {
var err error var err error
// Find the channel that the message came from. // Find the channel that the message came from.
c, err := conn.session.State.Channel(m.ChannelID) c, err := conn.Session.State.Channel(m.ChannelID)
if err != nil { if err != nil {
// Could not find channel. // Could not find channel.
log.Error("User channel not found.") log.Error("User channel not found.")
@@ -143,7 +144,7 @@ func (conn *audioConnection) summon(m *discordgo.MessageCreate) {
} }
// Find the guild for that channel. // Find the guild for that channel.
g, err := conn.session.State.Guild(c.GuildID) g, err := conn.Session.State.Guild(c.GuildID)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
return return
@@ -153,19 +154,19 @@ func (conn *audioConnection) summon(m *discordgo.MessageCreate) {
for _, vs := range g.VoiceStates { for _, vs := range g.VoiceStates {
if vs.UserID == m.Author.ID { if vs.UserID == m.Author.ID {
conn.voiceConnection, err = conn.session.ChannelVoiceJoin(g.ID, vs.ChannelID, false, false) conn.VoiceConnection, err = conn.Session.ChannelVoiceJoin(g.ID, vs.ChannelID, false, false)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
// set the current channel // set the current channel
conn.currentChannel = c conn.CurrentChannel = c
// start listening to audio if not locked // start listening to audio if not locked
if !conn.audioListenerLock { if !conn.AudioListenerLock {
go conn.startAudioListener() go conn.startAudioListener()
conn.audioListenerLock = true conn.AudioListenerLock = true
} }
return return
@@ -176,10 +177,10 @@ func (conn *audioConnection) summon(m *discordgo.MessageCreate) {
} }
// play audio in channel that user is in // play audio in channel that user is in
func (conn *audioConnection) playAudio(soundName string, m *discordgo.MessageCreate) { func (conn *AudioConnection) playAudio(soundName string, m *discordgo.MessageCreate) {
// check if sound exists in memory // check if sound exists in memory
if _, ok := conn.sounds[soundName]; !ok { if _, ok := conn.Sounds[soundName]; !ok {
// try to load the sound if not found in memory // try to load the sound if not found in memory
err := conn.loadFile(soundName) err := conn.loadFile(soundName)
@@ -194,7 +195,7 @@ func (conn *audioConnection) playAudio(soundName string, m *discordgo.MessageCre
// add sound to queue if queue isn't full // add sound to queue if queue isn't full
select { select {
case conn.soundQueue <- soundName: case conn.SoundQueue <- soundName:
default: default:
return return
@@ -203,7 +204,7 @@ func (conn *audioConnection) playAudio(soundName string, m *discordgo.MessageCre
} }
// load audio file into memory // load audio file into memory
func (conn *audioConnection) loadFile(fileName string) error { func (conn *AudioConnection) loadFile(fileName string) error {
// scan directory for file // scan directory for file
files, _ := ioutil.ReadDir(config.Config.SoundsPath) files, _ := ioutil.ReadDir(config.Config.SoundsPath)
@@ -250,7 +251,7 @@ func (conn *audioConnection) loadFile(fileName string) error {
return errors.New("NewEncoder error.") return errors.New("NewEncoder error.")
} }
conn.sounds[fileName] = &audioClip{ conn.Sounds[fileName] = &AudioClip{
Content: make([][]byte, 0), Content: make([][]byte, 0),
Name: fileName, Name: fileName,
Extension: fextension, Extension: fextension,
@@ -274,17 +275,17 @@ func (conn *audioConnection) loadFile(fileName string) error {
} }
// append sound bytes to the content for this audio file // append sound bytes to the content for this audio file
conn.sounds[fileName].Content = append(conn.sounds[fileName].Content, opus) conn.Sounds[fileName].Content = append(conn.Sounds[fileName].Content, opus)
} }
} }
func (conn *audioConnection) clipAudio(m *discordgo.MessageCreate) { func (conn *AudioConnection) clipAudio(m *discordgo.MessageCreate) {
if len(conn.voiceClipQueue) < 10 { if len(conn.VoiceClipQueue) < 10 {
conn.session.ChannelMessageSend(m.ChannelID, "Clip failed.") conn.Session.ChannelMessageSend(m.ChannelID, "Clip failed.")
} else { } else {
writePacketsToFile(m.Author.Username, conn.voiceClipQueue) writePacketsToFile(m.Author.Username, conn.VoiceClipQueue)
conn.session.ChannelMessageSend(m.ChannelID, "Sound clipped!") conn.Session.ChannelMessageSend(m.ChannelID, "Sound clipped!")
} }
} }
@@ -329,10 +330,10 @@ loop:
} }
// start listening to the voice channel // start listening to the voice channel
func (conn *audioConnection) startAudioListener() { func (conn *AudioConnection) startAudioListener() {
if conn.voiceClipQueue == nil { if conn.VoiceClipQueue == nil {
conn.voiceClipQueue = make(chan *discordgo.Packet, voiceClipQueuePacketSize) conn.VoiceClipQueue = make(chan *discordgo.Packet, voiceClipQueuePacketSize)
} }
speakers := make(map[uint32]*gopus.Decoder) speakers := make(map[uint32]*gopus.Decoder)
@@ -343,7 +344,7 @@ loop:
select { select {
// grab incomming audio // grab incomming audio
case opusChannel, ok := <-conn.voiceConnection.OpusRecv: case opusChannel, ok := <-conn.VoiceConnection.OpusRecv:
if !ok { if !ok {
continue continue
} }
@@ -365,16 +366,16 @@ loop:
} }
// if channel is full trim off from beginning // if channel is full trim off from beginning
if len(conn.voiceClipQueue) == cap(conn.voiceClipQueue) { if len(conn.VoiceClipQueue) == cap(conn.VoiceClipQueue) {
<-conn.voiceClipQueue <-conn.VoiceClipQueue
} }
// add current packet to channel queue // add current packet to channel queue
conn.voiceClipQueue <- opusChannel conn.VoiceClipQueue <- opusChannel
// check if voice connection fails then break out of audio listener // check if voice connection fails then break out of audio listener
default: default:
if !conn.voiceConnection.Ready { if !conn.VoiceConnection.Ready {
break loop break loop
} }
@@ -385,31 +386,31 @@ loop:
} }
// remove lock upon exit // remove lock upon exit
conn.audioListenerLock = false conn.AudioListenerLock = false
} }
// playSounds - plays the current buffer to the provided channel. // playSounds - plays the current buffer to the provided channel.
func (conn *audioConnection) playSounds() (err error) { func (conn *AudioConnection) playSounds() (err error) {
for { for {
newSoundName := <-conn.soundQueue newSoundName := <-conn.SoundQueue
conn.toggleSoundPlayingLock(true) conn.toggleSoundPlayingLock(true)
if !conn.voiceConnection.Ready { if !conn.VoiceConnection.Ready {
continue continue
} }
// Start speaking. // Start speaking.
_ = conn.voiceConnection.Speaking(true) _ = conn.VoiceConnection.Speaking(true)
// Send the buffer data. // Send the buffer data.
for _, buff := range conn.sounds[newSoundName].Content { for _, buff := range conn.Sounds[newSoundName].Content {
conn.voiceConnection.OpusSend <- buff conn.VoiceConnection.OpusSend <- buff
} }
// Stop speaking // Stop speaking
_ = conn.voiceConnection.Speaking(false) _ = conn.VoiceConnection.Speaking(false)
// Sleep for a specificed amount of time before ending. // Sleep for a specificed amount of time before ending.
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
@@ -419,10 +420,10 @@ func (conn *audioConnection) playSounds() (err error) {
} }
func (conn *audioConnection) toggleSoundPlayingLock(playing bool) { func (conn *AudioConnection) toggleSoundPlayingLock(playing bool) {
conn.mutex.Lock() conn.Mutex.Lock()
conn.soundPlayingLock = playing conn.SoundPlayingLock = playing
conn.mutex.Unlock() conn.Mutex.Unlock()
} }
func checkErr(err error) { func checkErr(err error) {

View File

@@ -28,6 +28,7 @@ type configFile struct {
ClipsPath string `json:"clips_path"` ClipsPath string `json:"clips_path"`
AdminEmails []string `json:"admin_emails"` AdminEmails []string `json:"admin_emails"`
ModEmails []string `json:"mod_emails"`
ServerAddr string `json:"server_addr"` ServerAddr string `json:"server_addr"`
JWTKey string `json:"jwt_key"` JWTKey string `json:"jwt_key"`

View File

@@ -11,27 +11,34 @@ import (
"gopkg.in/dgrijalva/jwt-go.v3" "gopkg.in/dgrijalva/jwt-go.v3"
) )
// permission levels
const (
PermAdmin = 3
PermMod = 2
PermUser = 1
)
// CustomClaims - // CustomClaims -
type CustomClaims struct { type CustomClaims struct {
ID string `json:"id"` ID string `json:"id"`
Username string `json:"username"` Username string `json:"username"`
Discriminator string `json:"discriminator"` Discriminator string `json:"discriminator"`
Email string `json:"email"` Email string `json:"email"`
Permissions string `json:"permissions"` Permissions int `json:"permissions"`
jwt.StandardClaims jwt.StandardClaims
} }
// GetJWT - get json web token // GetJWT - get json web token
func GetJWT(user discord.User) (string, error) { func GetJWT(user discord.User) (string, error) {
permissions := "user" permissions := PermUser
// check if email is in config admin list if checkEmailPermissions(user.Email, config.Config.ModEmails) {
for _, email := range config.Config.AdminEmails { permissions = PermMod
if user.Email == email {
permissions = "admin"
break
} }
if checkEmailPermissions(user.Email, config.Config.AdminEmails) {
permissions = PermAdmin
} }
claims := CustomClaims{ claims := CustomClaims{
@@ -50,6 +57,31 @@ func GetJWT(user discord.User) (string, error) {
return token.SignedString([]byte(config.Config.JWTKey)) return token.SignedString([]byte(config.Config.JWTKey))
} }
func checkEmailPermissions(email string, emails []string) bool {
for _, e := range emails {
if email == e {
return true
}
}
return false
}
// AuthPermissions - secure end points based on auth levels
func AuthPermissions(p int) gin.HandlerFunc {
return func(c *gin.Context) {
cl, _ := c.Get("claims")
if claims, ok := cl.(*CustomClaims); ok {
if p <= claims.Permissions {
c.Next()
return
}
}
unauthorizedResponse(c, nil)
}
}
// AuthorizedJWT - jwt middleware // AuthorizedJWT - jwt middleware
func AuthorizedJWT() gin.HandlerFunc { func AuthorizedJWT() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
@@ -58,8 +90,7 @@ func AuthorizedJWT() gin.HandlerFunc {
tokenString := strings.Split(c.GetHeader("Authorization"), " ") tokenString := strings.Split(c.GetHeader("Authorization"), " ")
if len(tokenString) != 2 { if len(tokenString) != 2 {
c.JSON(401, "Unauthorized") unauthorizedResponse(c, nil)
c.Abort()
return return
} }
@@ -69,9 +100,7 @@ func AuthorizedJWT() gin.HandlerFunc {
}) })
if err != nil { if err != nil {
log.Error(err) unauthorizedResponse(c, err)
c.JSON(401, "Unauthorized")
c.Abort()
return return
} }
@@ -79,12 +108,18 @@ func AuthorizedJWT() gin.HandlerFunc {
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
c.Set("claims", claims) c.Set("claims", claims)
} else { } else {
log.Error(err) unauthorizedResponse(c, err)
c.JSON(401, "Unauthorized")
c.Abort()
return return
} }
c.Next() c.Next()
} }
} }
func unauthorizedResponse(c *gin.Context, err error) {
if err != nil {
log.Error(err)
}
c.JSON(401, "unauthorized")
c.Abort()
}

View File

@@ -1,6 +1,8 @@
package webserver package webserver
import ( import (
"github.com/gobuffalo/packr"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/mgerb/go-discord-bot/server/config" "github.com/mgerb/go-discord-bot/server/config"
"github.com/mgerb/go-discord-bot/server/webserver/handlers" "github.com/mgerb/go-discord-bot/server/webserver/handlers"
@@ -10,13 +12,15 @@ import (
func getRouter() *gin.Engine { func getRouter() *gin.Engine {
router := gin.Default() router := gin.Default()
router.Static("/static", "./dist/static") box := packr.NewBox("../../dist/static")
router.StaticFS("/static", box)
router.Static("/public/sounds", config.Config.SoundsPath) router.Static("/public/sounds", config.Config.SoundsPath)
router.Static("/public/youtube", "./youtube") router.Static("/public/youtube", "./youtube")
router.Static("/public/clips", config.Config.ClipsPath) router.Static("/public/clips", config.Config.ClipsPath)
router.NoRoute(func(c *gin.Context) { router.NoRoute(func(c *gin.Context) {
c.File("./dist/static/index.html") c.Data(200, "text/html", box.Bytes("index.html"))
}) })
api := router.Group("/api") api := router.Group("/api")