mirror of
https://github.com/Luzifer/discord-community.git
synced 2024-12-20 10:21:22 +00:00
Add live-role module
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
da6c0b35c2
commit
9026200947
5 changed files with 255 additions and 5 deletions
151
mod_liveRole.go
Normal file
151
mod_liveRole.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/Luzifer/go_helpers/v2/str"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/robfig/cron/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
/*
|
||||
* @module liverole
|
||||
* @module_desc Adds live-role to certain group of users if they are streaming on Twitch
|
||||
*/
|
||||
|
||||
func init() {
|
||||
RegisterModule("liverole", func() module { return &modLiveRole{} })
|
||||
}
|
||||
|
||||
type modLiveRole struct {
|
||||
attrs moduleAttributeStore
|
||||
discord *discordgo.Session
|
||||
}
|
||||
|
||||
func (m *modLiveRole) Initialize(crontab *cron.Cron, discord *discordgo.Session, attrs moduleAttributeStore) error {
|
||||
m.attrs = attrs
|
||||
m.discord = discord
|
||||
|
||||
if err := attrs.Expect(
|
||||
"role_streamers_live",
|
||||
"twitch_client_id",
|
||||
"twitch_client_secret",
|
||||
); err != nil {
|
||||
return errors.Wrap(err, "validating attributes")
|
||||
}
|
||||
|
||||
discord.AddHandler(m.handlePresenceUpdate)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m modLiveRole) addLiveStreamerRole(guildID, userID string, presentRoles []string) error {
|
||||
// @attr role_streamers_live required string "" Role ID to assign to live streamers
|
||||
roleID := m.attrs.MustString("role_streamers_live", nil)
|
||||
if str.StringInSlice(roleID, presentRoles) {
|
||||
// Already there fine!
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
m.discord.GuildMemberRoleAdd(guildID, userID, roleID),
|
||||
"adding role",
|
||||
)
|
||||
}
|
||||
|
||||
func (m modLiveRole) handlePresenceUpdate(d *discordgo.Session, p *discordgo.PresenceUpdate) {
|
||||
if p.User == nil {
|
||||
// The frick? Non-user presence?
|
||||
return
|
||||
}
|
||||
|
||||
logger := log.WithFields(log.Fields{
|
||||
"user": p.User.ID,
|
||||
})
|
||||
|
||||
member, err := d.GuildMember(p.GuildID, p.User.ID)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Unable to fetch member status for user")
|
||||
return
|
||||
}
|
||||
|
||||
// @attr role_streamers optional string "" Only take members with this role into account
|
||||
roleStreamer := m.attrs.MustString("role_streamers", ptrStringEmpty)
|
||||
if roleStreamer != "" && !str.StringInSlice(roleStreamer, member.Roles) {
|
||||
// User is not part of the streamer role
|
||||
return
|
||||
}
|
||||
|
||||
var exitFunc func(string, string, []string) error = m.removeLiveStreamerRole
|
||||
defer func() {
|
||||
if exitFunc != nil {
|
||||
if err := exitFunc(p.GuildID, p.User.ID, member.Roles); err != nil {
|
||||
logger.WithError(err).Error("Unable to update live-streamer-role")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var activity *discordgo.Activity
|
||||
|
||||
for _, a := range p.Activities {
|
||||
if a.Type == discordgo.ActivityTypeStreaming {
|
||||
activity = a
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if activity == nil {
|
||||
// No streaming activity: Remove role
|
||||
exitFunc = m.removeLiveStreamerRole
|
||||
return
|
||||
}
|
||||
|
||||
u, err := url.Parse(activity.URL)
|
||||
if err != nil {
|
||||
logger.WithError(err).WithField("url", activity.URL).Warning("Unable to parse activity URL")
|
||||
exitFunc = m.removeLiveStreamerRole
|
||||
return
|
||||
}
|
||||
|
||||
if u.Host != "twitch.tv" {
|
||||
logger.WithError(err).WithField("url", activity.URL).Warning("Activity is not on Twitch")
|
||||
exitFunc = m.removeLiveStreamerRole
|
||||
return
|
||||
}
|
||||
|
||||
twitch := newTwitchAdapter(
|
||||
// @attr twitch_client_id required string "" Twitch client ID the token was issued for
|
||||
m.attrs.MustString("twitch_client_id", nil),
|
||||
// @attr twitch_client_secret required string "" Secret for the Twitch app identified with twitch_client_id
|
||||
m.attrs.MustString("twitch_client_secret", nil),
|
||||
"", // No User-Token used
|
||||
)
|
||||
|
||||
streams, err := twitch.GetStreamsForUser(context.Background(), strings.TrimLeft(u.Path, "/"))
|
||||
if err != nil {
|
||||
logger.WithError(err).WithField("user", strings.TrimLeft(u.Path, "/")).Warning("Unable to fetch streams for user")
|
||||
exitFunc = m.removeLiveStreamerRole
|
||||
return
|
||||
}
|
||||
|
||||
if len(streams.Data) > 0 {
|
||||
exitFunc = m.addLiveStreamerRole
|
||||
}
|
||||
}
|
||||
|
||||
func (m modLiveRole) removeLiveStreamerRole(guildID, userID string, presentRoles []string) error {
|
||||
roleID := m.attrs.MustString("role_streamers_live", nil)
|
||||
if !str.StringInSlice(roleID, presentRoles) {
|
||||
// Not there: fine!
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Wrap(
|
||||
m.discord.GuildMemberRoleRemove(guildID, userID, roleID),
|
||||
"adding role",
|
||||
)
|
||||
}
|
|
@ -54,6 +54,7 @@ func (m modPresence) cronUpdatePresence() {
|
|||
twitch := newTwitchAdapter(
|
||||
// @attr twitch_client_id required string "" Twitch client ID the token was issued for
|
||||
m.attrs.MustString("twitch_client_id", nil),
|
||||
"", // No client secret used
|
||||
// @attr twitch_token required string "" Token for the user the `twitch_channel_id` belongs to
|
||||
m.attrs.MustString("twitch_token", nil),
|
||||
)
|
||||
|
|
|
@ -57,6 +57,7 @@ func (m modStreamSchedule) cronUpdateSchedule() {
|
|||
twitch := newTwitchAdapter(
|
||||
// @attr twitch_client_id required string "" Twitch client ID the token was issued for
|
||||
m.attrs.MustString("twitch_client_id", nil),
|
||||
"", // No Client Secret used
|
||||
// @attr twitch_token required string "" Token for the user the `twitch_channel_id` belongs to
|
||||
m.attrs.MustString("twitch_token", nil),
|
||||
)
|
||||
|
|
96
twitch.go
96
twitch.go
|
@ -21,8 +21,31 @@ const (
|
|||
|
||||
type (
|
||||
twitchAdapter struct {
|
||||
clientID string
|
||||
token string
|
||||
clientID string
|
||||
clientSecret string
|
||||
token string
|
||||
}
|
||||
|
||||
twitchStreamListing struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
UserLogin string `json:"user_login"`
|
||||
UserName string `json:"user_name"`
|
||||
GameID string `json:"game_id"`
|
||||
GameName string `json:"game_name"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
ViewerCount int64 `json:"viewer_count"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
Language string `json:"language"`
|
||||
ThumbnailURL string `json:"thumbnail_url"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
IsMature bool `json:"is_mature"`
|
||||
} `json:"data"`
|
||||
Pagination struct {
|
||||
Cursor string `json:"cursor"`
|
||||
} `json:"pagination"`
|
||||
}
|
||||
|
||||
twitchStreamSchedule struct {
|
||||
|
@ -53,10 +76,11 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
func newTwitchAdapter(clientID, token string) *twitchAdapter {
|
||||
func newTwitchAdapter(clientID, clientSecret, token string) *twitchAdapter {
|
||||
return &twitchAdapter{
|
||||
clientID: clientID,
|
||||
token: token,
|
||||
clientID: clientID,
|
||||
clientSecret: clientSecret,
|
||||
token: token,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,6 +103,60 @@ func (t twitchAdapter) GetChannelStreamSchedule(ctx context.Context, broadcaster
|
|||
})
|
||||
}
|
||||
|
||||
func (t twitchAdapter) GetStreamsForUser(ctx context.Context, userName string) (*twitchStreamListing, error) {
|
||||
out := &twitchStreamListing{}
|
||||
|
||||
params := make(url.Values)
|
||||
params.Set("user_login", strings.ToLower(userName))
|
||||
|
||||
return out, backoff.NewBackoff().
|
||||
WithMaxIterations(twitchAPIRequestLimit).
|
||||
Retry(func() error {
|
||||
return errors.Wrap(
|
||||
t.request(ctx, http.MethodGet, "/helix/streams", params, nil, out),
|
||||
"fetching streams",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (t twitchAdapter) getAppAccessToken(ctx context.Context) (string, error) {
|
||||
var rData struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Scope []interface{} `json:"scope"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
params := make(url.Values)
|
||||
params.Set("client_id", "<your client ID>")
|
||||
params.Set("client_secret", "<your client secret>")
|
||||
params.Set("grant_type", "client_credentials")
|
||||
|
||||
u, _ := url.Parse("https://id.twitch.tv/oauth2/token")
|
||||
u.RawQuery = params.Encode()
|
||||
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "fetching response")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "unexpected status %d and cannot read body", resp.StatusCode)
|
||||
}
|
||||
return "", errors.Errorf("unexpected status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
|
||||
return rData.AccessToken, errors.Wrap(
|
||||
json.NewDecoder(resp.Body).Decode(&rData),
|
||||
"decoding response",
|
||||
)
|
||||
}
|
||||
|
||||
func (t twitchAdapter) request(ctx context.Context, method, path string, params url.Values, body io.Reader, output interface{}) error {
|
||||
ctxTimed, cancel := context.WithTimeout(ctx, twitchAPIRequestTimeout)
|
||||
defer cancel()
|
||||
|
@ -96,6 +174,14 @@ func (t twitchAdapter) request(ctx context.Context, method, path string, params
|
|||
req.Header.Set("Authorization", strings.Join([]string{"Bearer", t.token}, " "))
|
||||
req.Header.Set("Client-Id", t.clientID)
|
||||
|
||||
if t.token == "" {
|
||||
accessToken, err := t.getAppAccessToken(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "fetching app-access-token")
|
||||
}
|
||||
req.Header.Set("Authorization", strings.Join([]string{"Bearer", accessToken}, " "))
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "fetching response")
|
||||
|
|
11
wiki/Home.md
11
wiki/Home.md
|
@ -16,6 +16,17 @@ module_configs:
|
|||
|
||||
# Modules
|
||||
|
||||
## Type: `liverole`
|
||||
|
||||
Adds live-role to certain group of users if they are streaming on Twitch
|
||||
|
||||
| Attribute | Req. | Type | Default Value | Description |
|
||||
| --------- | :--: | ---- | ------------- | ----------- |
|
||||
| `role_streamers_live` | ✅ | string | | Role ID to assign to live streamers |
|
||||
| `twitch_client_id` | ✅ | string | | Twitch client ID the token was issued for |
|
||||
| `twitch_client_secret` | ✅ | string | | Secret for the Twitch app identified with twitch_client_id |
|
||||
| `role_streamers` | | string | | Only take members with this role into account |
|
||||
|
||||
## Type: `presence`
|
||||
|
||||
Updates the presence status of the bot to display the next stream
|
||||
|
|
Loading…
Reference in a new issue