mirror of
https://github.com/Luzifer/discord-community.git
synced 2024-12-20 18:31:23 +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(
|
twitch := newTwitchAdapter(
|
||||||
// @attr twitch_client_id required string "" Twitch client ID the token was issued for
|
// @attr twitch_client_id required string "" Twitch client ID the token was issued for
|
||||||
m.attrs.MustString("twitch_client_id", nil),
|
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
|
// @attr twitch_token required string "" Token for the user the `twitch_channel_id` belongs to
|
||||||
m.attrs.MustString("twitch_token", nil),
|
m.attrs.MustString("twitch_token", nil),
|
||||||
)
|
)
|
||||||
|
|
|
@ -57,6 +57,7 @@ func (m modStreamSchedule) cronUpdateSchedule() {
|
||||||
twitch := newTwitchAdapter(
|
twitch := newTwitchAdapter(
|
||||||
// @attr twitch_client_id required string "" Twitch client ID the token was issued for
|
// @attr twitch_client_id required string "" Twitch client ID the token was issued for
|
||||||
m.attrs.MustString("twitch_client_id", nil),
|
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
|
// @attr twitch_token required string "" Token for the user the `twitch_channel_id` belongs to
|
||||||
m.attrs.MustString("twitch_token", nil),
|
m.attrs.MustString("twitch_token", nil),
|
||||||
)
|
)
|
||||||
|
|
96
twitch.go
96
twitch.go
|
@ -21,8 +21,31 @@ const (
|
||||||
|
|
||||||
type (
|
type (
|
||||||
twitchAdapter struct {
|
twitchAdapter struct {
|
||||||
clientID string
|
clientID string
|
||||||
token 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 {
|
twitchStreamSchedule struct {
|
||||||
|
@ -53,10 +76,11 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTwitchAdapter(clientID, token string) *twitchAdapter {
|
func newTwitchAdapter(clientID, clientSecret, token string) *twitchAdapter {
|
||||||
return &twitchAdapter{
|
return &twitchAdapter{
|
||||||
clientID: clientID,
|
clientID: clientID,
|
||||||
token: token,
|
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 {
|
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)
|
ctxTimed, cancel := context.WithTimeout(ctx, twitchAPIRequestTimeout)
|
||||||
defer cancel()
|
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("Authorization", strings.Join([]string{"Bearer", t.token}, " "))
|
||||||
req.Header.Set("Client-Id", t.clientID)
|
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)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "fetching response")
|
return errors.Wrap(err, "fetching response")
|
||||||
|
|
11
wiki/Home.md
11
wiki/Home.md
|
@ -16,6 +16,17 @@ module_configs:
|
||||||
|
|
||||||
# Modules
|
# 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`
|
## Type: `presence`
|
||||||
|
|
||||||
Updates the presence status of the bot to display the next stream
|
Updates the presence status of the bot to display the next stream
|
||||||
|
|
Loading…
Reference in a new issue