grim/wasdead

overhaul all the things
draft
2019-04-06, Gary Kramlich
17444953eb59
Parents 4a849898cd49
Children 285ce61c2d55
overhaul all the things

* Changed the main loop
* Moved to logsrus
* Broke out discord to it's own package
* Broke out twitch to it's own package
* Added an abstraction layer to allow for other streaming services in the future
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/discord/commands.go Sat Apr 06 05:22:07 2019 -0500
@@ -0,0 +1,75 @@
+package discord
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/bwmarrin/discordgo"
+ "github.com/dustin/go-humanize"
+ log "github.com/sirupsen/logrus"
+
+ "bitbucket.org/TheToyz/nowdead/presence"
+)
+
+type commandHandler func(c *DiscordClient, m *discordgo.MessageCreate) error
+
+var commands map[string]commandHandler
+
+func init() {
+ commands = map[string]commandHandler{
+ "!uptime": uptimeCommand,
+ "!setchannel": setChannelCommand,
+ "!islive": isLiveCommand,
+ }
+}
+
+func (c *DiscordClient) messageHandler(s *discordgo.Session, m *discordgo.MessageCreate) {
+ // ignore messages for ourselves
+ if m.Author.ID == s.State.User.ID {
+ return
+ }
+
+ log.Debugf("processing message %v", m)
+
+ parts := strings.Split(m.Content, " ")
+
+ command := strings.ToLower(parts[0])
+
+ if handler, found := commands[command]; found {
+ err := handler(c, m)
+ if err != nil {
+ log.Warnf("failed to send response: %v", err)
+ c.sendChannel(m.ChannelID, fmt.Sprintf("error: %#v", err))
+ }
+ }
+}
+
+func uptimeCommand(c *DiscordClient, m *discordgo.MessageCreate) error {
+ return c.sendChannel(
+ m.ChannelID,
+ fmt.Sprintf("Started %s", humanize.Time(c.started)),
+ )
+}
+
+func setChannelCommand(c *DiscordClient, m *discordgo.MessageCreate) error {
+ c.db.SetChannel(m.GuildID, m.ChannelID)
+
+ return c.sendChannel(
+ m.ChannelID,
+ fmt.Sprintf("Set <#%s> as the announcement channel", m.ChannelID),
+ )
+}
+
+func isLiveCommand(c *DiscordClient, m *discordgo.MessageCreate) error {
+ args := strings.Split(m.Content, " ")
+ if len(args) != 2 {
+ return fmt.Errorf("invalid arguments")
+ }
+
+ presence, err := presence.GetPresence("https://twitch.tv/" + args[1])
+ if err != nil {
+ return err
+ }
+
+ return c.sendPresence(m.GuildID, presence)
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/discord/discord.go Sat Apr 06 05:22:07 2019 -0500
@@ -0,0 +1,63 @@
+package discord
+
+import (
+ "errors"
+ "strings"
+ "time"
+
+ "github.com/bwmarrin/discordgo"
+
+ "bitbucket.org/TheToyz/nowdead/database"
+)
+
+type DiscordClient struct {
+ client *discordgo.Session
+ db database.Database
+ started time.Time
+}
+
+func New(token string, db database.Database) (*DiscordClient, error) {
+ client, err := discordgo.New("Bot " + strings.TrimSpace(token))
+ if err != nil {
+ return nil, err
+ }
+
+ dc := &DiscordClient{
+ client: client,
+ db: db,
+ started: time.Now().UTC(),
+ }
+
+ client.AddHandler(dc.messageHandler)
+ client.AddHandler(dc.presenceHandler)
+
+ return dc, nil
+}
+
+func (c *DiscordClient) Start(errChan chan error) {
+ err := c.client.Open()
+ if err != nil {
+ errChan <- err
+ return
+ }
+}
+
+func (c *DiscordClient) Shutdown() error {
+ return c.client.Close()
+}
+
+func (c *DiscordClient) send(guild, message string) error {
+ channelID := c.db.GetChannel(guild)
+ if channelID == "" {
+ return errors.New("No channel set")
+ }
+
+ return c.sendChannel(channelID, message)
+}
+
+func (c *DiscordClient) sendChannel(channelID, message string) error {
+ // ignore the message that we sent
+ _, err := c.client.ChannelMessageSend(channelID, message)
+
+ return err
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/discord/presence.go Sat Apr 06 05:22:07 2019 -0500
@@ -0,0 +1,74 @@
+package discord
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/bwmarrin/discordgo"
+ "github.com/dustin/go-humanize"
+ log "github.com/sirupsen/logrus"
+
+ "bitbucket.org/TheToyz/nowdead/presence"
+)
+
+func (c *DiscordClient) presenceHandler(s *discordgo.Session, p *discordgo.PresenceUpdate) {
+ if p.Game == nil {
+ return
+ }
+
+ log.Debugf("processing presence %v", p)
+
+ if p.Game.Type != discordgo.GameTypeStreaming {
+ presence, _ := presence.GetPresence(p.Game.URL)
+
+ if err := c.sendPresence(p.GuildID, presence); err != nil {
+ log.Warnf("failed to send presence to guild %s: %v", p.GuildID, err)
+ }
+ }
+}
+
+func (c *DiscordClient) sendPresence(guild string, presence presence.Presence) error {
+ channelID := c.db.GetChannel(guild)
+
+ if channelID == "" {
+ return errors.New("No channel set")
+ }
+
+ return c.sendPresenceChannel(channelID, presence)
+}
+
+func (c *DiscordClient) sendPresenceChannel(channelID string, presence presence.Presence) error {
+ _, err := c.client.ChannelMessageSendEmbed(
+ channelID,
+ &discordgo.MessageEmbed{
+ Image: &discordgo.MessageEmbedImage{
+ URL: presence.ProfileImageURL,
+ },
+ Author: &discordgo.MessageEmbedAuthor{
+ URL: presence.URL,
+ Name: fmt.Sprintf("%s is now live", presence.Username),
+ },
+ Thumbnail: &discordgo.MessageEmbedThumbnail{
+ URL: presence.ThumbnailURL,
+ },
+ Fields: []*discordgo.MessageEmbedField{
+ &discordgo.MessageEmbedField{
+ Name: "Title",
+ Value: presence.Title,
+ },
+ &discordgo.MessageEmbedField{
+ Name: "Viewers",
+ Value: humanize.Comma(presence.Viewers),
+ Inline: true,
+ },
+ &discordgo.MessageEmbedField{
+ Name: "Language",
+ Value: presence.Language,
+ Inline: true,
+ },
+ },
+ },
+ )
+
+ return err
+}
--- a/go.mod Sat Apr 06 00:39:10 2019 -0700
+++ b/go.mod Sat Apr 06 05:22:07 2019 -0500
@@ -5,7 +5,8 @@
require (
github.com/bwmarrin/discordgo v0.19.0
github.com/dustin/go-humanize v1.0.0
- github.com/go-redis/redis v6.15.2+incompatible // indirect
+ github.com/go-redis/redis v6.15.2+incompatible
github.com/nicklaw5/helix v0.5.1
github.com/prologic/bitcask v0.1.6
+ github.com/sirupsen/logrus v1.4.0
)
--- a/go.sum Sat Apr 06 00:39:10 2019 -0700
+++ b/go.sum Sat Apr 06 05:22:07 2019 -0500
@@ -47,6 +47,7 @@
github.com/prologic/trie v0.0.0-20190322091023-3972df81f9b5 h1:H8dTZzU3aWNQnuRyiT45J9szv7EFakAhFzsFq27t3Uo=
github.com/prologic/trie v0.0.0-20190322091023-3972df81f9b5/go.mod h1:LFuDmpHJGmciXd8Rl5YMhVlLMps9gz2GtYLzwxrFhzs=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.0 h1:yKenngtzGh+cUSSh6GWbxW2abRqhYUSR/t/6+2QqNvE=
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
@@ -85,6 +86,7 @@
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc h1:4gbWbmmPFp4ySWICouJl6emP0MyS31yy9SrTlAGFT+g=
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
--- a/main.go Sat Apr 06 00:39:10 2019 -0700
+++ b/main.go Sat Apr 06 05:22:07 2019 -0500
@@ -1,39 +1,33 @@
package main
import (
- "errors"
"flag"
"fmt"
- "log"
- "net/url"
"os"
"os/signal"
- "strings"
"syscall"
"time"
- "bitbucket.org/TheToyz/nowdead/database"
+ log "github.com/sirupsen/logrus"
- "github.com/dustin/go-humanize"
-
- "github.com/bwmarrin/discordgo"
- "github.com/nicklaw5/helix"
+ "bitbucket.org/TheToyz/nowdead/database"
+ "bitbucket.org/TheToyz/nowdead/discord"
+ "bitbucket.org/TheToyz/nowdead/presence"
+ "bitbucket.org/TheToyz/nowdead/twitch"
)
var (
DiscordToken string
TwitchToken string
DatabaseType string
- TwitchClient *helix.Client
- logger *log.Logger
startTime time.Time
db database.Database
)
func init() {
- logger = log.New(os.Stderr, " ", log.Ldate|log.Ltime)
- startTime = time.Now()
+ log.SetOutput(os.Stdout)
+ log.SetLevel(log.DebugLevel)
flag.StringVar(&DiscordToken, "t", "", "Discord Bot Token")
flag.StringVar(&TwitchToken, "tcid", "", "Twitch Client ID")
@@ -42,209 +36,53 @@
flag.Parse()
}
-func logDebug(v ...interface{}) {
- logger.SetPrefix("DEBUG ")
- logger.Println(v...)
-}
-
-func logInfo(v ...interface{}) {
- logger.SetPrefix("INFO ")
- logger.Println(v...)
-}
-
-func logError(v ...interface{}) {
- logger.SetPrefix("ERROR ")
- logger.Println(v...)
-}
-
-func logPanic(v ...interface{}) {
- logger.SetPrefix("PANIC ")
- logger.Panicln(v...)
-}
-
func main() {
db = database.Get(DatabaseType)
if len(DiscordToken) <= 0 || len(TwitchToken) <= 0 {
- logPanic("Discord Token and Twitch Client ID must be set")
+ log.Panic("Discord Token and Twitch Client ID must be set")
}
- logDebug("Discord Token:", DiscordToken)
- logDebug("Twitch Token:", TwitchToken)
+ if len(TwitchToken) > 0 {
+ provider, err := twitch.New(TwitchToken)
+ if err != nil {
+ log.Panic(err)
+ }
- client, err := helix.NewClient(&helix.Options{
- ClientID: TwitchToken,
- })
- if err != nil {
- logPanic(err)
+ presence.AddProvider("twitch.tv", provider)
+ log.Info("Added twitch provider")
}
- TwitchClient = client
- dg, err := discordgo.New("Bot " + strings.TrimSpace(DiscordToken))
+ // create our error channel that's used by the various clients
+ errChan := make(chan error, 1)
+
+ // create the discord client
+ dc, err := discord.New(DiscordToken, db)
if err != nil {
fmt.Println("error creating Discord session,", err)
return
}
-
- dg.AddHandler(presenceUpdate)
- dg.AddHandler(messageCreate)
-
- err = dg.Open()
- if err != nil {
- log.Panic(err)
- return
- }
+ dc.Start(errChan)
+ defer dc.Shutdown()
+ log.Info("Connected to discord")
- fmt.Println("Bot is now running. Press CTRL-C to exit.")
- sc := make(chan os.Signal, 1)
- signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
- <-sc
-
- dg.Close()
-}
-
-func presenceUpdate(sess *discordgo.Session, evt *discordgo.PresenceUpdate) {
- logDebug("PRESENSE UPDATE user-ID:", evt.User.ID)
-
- if evt.Game != nil {
- logDebug(fmt.Sprintf("PRESENSE UPDATE game: %#v", evt.Game))
+ // create a channel for handling unix signals
+ signalChan := make(chan os.Signal, 1)
+ signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
- if evt.Game.Type == discordgo.GameTypeStreaming {
- pURL, _ := url.Parse(evt.Game.URL)
-
- channel := db.GetChannel(evt.GuildID)
- if channel != "" {
- err := processPresenceUpdate(sess, channel, strings.TrimLeft(pURL.Path, "/"), evt.Game.URL)
-
- if err != nil {
- logError(err)
- }
- }
- }
- }
-}
-
-func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
- if m.Author.ID == s.State.User.ID {
- return
- }
-
- commandItems := strings.Split(m.Content, " ")
-
- if strings.EqualFold(commandItems[0], "!uptime") {
- duration := time.Now().Sub(startTime)
- channel := db.GetChannel(m.GuildID)
- if channel != "" {
- if channel != m.ChannelID {
+ // start the main loop
+ log.Info("Bot is now running. Press CTRL-C to exit.")
+ for {
+ select {
+ case err := <-errChan:
+ if err != nil {
+ log.Error(err)
return
}
- err := sendMessage(s, channel, fmt.Sprintf(
- "Uptime is: **%02d:%02d:%02d** (since **%s**)",
- int(duration.Hours()),
- int(duration.Minutes())%60,
- int(duration.Seconds())%60,
- startTime.Format(time.Stamp)))
-
- logError(err)
- }
- }
-
- if strings.EqualFold(commandItems[0], "!isLive") {
- if len(commandItems) == 1 {
+ case s := <-signalChan:
+ log.Infof("caught %s Exiting...", s)
return
}
- channel := db.GetChannel(m.GuildID)
- if channel != "" {
- if channel != m.ChannelID {
- return
- }
- err := processPresenceUpdate(s, channel, commandItems[1], "https://twitch.com/"+commandItems[1])
-
- if err != nil {
- logError(err)
-
- sendMessage(s, channel, fmt.Sprintf("_%s_ does not seem to be live right now!", commandItems[1]))
- }
-
- }
- }
-
- if strings.EqualFold(commandItems[0], "!init") {
- sendMessage(s, m.ChannelID, "All set up to talk in this channel!")
-
- db.SetChannel(m.GuildID, m.ChannelID)
}
}
-
-func sendMessage(sess *discordgo.Session, channelid, message string) error {
- logInfo("SENDING MESSAGE:", channelid, message)
- _, err := sess.ChannelMessageSend(channelid, message)
- return err
-}
-
-func processPresenceUpdate(s *discordgo.Session, channelid, twitchName, url string) error {
- resp, err := TwitchClient.GetUsers(&helix.UsersParams{
- Logins: []string{twitchName},
- })
-
- if err != nil {
- return err
- }
-
- data, _ := TwitchClient.GetStreams(&helix.StreamsParams{
- UserLogins: []string{twitchName},
- })
-
- if len(resp.Data.Users) <= 0 || len(data.Data.Streams) <= 0 {
- return errors.New("user was not found")
- }
-
- user := resp.Data.Users[0]
- stream := data.Data.Streams[0]
-
- f := make([]*discordgo.MessageEmbedField, 0)
- f = append(f, &discordgo.MessageEmbedField{
- Name: "Title",
- Value: stream.Title,
- })
-
- f = append(f, &discordgo.MessageEmbedField{
- Name: "Viewer",
- Value: humanize.Comma(int64(stream.ViewerCount)),
- Inline: true,
- })
-
- f = append(f, &discordgo.MessageEmbedField{
- Name: "Lagnauge",
- Value: stream.Language,
- Inline: true,
- })
-
- image := strings.Replace(stream.ThumbnailURL, "{width}", "400", -1)
- image = strings.Replace(image, "{height}", "225", -1)
- _, err = s.ChannelMessageSendEmbed(channelid, &discordgo.MessageEmbed{
- Image: &discordgo.MessageEmbedImage{
- URL: image,
- },
- Author: &discordgo.MessageEmbedAuthor{
- URL: url,
- Name: user.DisplayName + " is now live",
- },
- Thumbnail: &discordgo.MessageEmbedThumbnail{
- URL: user.ProfileImageURL,
- },
- Fields: f,
- })
-
- return err
-}
-
-func fileExist(name string) bool {
- if _, err := os.Stat(name); err != nil {
- if os.IsNotExist(err) {
- return false
- }
- }
- return true
-}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/presence/presence.go Sat Apr 06 05:22:07 2019 -0500
@@ -0,0 +1,29 @@
+package presence
+
+type Presence struct {
+ Username string
+ Title string
+ Viewers int64
+ Language string
+ URL string
+ ProfileImageURL string
+ ThumbnailURL string
+}
+
+type Provider interface {
+ GetPresence(url string) (Presence, error)
+}
+
+var providers map[string]Provider
+
+func init() {
+ providers = map[string]Provider{}
+}
+
+func AddProvider(hostname string, provider Provider) {
+ providers[hostname] = provider
+}
+
+func GetPresence(url string) (Presence, error) {
+ return providers["twitch.tv"].GetPresence(url)
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/twitch/twitch.go Sat Apr 06 05:22:07 2019 -0500
@@ -0,0 +1,67 @@
+package twitch
+
+import (
+ "errors"
+ "net/url"
+ "strings"
+
+ "github.com/nicklaw5/helix"
+
+ "bitbucket.org/TheToyz/nowdead/presence"
+)
+
+type Twitch struct {
+ client *helix.Client
+}
+
+func New(token string) (presence.Provider, error) {
+ client, err := helix.NewClient(&helix.Options{
+ ClientID: token,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return &Twitch{client: client}, nil
+}
+
+func (t *Twitch) GetPresence(uri string) (presence.Presence, error) {
+ parsedURL, _ := url.Parse(uri)
+ username := strings.TrimLeft(parsedURL.Path, "/")
+
+ users, err := t.client.GetUsers(&helix.UsersParams{
+ Logins: []string{username},
+ })
+ if err != nil {
+ return presence.Presence{}, err
+ }
+
+ streams, err := t.client.GetStreams(&helix.StreamsParams{
+ UserLogins: []string{username},
+ })
+ if err != nil {
+ return presence.Presence{}, err
+ }
+
+ if len(users.Data.Users) <= 0 || len(streams.Data.Streams) <= 0 {
+ return presence.Presence{}, errors.New("user not found")
+ }
+
+ user := users.Data.Users[0]
+ stream := streams.Data.Streams[0]
+
+ profile_url := strings.Replace(stream.ThumbnailURL, "{width}", "400", -1)
+ profile_url = strings.Replace(profile_url, "{height}", "255", -1)
+
+ p := presence.Presence{
+ Username: user.DisplayName,
+ Title: stream.Title,
+ Viewers: int64(stream.ViewerCount),
+ Language: stream.Language,
+ ProfileImageURL: profile_url,
+ ThumbnailURL: user.ProfileImageURL,
+ URL: uri,
+ }
+
+ return p, nil
+}