mirror of
https://github.com/Luzifer/discord-community.git
synced 2024-11-09 15:40:03 +00:00
Add live announcements
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
5524101e6c
commit
5df755d13b
5 changed files with 283 additions and 0 deletions
|
@ -110,3 +110,30 @@ func (m moduleAttributeStore) String(name string) (string, error) {
|
|||
|
||||
return "", errValueMismatch
|
||||
}
|
||||
|
||||
func (m moduleAttributeStore) StringSlice(name string) ([]string, error) {
|
||||
v, ok := m[name]
|
||||
if !ok {
|
||||
return nil, errValueNotSet
|
||||
}
|
||||
|
||||
switch v.(type) {
|
||||
case []string:
|
||||
return v.([]string), nil
|
||||
|
||||
case []interface{}:
|
||||
var out []string
|
||||
|
||||
for _, iv := range v.([]interface{}) {
|
||||
sv, ok := iv.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("value in slice was not string")
|
||||
}
|
||||
out = append(out, sv)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
return nil, errValueMismatch
|
||||
}
|
||||
|
|
205
mod_livePosting.go
Normal file
205
mod_livePosting.go
Normal file
|
@ -0,0 +1,205 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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 liveposting
|
||||
* @module_desc Announces stream live status based on Discord streaming status
|
||||
*/
|
||||
|
||||
func init() {
|
||||
RegisterModule("liveposting", func() module { return &modLivePosting{} })
|
||||
}
|
||||
|
||||
type modLivePosting struct {
|
||||
attrs moduleAttributeStore
|
||||
discord *discordgo.Session
|
||||
}
|
||||
|
||||
func (m *modLivePosting) Initialize(crontab *cron.Cron, discord *discordgo.Session, attrs moduleAttributeStore) error {
|
||||
m.attrs = attrs
|
||||
m.discord = discord
|
||||
|
||||
if err := attrs.Expect(
|
||||
"discord_channel_id",
|
||||
"post_text",
|
||||
"whitelisted_role",
|
||||
"twitch_client_id",
|
||||
"twitch_client_secret",
|
||||
); err != nil {
|
||||
return errors.Wrap(err, "validating attributes")
|
||||
}
|
||||
|
||||
discord.AddHandler(m.handlePresenceUpdate)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m modLivePosting) 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 whitelisted_role optional string "" Only post for members of this role
|
||||
whitelistedRole := m.attrs.MustString("whitelisted_role", ptrStringEmpty)
|
||||
if whitelistedRole != "" && !str.StringInSlice(whitelistedRole, member.Roles) {
|
||||
// User is not allowed for this config
|
||||
return
|
||||
}
|
||||
|
||||
var activity *discordgo.Activity
|
||||
|
||||
for _, a := range p.Activities {
|
||||
if a.Type == discordgo.ActivityTypeStreaming {
|
||||
activity = a
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if activity == nil {
|
||||
// No streaming activity: Do nothing
|
||||
return
|
||||
}
|
||||
|
||||
u, err := url.Parse(activity.URL)
|
||||
if err != nil {
|
||||
logger.WithError(err).WithField("url", activity.URL).Warning("Unable to parse activity URL")
|
||||
return
|
||||
}
|
||||
|
||||
if u.Host != "www.twitch.tv" {
|
||||
logger.WithError(err).WithField("url", activity.URL).Debug("Activity is not on Twitch")
|
||||
return
|
||||
}
|
||||
|
||||
twitchUsername := strings.TrimLeft(u.Path, "/")
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
users, err := twitch.GetUserByUsername(context.Background(), twitchUsername)
|
||||
if err != nil {
|
||||
logger.WithError(err).WithField("user", twitchUsername).Warning("Unable to fetch details for user")
|
||||
return
|
||||
}
|
||||
|
||||
if l := len(users.Data); l != 1 {
|
||||
logger.WithError(err).WithField("url", activity.URL).Warning("Unable to fetch user for login")
|
||||
return
|
||||
}
|
||||
|
||||
streams, err := twitch.GetStreamsForUser(context.Background(), twitchUsername)
|
||||
if err != nil {
|
||||
logger.WithError(err).WithField("user", twitchUsername).Debug("Unable to fetch streams for user")
|
||||
return
|
||||
}
|
||||
|
||||
if l := len(streams.Data); l != 1 {
|
||||
logger.WithError(err).WithField("url", activity.URL).Debug("Unable to fetch streams for login")
|
||||
return
|
||||
}
|
||||
|
||||
// @attr stream_freshness optional duration "5m" How long after stream start to post shoutout
|
||||
ignoreTime := m.attrs.MustDuration("stream_freshness", ptrDuration(5*time.Minute))
|
||||
if streams.Data[0].StartedAt.Add(ignoreTime).Before(time.Now()) {
|
||||
// Stream is too old, don't annoounce
|
||||
return
|
||||
}
|
||||
|
||||
if err = m.sendLivePost(
|
||||
users.Data[0].Login,
|
||||
users.Data[0].DisplayName,
|
||||
streams.Data[0].Title,
|
||||
streams.Data[0].GameName,
|
||||
streams.Data[0].ThumbnailURL,
|
||||
users.Data[0].ProfileImageURL,
|
||||
); err != nil {
|
||||
logger.WithError(err).WithField("url", activity.URL).Error("Unable to send post")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (m modLivePosting) sendLivePost(username, displayName, title, game, previewImage, profileImage string) error {
|
||||
postText := strings.NewReplacer(
|
||||
"${displayname}", displayName,
|
||||
"${username}", username,
|
||||
).Replace(
|
||||
// @attr post_text required string "" Message to post to channel use `${displayname}` and `${username}` as placeholders
|
||||
m.attrs.MustString("post_text", nil),
|
||||
)
|
||||
|
||||
// @attr discord_channel_id required string "" ID of the Discord channel to post the message to
|
||||
msgs, err := m.discord.ChannelMessages(m.attrs.MustString("discord_channel_id", nil), 100, "", "", "")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "fetching previous messages")
|
||||
}
|
||||
|
||||
ignoreTime := m.attrs.MustDuration("stream_freshness", ptrDuration(5*time.Minute))
|
||||
for _, msg := range msgs {
|
||||
mt, err := msg.Timestamp.Parse()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "parsing message timestamp")
|
||||
}
|
||||
if msg.Content == postText && time.Since(mt) < ignoreTime {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
msgEmbed := &discordgo.MessageEmbed{
|
||||
Author: &discordgo.MessageEmbedAuthor{
|
||||
Name: displayName,
|
||||
IconURL: profileImage,
|
||||
},
|
||||
Color: 0x6441a5,
|
||||
Fields: []*discordgo.MessageEmbedField{
|
||||
{Name: "Game", Value: game},
|
||||
},
|
||||
Image: &discordgo.MessageEmbedImage{
|
||||
URL: strings.NewReplacer("{width}", "320", "{height}", "180").Replace(previewImage),
|
||||
Width: 320,
|
||||
Height: 180,
|
||||
},
|
||||
Thumbnail: &discordgo.MessageEmbedThumbnail{
|
||||
URL: profileImage,
|
||||
Width: 300,
|
||||
Height: 300,
|
||||
},
|
||||
Title: title,
|
||||
Type: discordgo.EmbedTypeRich,
|
||||
URL: strings.Join([]string{"https://www.twitch.tv", username}, "/"),
|
||||
}
|
||||
|
||||
_, err = m.discord.ChannelMessageSendComplex(m.attrs.MustString("discord_channel_id", nil), &discordgo.MessageSend{
|
||||
Content: postText,
|
||||
Embed: msgEmbed,
|
||||
})
|
||||
|
||||
return errors.Wrap(err, "sending message")
|
||||
}
|
|
@ -46,6 +46,9 @@ func (m *modLiveRole) Initialize(crontab *cron.Cron, discord *discordgo.Session,
|
|||
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 roleID == "" {
|
||||
return errors.New("empty live-role-id")
|
||||
}
|
||||
if str.StringInSlice(roleID, presentRoles) {
|
||||
// Already there fine!
|
||||
return nil
|
||||
|
@ -139,6 +142,9 @@ func (m modLiveRole) handlePresenceUpdate(d *discordgo.Session, p *discordgo.Pre
|
|||
|
||||
func (m modLiveRole) removeLiveStreamerRole(guildID, userID string, presentRoles []string) error {
|
||||
roleID := m.attrs.MustString("role_streamers_live", nil)
|
||||
if roleID == "" {
|
||||
return errors.New("empty live-role-id")
|
||||
}
|
||||
if !str.StringInSlice(roleID, presentRoles) {
|
||||
// Not there: fine!
|
||||
return nil
|
||||
|
|
32
twitch.go
32
twitch.go
|
@ -74,6 +74,22 @@ type (
|
|||
Cursor string `json:"cursor"`
|
||||
} `json:"pagination"`
|
||||
}
|
||||
|
||||
twitchUserListing struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
Login string `json:"login"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Type string `json:"type"`
|
||||
BroadcasterType string `json:"broadcaster_type"`
|
||||
Description string `json:"description"`
|
||||
ProfileImageURL string `json:"profile_image_url"`
|
||||
OfflineImageURL string `json:"offline_image_url"`
|
||||
ViewCount int64 `json:"view_count"`
|
||||
Email string `json:"email"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
} `json:"data"`
|
||||
}
|
||||
)
|
||||
|
||||
func newTwitchAdapter(clientID, clientSecret, token string) *twitchAdapter {
|
||||
|
@ -119,6 +135,22 @@ func (t twitchAdapter) GetStreamsForUser(ctx context.Context, userName string) (
|
|||
})
|
||||
}
|
||||
|
||||
func (t twitchAdapter) GetUserByUsername(ctx context.Context, userName string) (*twitchUserListing, error) {
|
||||
out := &twitchUserListing{}
|
||||
|
||||
params := make(url.Values)
|
||||
params.Set("login", strings.ToLower(userName))
|
||||
|
||||
return out, backoff.NewBackoff().
|
||||
WithMaxIterations(twitchAPIRequestLimit).
|
||||
Retry(func() error {
|
||||
return errors.Wrap(
|
||||
t.request(ctx, http.MethodGet, "/helix/users", params, nil, out),
|
||||
"fetching user",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (t twitchAdapter) getAppAccessToken(ctx context.Context) (string, error) {
|
||||
var rData struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
|
|
13
wiki/Home.md
13
wiki/Home.md
|
@ -16,6 +16,19 @@ module_configs:
|
|||
|
||||
# Modules
|
||||
|
||||
## Type: `liveposting`
|
||||
|
||||
Announces stream live status based on Discord streaming status
|
||||
|
||||
| Attribute | Req. | Type | Default Value | Description |
|
||||
| --------- | :--: | ---- | ------------- | ----------- |
|
||||
| `discord_channel_id` | ✅ | string | | ID of the Discord channel to post the message to |
|
||||
| `post_text` | ✅ | string | | Message to post to channel use `${displayname}` and `${username}` as placeholders |
|
||||
| `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 |
|
||||
| `stream_freshness` | | duration | `5m` | How long after stream start to post shoutout |
|
||||
| `whitelisted_role` | | string | | Only post for members of this role |
|
||||
|
||||
## Type: `liverole`
|
||||
|
||||
Adds live-role to certain group of users if they are streaming on Twitch
|
||||
|
|
Loading…
Reference in a new issue