twitch: Add IRC-Discord bridge

* Should connect a bridge to streamer's channel any time a stream starts
* Should disconnect when stream ends
* Add `track` and `untrack` commands to manually modify bridge
* Adds support for creating Discord threads
This commit is contained in:
Chris Sexton 2022-09-18 17:43:44 -04:00
parent bc710219ed
commit dd262f524e
8 changed files with 453 additions and 67 deletions

View File

@ -30,6 +30,8 @@ type Discord struct {
registeredCmds []*discordgo.ApplicationCommand registeredCmds []*discordgo.ApplicationCommand
cmdHandlers map[string]CmdHandler cmdHandlers map[string]CmdHandler
guildID string
} }
func New(config *config.Config) *Discord { func New(config *config.Config) *Discord {
@ -37,12 +39,17 @@ func New(config *config.Config) *Discord {
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("Could not connect to Discord") log.Fatal().Err(err).Msg("Could not connect to Discord")
} }
guildID := config.Get("discord.guildid", "")
if guildID == "" {
log.Fatal().Msgf("You must set either DISCORD_GUILDID env or discord.guildid db config")
}
d := &Discord{ d := &Discord{
config: config, config: config,
client: client, client: client,
uidCache: map[string]string{}, uidCache: map[string]string{},
registeredCmds: []*discordgo.ApplicationCommand{}, registeredCmds: []*discordgo.ApplicationCommand{},
cmdHandlers: map[string]CmdHandler{}, cmdHandlers: map[string]CmdHandler{},
guildID: guildID,
} }
d.client.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { d.client.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
if h, ok := d.cmdHandlers[i.ApplicationCommandData().Name]; ok { if h, ok := d.cmdHandlers[i.ApplicationCommandData().Name]; ok {
@ -155,12 +162,7 @@ func (d *Discord) GetEmojiList(force bool) map[string]string {
if d.emojiCache != nil && !force { if d.emojiCache != nil && !force {
return d.emojiCache return d.emojiCache
} }
guildID := d.config.Get("discord.guildid", "") e, err := d.client.GuildEmojis(d.guildID)
if guildID == "" {
log.Error().Msg("no guild ID set")
return map[string]string{}
}
e, err := d.client.GuildEmojis(guildID)
if err != nil { if err != nil {
log.Error().Err(err).Msg("could not retrieve emojis") log.Error().Err(err).Msg("could not retrieve emojis")
return map[string]string{} return map[string]string{}
@ -220,17 +222,12 @@ func (d *Discord) convertUser(u *discordgo.User) *user.User {
} }
nick := u.Username nick := u.Username
guildID := d.config.Get("discord.guildid", "") mem, err := d.client.GuildMember(d.guildID, u.ID)
if guildID == "" {
log.Error().Msg("no guild ID set")
} else {
mem, err := d.client.GuildMember(guildID, u.ID)
if err != nil { if err != nil {
log.Error().Err(err).Msg("could not get guild member") log.Error().Err(err).Msg("could not get guild member")
} else if mem.Nick != "" { } else if mem.Nick != "" {
nick = mem.Nick nick = mem.Nick
} }
}
return &user.User{ return &user.User{
ID: u.ID, ID: u.ID,
@ -327,9 +324,8 @@ func (d *Discord) Emojy(name string) string {
} }
func (d *Discord) UploadEmojy(emojy, path string) error { func (d *Discord) UploadEmojy(emojy, path string) error {
guildID := d.config.Get("discord.guildid", "")
defaultRoles := d.config.GetArray("discord.emojyRoles", []string{}) defaultRoles := d.config.GetArray("discord.emojyRoles", []string{})
_, err := d.client.GuildEmojiCreate(guildID, &discordgo.EmojiParams{ _, err := d.client.GuildEmojiCreate(d.guildID, &discordgo.EmojiParams{
emojy, path, defaultRoles, emojy, path, defaultRoles,
}) })
if err != nil { if err != nil {
@ -339,9 +335,8 @@ func (d *Discord) UploadEmojy(emojy, path string) error {
} }
func (d *Discord) DeleteEmojy(emojy string) error { func (d *Discord) DeleteEmojy(emojy string) error {
guildID := d.config.Get("discord.guildid", "")
emojyID := d.GetEmojySnowflake(emojy) emojyID := d.GetEmojySnowflake(emojy)
return d.client.GuildEmojiDelete(guildID, emojyID) return d.client.GuildEmojiDelete(d.guildID, emojyID)
} }
func (d *Discord) URLFormat(title, url string) string { func (d *Discord) URLFormat(title, url string) string {
@ -350,11 +345,7 @@ func (d *Discord) URLFormat(title, url string) string {
// GetChannelName returns the channel ID for a human-friendly name (if possible) // GetChannelName returns the channel ID for a human-friendly name (if possible)
func (d *Discord) GetChannelID(name string) string { func (d *Discord) GetChannelID(name string) string {
guildID := d.config.Get("discord.guildid", "") chs, err := d.client.GuildChannels(d.guildID)
if guildID == "" {
return name
}
chs, err := d.client.GuildChannels(guildID)
if err != nil { if err != nil {
return name return name
} }
@ -378,11 +369,7 @@ func (d *Discord) GetChannelName(id string) string {
func (d *Discord) GetRoles() ([]bot.Role, error) { func (d *Discord) GetRoles() ([]bot.Role, error) {
ret := []bot.Role{} ret := []bot.Role{}
guildID := d.config.Get("discord.guildid", "") roles, err := d.client.GuildRoles(d.guildID)
if guildID == "" {
return nil, errors.New("no guildID set")
}
roles, err := d.client.GuildRoles(guildID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -398,24 +385,22 @@ func (d *Discord) GetRoles() ([]bot.Role, error) {
} }
func (d *Discord) SetRole(userID, roleID string) error { func (d *Discord) SetRole(userID, roleID string) error {
guildID := d.config.Get("discord.guildid", "") member, err := d.client.GuildMember(d.guildID, userID)
member, err := d.client.GuildMember(guildID, userID)
if err != nil { if err != nil {
return err return err
} }
for _, r := range member.Roles { for _, r := range member.Roles {
if r == roleID { if r == roleID {
return d.client.GuildMemberRoleRemove(guildID, userID, roleID) return d.client.GuildMemberRoleRemove(d.guildID, userID, roleID)
} }
} }
return d.client.GuildMemberRoleAdd(guildID, userID, roleID) return d.client.GuildMemberRoleAdd(d.guildID, userID, roleID)
} }
type CmdHandler func(s *discordgo.Session, i *discordgo.InteractionCreate) type CmdHandler func(s *discordgo.Session, i *discordgo.InteractionCreate)
func (d *Discord) RegisterSlashCmd(c discordgo.ApplicationCommand, handler CmdHandler) error { func (d *Discord) RegisterSlashCmd(c discordgo.ApplicationCommand, handler CmdHandler) error {
guildID := d.config.Get("discord.guildid", "") cmd, err := d.client.ApplicationCommandCreate(d.client.State.User.ID, d.guildID, &c)
cmd, err := d.client.ApplicationCommandCreate(d.client.State.User.ID, guildID, &c)
d.cmdHandlers[c.Name] = handler d.cmdHandlers[c.Name] = handler
if err != nil { if err != nil {
return err return err
@ -426,17 +411,15 @@ func (d *Discord) RegisterSlashCmd(c discordgo.ApplicationCommand, handler CmdHa
func (d *Discord) Shutdown() { func (d *Discord) Shutdown() {
log.Debug().Msgf("Shutting down and deleting %d slash commands", len(d.registeredCmds)) log.Debug().Msgf("Shutting down and deleting %d slash commands", len(d.registeredCmds))
guildID := d.config.Get("discord.guildid", "")
for _, c := range d.registeredCmds { for _, c := range d.registeredCmds {
if err := d.client.ApplicationCommandDelete(d.client.State.User.ID, guildID, c.ID); err != nil { if err := d.client.ApplicationCommandDelete(d.client.State.User.ID, d.guildID, c.ID); err != nil {
log.Error().Err(err).Msgf("could not delete command %s", c.Name) log.Error().Err(err).Msgf("could not delete command %s", c.Name)
} }
} }
} }
func (d *Discord) Nick(nick string) error { func (d *Discord) Nick(nick string) error {
guildID := d.config.Get("discord.guildid", "") return d.client.GuildMemberNickname(d.guildID, "@me", nick)
return d.client.GuildMemberNickname(guildID, "@me", nick)
} }
func (d *Discord) Topic(channelID string) (string, error) { func (d *Discord) Topic(channelID string) (string, error) {
@ -454,3 +437,34 @@ func (d *Discord) SetTopic(channelID, topic string) error {
_, err := d.client.ChannelEditComplex(channelID, ce) _, err := d.client.ChannelEditComplex(channelID, ce)
return err return err
} }
type ThreadStart struct {
Name string `json:"name"`
AutoArchiveDuration int `json:"auto_archive_duration,omitempty"`
RateLimitPerUser int `json:"rate_limit_per_user,omitempty"`
AppliedTags []string `json:"applied_tags,omitempty"`
Message ForumMessageData `json:"message"`
}
type ForumMessageData struct {
Content string `json:"content"`
}
func (d *Discord) CreateRoom(name, message, parent string, duration int) (string, error) {
data := &ThreadStart{
Name: name,
AutoArchiveDuration: duration,
Message: ForumMessageData{message},
}
ch := &discordgo.Channel{}
endpoint := discordgo.EndpointChannelThreads(parent)
body, err := d.client.RequestWithBucketID("POST", endpoint, data, endpoint)
if err != nil {
return "", err
}
if err = discordgo.Unmarshal(body, &ch); err != nil {
return "", err
}
return ch.ID, nil
}

7
go.mod
View File

@ -43,11 +43,13 @@ require (
github.com/antchfx/xpath v1.1.1 // indirect github.com/antchfx/xpath v1.1.1 // indirect
github.com/armon/go-radix v1.0.0 // indirect github.com/armon/go-radix v1.0.0 // indirect
github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330 // indirect github.com/azr/backoff v0.0.0-20160115115103-53511d3c7330 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc // indirect github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc // indirect
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 // indirect github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 // indirect
github.com/gempir/go-twitch-irc/v3 v3.2.0 // indirect
github.com/go-stack/stack v1.8.0 // indirect github.com/go-stack/stack v1.8.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
@ -79,11 +81,12 @@ require (
github.com/temoto/robotstxt v1.1.1 // indirect github.com/temoto/robotstxt v1.1.1 // indirect
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
github.com/ttacon/libphonenumber v1.1.0 // indirect github.com/ttacon/libphonenumber v1.1.0 // indirect
github.com/vikpe/twitch-chatbot v0.2.0 // indirect
golang.org/x/exp v0.0.0-20191014171548-69215a2ee97e // indirect golang.org/x/exp v0.0.0-20191014171548-69215a2ee97e // indirect
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959 // indirect golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
gonum.org/v1/gonum v0.6.0 // indirect gonum.org/v1/gonum v0.6.0 // indirect
google.golang.org/appengine v1.6.5 // indirect google.golang.org/appengine v1.6.5 // indirect

12
go.sum
View File

@ -31,6 +31,8 @@ github.com/bwmarrin/discordgo v0.26.1 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89
github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/cdipaolo/goml v0.0.0-20190412180403-e1f51f713598 h1:j2XRGH5Y5uWtBYXGwmrjKeM/kfu/jh7ZcnrGvyN5Ttk= github.com/cdipaolo/goml v0.0.0-20190412180403-e1f51f713598 h1:j2XRGH5Y5uWtBYXGwmrjKeM/kfu/jh7ZcnrGvyN5Ttk=
github.com/cdipaolo/goml v0.0.0-20190412180403-e1f51f713598/go.mod h1:sduMkaHcXDIWurl/Bd/z0rNEUHw5tr6LUA9IO8E9o0o= github.com/cdipaolo/goml v0.0.0-20190412180403-e1f51f713598/go.mod h1:sduMkaHcXDIWurl/Bd/z0rNEUHw5tr6LUA9IO8E9o0o=
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff h1:+TEqaP0eO1unI7XHHFeMDhsxhLDIb0x8KYuZbqbAmxA= github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff h1:+TEqaP0eO1unI7XHHFeMDhsxhLDIb0x8KYuZbqbAmxA=
@ -54,6 +56,8 @@ github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkF
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 h1:GOfMz6cRgTJ9jWV0qAezv642OhPnKEG7gtUjJSdStHE= github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17 h1:GOfMz6cRgTJ9jWV0qAezv642OhPnKEG7gtUjJSdStHE=
github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17/go.mod h1:HfkOCN6fkKKaPSAeNq/er3xObxTW4VLeY6UUK895gLQ= github.com/garyburd/go-oauth v0.0.0-20180319155456-bca2e7f09a17/go.mod h1:HfkOCN6fkKKaPSAeNq/er3xObxTW4VLeY6UUK895gLQ=
github.com/gempir/go-twitch-irc/v3 v3.2.0 h1:ENhsa7RgBE1GMmDqe0iMkvcSYfgw6ZsXilt+sAg32/U=
github.com/gempir/go-twitch-irc/v3 v3.2.0/go.mod h1:/W9KZIiyizVecp4PEb7kc4AlIyXKiCmvlXrzlpPUytU=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/httprate v0.7.0 h1:8W0dF7Xa2Duz2p8ncGaehIphrxQGNlOtoGY0+NRRfjQ= github.com/go-chi/httprate v0.7.0 h1:8W0dF7Xa2Duz2p8ncGaehIphrxQGNlOtoGY0+NRRfjQ=
@ -176,6 +180,8 @@ github.com/ttacon/libphonenumber v1.1.0/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkU
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158 h1:p3rTUXxzuKsBOsHlkly7+rj9wagFBKeIsCDKkDII9sw= github.com/velour/velour v0.0.0-20160303155839-8e090e68d158 h1:p3rTUXxzuKsBOsHlkly7+rj9wagFBKeIsCDKkDII9sw=
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158/go.mod h1:Ojy3BTOiOTwpHpw7/HNi+TVTFppao191PQs+Qc3sqpE= github.com/velour/velour v0.0.0-20160303155839-8e090e68d158/go.mod h1:Ojy3BTOiOTwpHpw7/HNi+TVTFppao191PQs+Qc3sqpE=
github.com/vikpe/twitch-chatbot v0.2.0 h1:iVzuha6xLq+mlcs0MeF3tnjtJNZfX2RV3Vxqv3Zjjhw=
github.com/vikpe/twitch-chatbot v0.2.0/go.mod h1:IUfuGEwGaDglqrpVls1vPoiBsb8yfZIeAdvGVzGxbP4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
@ -203,6 +209,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -215,8 +223,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959 h1:qSa+Hg9oBe6UJXrznE+yYvW51V9UbyIj/nj/KpDigo8= golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 h1:ohgcoMbSofXygzo6AD2I1kz3BFmW1QArPYTtwEM3UXc=
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -5,6 +5,7 @@ package main
import ( import (
"flag" "flag"
"github.com/velour/catbase/plugins/pagecomment" "github.com/velour/catbase/plugins/pagecomment"
"github.com/velour/catbase/plugins/talker"
"github.com/velour/catbase/plugins/topic" "github.com/velour/catbase/plugins/topic"
"io" "io"
"math/rand" "math/rand"
@ -64,7 +65,6 @@ import (
"github.com/velour/catbase/plugins/rss" "github.com/velour/catbase/plugins/rss"
"github.com/velour/catbase/plugins/sisyphus" "github.com/velour/catbase/plugins/sisyphus"
"github.com/velour/catbase/plugins/stock" "github.com/velour/catbase/plugins/stock"
"github.com/velour/catbase/plugins/talker"
"github.com/velour/catbase/plugins/tell" "github.com/velour/catbase/plugins/tell"
"github.com/velour/catbase/plugins/tldr" "github.com/velour/catbase/plugins/tldr"
"github.com/velour/catbase/plugins/twitch" "github.com/velour/catbase/plugins/twitch"
@ -132,6 +132,7 @@ func main() {
b.AddPlugin(admin.New(b)) b.AddPlugin(admin.New(b))
b.AddPlugin(roles.New(b)) b.AddPlugin(roles.New(b))
b.AddPlugin(twitch.New(b))
b.AddPlugin(pagecomment.New(b)) b.AddPlugin(pagecomment.New(b))
b.AddPlugin(gpt3.New(b)) b.AddPlugin(gpt3.New(b))
b.AddPlugin(secrets.New(b)) b.AddPlugin(secrets.New(b))
@ -141,7 +142,6 @@ func main() {
b.AddPlugin(last.New(b)) b.AddPlugin(last.New(b))
b.AddPlugin(first.New(b)) b.AddPlugin(first.New(b))
b.AddPlugin(leftpad.New(b)) b.AddPlugin(leftpad.New(b))
b.AddPlugin(talker.New(b))
b.AddPlugin(dice.New(b)) b.AddPlugin(dice.New(b))
b.AddPlugin(picker.New(b)) b.AddPlugin(picker.New(b))
b.AddPlugin(beers.New(b)) b.AddPlugin(beers.New(b))
@ -153,7 +153,6 @@ func main() {
b.AddPlugin(babbler.New(b)) b.AddPlugin(babbler.New(b))
b.AddPlugin(rss.New(b)) b.AddPlugin(rss.New(b))
b.AddPlugin(reaction.New(b)) b.AddPlugin(reaction.New(b))
b.AddPlugin(twitch.New(b))
b.AddPlugin(inventory.New(b)) b.AddPlugin(inventory.New(b))
b.AddPlugin(rpgORdie.New(b)) b.AddPlugin(rpgORdie.New(b))
b.AddPlugin(sisyphus.New(b)) b.AddPlugin(sisyphus.New(b))
@ -177,6 +176,7 @@ func main() {
b.AddPlugin(emojy.New(b)) b.AddPlugin(emojy.New(b))
b.AddPlugin(cowboy.New(b)) b.AddPlugin(cowboy.New(b))
b.AddPlugin(topic.New(b)) b.AddPlugin(topic.New(b))
b.AddPlugin(talker.New(b))
// catches anything left, will always return true // catches anything left, will always return true
b.AddPlugin(fact.New(b)) b.AddPlugin(fact.New(b))

142
plugins/twitch/bridge.go Normal file
View File

@ -0,0 +1,142 @@
package twitch
import (
"fmt"
"github.com/cenkalti/backoff/v4"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/connectors/discord"
"strings"
)
func (t *Twitch) mkBridge(r bot.Request) bool {
ircCh := "#" + r.Values["twitchChannel"]
if err := t.startConn(); err != nil {
t.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("Could not connect to IRC: %s", err))
}
t.irc.Join(ircCh)
t.bridgeMap[r.Msg.Channel] = ircCh
t.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("This post is tracking %s", ircCh))
return true
}
func (t *Twitch) rmBridge(r bot.Request) bool {
ch, ok := t.bridgeMap[r.Msg.Channel]
if ok {
delete(t.bridgeMap, r.Msg.Channel)
t.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("No longer tracking %s.", ch))
return true
}
t.b.Send(r.Conn, bot.Message, r.Msg.Channel, "This is not a connected bridge channel.")
return true
}
func (t *Twitch) bridgeMsg(r bot.Request) bool {
if ircCh := t.bridgeMap[r.Msg.Channel]; ircCh != "" {
if t.irc == nil {
if err := t.startConn(); err != nil {
t.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("Could not connect to IRC: %s", err))
}
t.irc.Join(ircCh)
}
replaceSet := t.c.Get("twitch.replaceset", "\"'-")
who := r.Msg.User.Name
for _, c := range replaceSet {
who = strings.ReplaceAll(who, string(c), "")
}
who = strings.Split(who, " ")[0][:9]
msg := fmt.Sprintf("%s: %s", who, r.Msg.Body)
t.irc.sendMessage(ircCh, msg)
}
return false
}
func (t *Twitch) ircMsg(channel, who, body string) {
for thread, ircCh := range t.bridgeMap {
if ircCh == channel {
t.b.Send(t.b.DefaultConnector(), bot.Message, thread, fmt.Sprintf("%s: %s", who, body))
}
}
}
func (t *Twitch) startBridgeMsg(threadName, twitchChannel, msg string) error {
if !strings.HasPrefix(twitchChannel, "#") {
twitchChannel = "#" + twitchChannel
}
if err := t.startConn(); err != nil {
return err
}
chID, err := t.mkForumPost(threadName, msg)
if err != nil {
return err
}
log.Debug().Msgf("Opened thread %s", chID)
err = t.irc.Join(twitchChannel)
if err != nil {
return err
}
t.bridgeMap[chID] = twitchChannel
return nil
}
func (t *Twitch) startBridge(threadName, twitchChannel string) error {
if !strings.HasPrefix(twitchChannel, "#") {
twitchChannel = "#" + twitchChannel
}
msg := fmt.Sprintf("This post is tracking %s", twitchChannel)
return t.startBridgeMsg(threadName, twitchChannel, msg)
}
func (t *Twitch) startConn() error {
if t.irc == nil {
err := backoff.Retry(func() error {
err := t.connect()
if err != nil {
log.Error().Err(err).Msg("could not connect to IRC")
return err
}
return nil
}, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 5))
if err != nil {
return err
}
}
return nil
}
func (t *Twitch) connect() error {
t.ircLock.Lock()
defer t.ircLock.Unlock()
twitchServer := t.c.Get("twitch.ircserver", "irc.chat.twitch.tv:6697")
twitchNick := t.c.Get("twitch.nick", "")
twitchPass := t.c.Get("twitch.pass", "")
twitchIRC, err := t.ConnectIRC(twitchServer, twitchNick, twitchPass, t.ircMsg, func() {
t.ircLock.Lock()
defer t.ircLock.Unlock()
t.irc = nil
})
if err != nil {
return err
}
t.irc = twitchIRC
return nil
}
func (t *Twitch) mkForumPost(name, msg string) (string, error) {
forum := t.c.Get("twitch.forum", "")
if forum == "" {
return "", fmt.Errorf("no forum available")
}
switch c := t.b.DefaultConnector().(type) {
case *discord.Discord:
chID, err := c.CreateRoom(name, msg, forum, t.c.GetInt("twitch.threadduration", 60))
if err != nil {
return "", err
}
return chID, nil
default:
return "", fmt.Errorf("non-Discord connectors not supported")
}
}

177
plugins/twitch/irc.go Normal file
View File

@ -0,0 +1,177 @@
package twitch
import (
"github.com/rs/zerolog/log"
"github.com/velour/catbase/config"
"github.com/velour/velour/irc"
"io"
"time"
)
var throttle <-chan time.Time
type eventFunc func(channel, who, body string)
type IRC struct {
t *Twitch
c *config.Config
client *irc.Client
event eventFunc
quit chan bool
}
func (t *Twitch) ConnectIRC(server, user, pass string, handler eventFunc, disconnect func()) (*IRC, error) {
log.Debug().Msgf("Connecting to %s, %s:%s", server, user, pass)
i := &IRC{
t: t,
c: t.c,
event: handler}
wait := make(chan bool)
go i.serve(server, user, pass, wait, disconnect)
<-wait
return i, nil
}
func (i *IRC) Join(channel string) error {
i.client.Out <- irc.Msg{Cmd: irc.JOIN, Args: []string{channel}}
return nil
}
func (i *IRC) Say(channel, body string) error {
if _, err := i.sendMessage(channel, body); err != nil {
return err
}
return nil
}
func (i *IRC) serve(server, user, pass string, wait chan bool, disconnect func()) {
if i.event == nil {
log.Error().Msgf("Missing event handler")
wait <- true
return
}
var err error
i.client, err = irc.DialSSL(server, user, user, pass, true)
if err != nil {
log.Error().
Err(err).
Strs("args", []string{server, user, pass}).
Msgf("Could not connect")
wait <- true
return
}
i.client.Out <- irc.Msg{Cmd: "CAP REQ", Args: []string{":twitch.tv/membership"}}
i.quit = make(chan bool)
go i.handleConnection()
wait <- true
<-i.quit
disconnect()
}
func (i *IRC) handleMsg(msg irc.Msg) {
switch msg.Cmd {
case irc.ERROR:
log.Info().Msgf("Received error: " + msg.Raw)
case irc.PING:
i.client.Out <- irc.Msg{Cmd: irc.PONG}
case irc.PONG:
// OK, ignore
case irc.KICK:
fallthrough
case irc.TOPIC:
fallthrough
case irc.NOTICE:
fallthrough
case irc.PRIVMSG:
if len(msg.Args) < 2 {
break
}
i.event(msg.Args[0], msg.Origin, msg.Args[1])
case irc.QUIT:
log.Debug().
Interface("msg", msg).
Msgf("QUIT")
i.quit <- true
default:
log.Debug().
Interface("msg", msg).
Msgf("IRC EVENT")
}
}
func (i *IRC) sendMessage(channel, message string, args ...any) (string, error) {
for len(message) > 0 {
m := irc.Msg{
Cmd: "PRIVMSG",
Args: []string{channel, message},
}
_, err := m.RawString()
if err != nil {
mtl := err.(irc.MsgTooLong)
m.Args[1] = message[:mtl.NTrunc]
message = message[mtl.NTrunc:]
} else {
message = ""
}
if throttle == nil {
ratePerSec := i.c.GetInt("RatePerSec", 5)
throttle = time.Tick(time.Second / time.Duration(ratePerSec))
}
<-throttle
i.client.Out <- m
}
return "NO_IRC_IDENTIFIERS", nil
}
func (i *IRC) handleConnection() {
pingTime := time.Duration(i.c.GetInt("twitch.pingtime", 60)) * time.Second
t := time.NewTimer(pingTime)
defer func() {
t.Stop()
close(i.client.Out)
for err := range i.client.Errors {
if err != io.EOF {
log.Error().Err(err)
}
}
}()
for {
select {
case msg, ok := <-i.client.In:
if !ok { // disconnect
i.quit <- true
return
}
t.Stop()
t = time.NewTimer(pingTime)
i.handleMsg(msg)
case <-t.C:
i.client.Out <- irc.Msg{Cmd: irc.PING, Args: []string{i.client.Server}}
t = time.NewTimer(pingTime)
case err, ok := <-i.client.Errors:
if ok && err != io.EOF {
log.Error().Err(err)
i.quit <- true
return
}
}
}
}

View File

@ -9,6 +9,7 @@ import (
"net/url" "net/url"
"regexp" "regexp"
"strings" "strings"
"sync"
"text/template" "text/template"
"time" "time"
@ -26,11 +27,15 @@ const (
stoppedStreamingTplFallback = "{{.Name}} just stopped streaming" stoppedStreamingTplFallback = "{{.Name}} just stopped streaming"
) )
type TwitchPlugin struct { type Twitch struct {
b bot.Bot b bot.Bot
c *config.Config c *config.Config
twitchList map[string]*Twitcher twitchList map[string]*Twitcher
tbl bot.HandlerTable tbl bot.HandlerTable
ircConnected bool
irc *IRC
ircLock sync.Mutex
bridgeMap map[string]string
} }
type Twitcher struct { type Twitcher struct {
@ -62,11 +67,12 @@ type stream struct {
} `json:"pagination"` } `json:"pagination"`
} }
func New(b bot.Bot) *TwitchPlugin { func New(b bot.Bot) *Twitch {
p := &TwitchPlugin{ p := &Twitch{
b: b, b: b,
c: b.Config(), c: b.Config(),
twitchList: map[string]*Twitcher{}, twitchList: map[string]*Twitcher{},
bridgeMap: map[string]string{},
} }
for _, ch := range p.c.GetArray("Twitch.Channels", []string{}) { for _, ch := range p.c.GetArray("Twitch.Channels", []string{}) {
@ -90,13 +96,13 @@ func New(b bot.Bot) *TwitchPlugin {
return p return p
} }
func (p *TwitchPlugin) registerWeb() { func (p *Twitch) registerWeb() {
r := chi.NewRouter() r := chi.NewRouter()
r.HandleFunc("/{user}", p.serveStreaming) r.HandleFunc("/{user}", p.serveStreaming)
p.b.RegisterWeb(r, "/isstreaming") p.b.RegisterWeb(r, "/isstreaming")
} }
func (p *TwitchPlugin) serveStreaming(w http.ResponseWriter, r *http.Request) { func (p *Twitch) serveStreaming(w http.ResponseWriter, r *http.Request) {
userName := strings.ToLower(chi.URLParam(r, "user")) userName := strings.ToLower(chi.URLParam(r, "user"))
if userName == "" { if userName == "" {
fmt.Fprint(w, "User not found.") fmt.Fprint(w, "User not found.")
@ -126,7 +132,7 @@ func (p *TwitchPlugin) serveStreaming(w http.ResponseWriter, r *http.Request) {
} }
} }
func (p *TwitchPlugin) register() { func (p *Twitch) register() {
p.tbl = bot.HandlerTable{ p.tbl = bot.HandlerTable{
{ {
Kind: bot.Message, IsCmd: true, Kind: bot.Message, IsCmd: true,
@ -146,12 +152,29 @@ func (p *TwitchPlugin) register() {
HelpText: "Reset the twitch templates", HelpText: "Reset the twitch templates",
Handler: p.resetTwitch, Handler: p.resetTwitch,
}, },
{
Kind: bot.Message, IsCmd: true,
Regex: regexp.MustCompile(`(?i)^track (?P<twitchChannel>.+)$`),
HelpText: "Bridge to this channel",
Handler: p.mkBridge,
},
{
Kind: bot.Message, IsCmd: true,
Regex: regexp.MustCompile(`(?i)^untrack$`),
HelpText: "Disconnect a bridge (only in bridged channel)",
Handler: p.rmBridge,
},
{
Kind: bot.Message, IsCmd: false,
Regex: regexp.MustCompile(`.*`),
Handler: p.bridgeMsg,
},
} }
p.b.Register(p, bot.Help, p.help) p.b.Register(p, bot.Help, p.help)
p.b.RegisterTable(p, p.tbl) p.b.RegisterTable(p, p.tbl)
} }
func (p *TwitchPlugin) twitchStatus(r bot.Request) bool { func (p *Twitch) twitchStatus(r bot.Request) bool {
channel := r.Msg.Channel channel := r.Msg.Channel
if users := p.c.GetArray("Twitch."+channel+".Users", []string{}); len(users) > 0 { if users := p.c.GetArray("Twitch."+channel+".Users", []string{}); len(users) > 0 {
for _, twitcherName := range users { for _, twitcherName := range users {
@ -168,7 +191,7 @@ func (p *TwitchPlugin) twitchStatus(r bot.Request) bool {
return true return true
} }
func (p *TwitchPlugin) twitchUserStatus(r bot.Request) bool { func (p *Twitch) twitchUserStatus(r bot.Request) bool {
who := strings.ToLower(r.Values["who"]) who := strings.ToLower(r.Values["who"])
if t, ok := p.twitchList[who]; ok { if t, ok := p.twitchList[who]; ok {
err := p.checkTwitch(r.Conn, r.Msg.Channel, t, true) err := p.checkTwitch(r.Conn, r.Msg.Channel, t, true)
@ -182,7 +205,7 @@ func (p *TwitchPlugin) twitchUserStatus(r bot.Request) bool {
return true return true
} }
func (p *TwitchPlugin) resetTwitch(r bot.Request) bool { func (p *Twitch) resetTwitch(r bot.Request) bool {
p.c.Set("twitch.istpl", isStreamingTplFallback) p.c.Set("twitch.istpl", isStreamingTplFallback)
p.c.Set("twitch.nottpl", notStreamingTplFallback) p.c.Set("twitch.nottpl", notStreamingTplFallback)
p.c.Set("twitch.stoppedtpl", stoppedStreamingTplFallback) p.c.Set("twitch.stoppedtpl", stoppedStreamingTplFallback)
@ -190,7 +213,7 @@ func (p *TwitchPlugin) resetTwitch(r bot.Request) bool {
return true return true
} }
func (p *TwitchPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...any) bool { func (p *Twitch) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...any) bool {
msg := "You can set the templates for streams with\n" msg := "You can set the templates for streams with\n"
msg += fmt.Sprintf("twitch.istpl (default: %s)\n", isStreamingTplFallback) msg += fmt.Sprintf("twitch.istpl (default: %s)\n", isStreamingTplFallback)
msg += fmt.Sprintf("twitch.nottpl (default: %s)\n", notStreamingTplFallback) msg += fmt.Sprintf("twitch.nottpl (default: %s)\n", notStreamingTplFallback)
@ -201,7 +224,7 @@ func (p *TwitchPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message,
return true return true
} }
func (p *TwitchPlugin) twitchAuthLoop(c bot.Connector) { func (p *Twitch) twitchAuthLoop(c bot.Connector) {
frequency := p.c.GetInt("Twitch.AuthFreq", 60*60) frequency := p.c.GetInt("Twitch.AuthFreq", 60*60)
cid := p.c.Get("twitch.clientid", "") cid := p.c.Get("twitch.clientid", "")
secret := p.c.Get("twitch.secret", "") secret := p.c.Get("twitch.secret", "")
@ -226,7 +249,7 @@ func (p *TwitchPlugin) twitchAuthLoop(c bot.Connector) {
} }
} }
func (p *TwitchPlugin) twitchChannelLoop(c bot.Connector, channel string) { func (p *Twitch) twitchChannelLoop(c bot.Connector, channel string) {
frequency := p.c.GetInt("Twitch.Freq", 60) frequency := p.c.GetInt("Twitch.Freq", 60)
if p.c.Get("twitch.clientid", "") == "" || p.c.Get("twitch.secret", "") == "" { if p.c.Get("twitch.clientid", "") == "" || p.c.Get("twitch.secret", "") == "" {
log.Info().Msgf("Disabling twitch autochecking.") log.Info().Msgf("Disabling twitch autochecking.")
@ -276,7 +299,7 @@ errCase:
return []byte{}, resp.StatusCode, false return []byte{}, resp.StatusCode, false
} }
func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Twitcher, alwaysPrintStatus bool) error { func (p *Twitch) checkTwitch(c bot.Connector, channel string, twitcher *Twitcher, alwaysPrintStatus bool) error {
baseURL, err := url.Parse("https://api.twitch.tv/helix/streams") baseURL, err := url.Parse("https://api.twitch.tv/helix/streams")
if err != nil { if err != nil {
err := fmt.Errorf("error parsing twitch stream URL") err := fmt.Errorf("error parsing twitch stream URL")
@ -333,6 +356,7 @@ func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Tw
twitcher.URL(), twitcher.URL(),
} }
// This is a logic mess and needs to be rejiggered. I am not doing that today. (2022-09-18)
if alwaysPrintStatus { if alwaysPrintStatus {
if gameID == "" { if gameID == "" {
t, err := template.New("notStreaming").Parse(notStreamingTpl) t, err := template.New("notStreaming").Parse(notStreamingTpl)
@ -363,6 +387,13 @@ func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Tw
} }
t.Execute(&buf, info) t.Execute(&buf, info)
p.b.Send(c, bot.Message, channel, buf.String()) p.b.Send(c, bot.Message, channel, buf.String())
log.Debug().Msgf("Disconnecting bridge: %s -> %+v", twitcher.name, p.bridgeMap)
for threadID, ircCh := range p.bridgeMap {
if strings.HasSuffix(ircCh, twitcher.name) {
delete(p.bridgeMap, threadID)
p.b.Send(c, bot.Message, threadID, "Stopped tracking #"+twitcher.name)
}
}
} }
twitcher.gameID = "" twitcher.gameID = ""
} else { } else {
@ -375,13 +406,24 @@ func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Tw
} }
t.Execute(&buf, info) t.Execute(&buf, info)
p.b.Send(c, bot.Message, channel, buf.String()) p.b.Send(c, bot.Message, channel, buf.String())
// start the bridge here
msg := fmt.Sprintf("This post is tracking #%s\n<%s>", twitcher.name, twitcher.URL())
err = p.startBridgeMsg(
fmt.Sprintf("%s-%s-%s", info.Name, info.Game, time.Now().Format("2006-01-02-15:04")),
twitcher.name,
msg,
)
if err != nil {
log.Error().Err(err).Msgf("unable to start bridge")
return err
}
} }
twitcher.gameID = gameID twitcher.gameID = gameID
} }
return nil return nil
} }
func (p *TwitchPlugin) validateCredentials() error { func (p *Twitch) validateCredentials() error {
cid := p.c.Get("twitch.clientid", "") cid := p.c.Get("twitch.clientid", "")
token := p.c.Get("twitch.token", "") token := p.c.Get("twitch.token", "")
if token == "" { if token == "" {
@ -395,7 +437,7 @@ func (p *TwitchPlugin) validateCredentials() error {
return nil return nil
} }
func (p *TwitchPlugin) reAuthenticate() error { func (p *Twitch) reAuthenticate() error {
cid := p.c.Get("twitch.clientid", "") cid := p.c.Get("twitch.clientid", "")
secret := p.c.Get("twitch.secret", "") secret := p.c.Get("twitch.secret", "")
if cid == "" || secret == "" { if cid == "" || secret == "" {

View File

@ -37,7 +37,7 @@ func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
} }
} }
func makeTwitchPlugin(t *testing.T) (*TwitchPlugin, *bot.MockBot) { func makeTwitchPlugin(t *testing.T) (*Twitch, *bot.MockBot) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
mb.Config().Set("twitch.clientid", "fake") mb.Config().Set("twitch.clientid", "fake")