2021-07-12 16:04:31 +00:00
package main
import (
2022-12-06 17:07:20 +00:00
"bytes"
2021-07-12 16:04:31 +00:00
"context"
"fmt"
2021-07-12 17:08:10 +00:00
"strings"
2022-12-06 17:07:20 +00:00
"text/template"
2021-07-12 16:04:31 +00:00
"time"
2022-12-06 17:07:20 +00:00
"github.com/Masterminds/sprig/v3"
2021-07-12 16:04:31 +00:00
"github.com/bwmarrin/discordgo"
"github.com/pkg/errors"
2021-07-22 22:54:11 +00:00
"github.com/robfig/cron/v3"
2021-07-12 16:04:31 +00:00
log "github.com/sirupsen/logrus"
)
2021-07-25 11:43:41 +00:00
/ *
* @ module schedule
* @ module_desc Posts stream schedule derived from Twitch schedule as embed in Discord channel
* /
2021-07-31 18:58:11 +00:00
const (
2021-08-07 15:00:49 +00:00
streamScheduleDefaultColor = 0x2ECC71
2021-07-31 18:58:11 +00:00
)
2021-07-25 11:43:41 +00:00
var (
2021-07-31 18:58:11 +00:00
defaultStreamScheduleEntries = ptrInt64 ( 5 ) //nolint: gomnd // This is already the "constant"
defaultStreamSchedulePastTime = ptrDuration ( 15 * time . Minute ) //nolint: gomnd // This is already the "constant"
2021-07-12 16:04:31 +00:00
)
2021-07-22 22:54:11 +00:00
func init ( ) {
RegisterModule ( "schedule" , func ( ) module { return & modStreamSchedule { } } )
2021-07-12 16:04:31 +00:00
}
2021-07-29 19:13:19 +00:00
type modStreamSchedule struct {
attrs moduleAttributeStore
discord * discordgo . Session
2021-08-07 14:16:14 +00:00
id string
2021-07-29 19:13:19 +00:00
}
2021-07-22 22:54:11 +00:00
2021-08-07 14:16:14 +00:00
func ( m modStreamSchedule ) ID ( ) string { return m . id }
func ( m * modStreamSchedule ) Initialize ( id string , crontab * cron . Cron , discord * discordgo . Session , attrs moduleAttributeStore ) error {
2021-07-22 22:54:11 +00:00
m . attrs = attrs
m . discord = discord
2021-08-07 14:16:14 +00:00
m . id = id
2021-07-22 22:54:11 +00:00
2021-07-25 11:43:41 +00:00
if err := attrs . Expect (
"discord_channel_id" ,
"twitch_channel_id" ,
"twitch_client_id" ,
2021-08-20 23:25:19 +00:00
"twitch_client_secret" ,
2021-07-25 11:43:41 +00:00
) ; err != nil {
return errors . Wrap ( err , "validating attributes" )
}
// @attr cron optional string "*/10 * * * *" When to execute the schedule transfer
2021-07-22 22:54:11 +00:00
if _ , err := crontab . AddFunc ( attrs . MustString ( "cron" , ptrString ( "*/10 * * * *" ) ) , m . cronUpdateSchedule ) ; err != nil {
2021-07-25 11:43:41 +00:00
return errors . Wrap ( err , "adding cron function" )
2021-07-12 16:04:31 +00:00
}
2021-07-22 22:54:11 +00:00
return nil
2021-07-12 16:04:31 +00:00
}
2021-08-07 13:24:11 +00:00
func ( m modStreamSchedule ) Setup ( ) error { return nil }
2022-02-11 21:45:36 +00:00
//nolint:funlen,gocyclo // Seeing no sense to split for 5 lines
2021-07-22 22:54:11 +00:00
func ( m modStreamSchedule ) cronUpdateSchedule ( ) {
2021-07-29 19:13:19 +00:00
twitch := newTwitchAdapter (
// @attr twitch_client_id required string "" Twitch client ID the token was issued for
m . attrs . MustString ( "twitch_client_id" , nil ) ,
2021-08-20 23:25:19 +00:00
// @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
2021-07-29 19:13:19 +00:00
)
2021-07-12 16:04:31 +00:00
2021-07-29 19:13:19 +00:00
data , err := twitch . GetChannelStreamSchedule (
context . Background ( ) ,
2021-07-25 11:43:41 +00:00
// @attr twitch_channel_id required string "" ID (not name) of the channel to fetch the schedule from
2021-07-29 19:13:19 +00:00
m . attrs . MustString ( "twitch_channel_id" , nil ) ,
2021-07-25 11:43:41 +00:00
// @attr schedule_past_time optional duration "15m" How long in the past should the schedule contain an entry
2021-07-29 19:13:19 +00:00
ptrTime ( time . Now ( ) . Add ( - m . attrs . MustDuration ( "schedule_past_time" , defaultStreamSchedulePastTime ) ) ) ,
)
if err != nil {
2021-07-12 16:04:31 +00:00
log . WithError ( err ) . Error ( "Unable to fetch stream schedule" )
return
}
2021-08-07 15:00:49 +00:00
// @attr discord_channel_id required string "" ID of the Discord channel to post the message to
channelID := m . attrs . MustString ( "discord_channel_id" , nil )
2022-12-06 17:07:20 +00:00
var msgEmbed * discordgo . MessageEmbed
// @attr embed_title optional string "" Title of the embed (embed will not be added when title is missing)
if m . attrs . MustString ( "embed_title" , ptrStringEmpty ) != "" {
msgEmbed = m . assembleEmbed ( data )
}
var contentString string
// @attr content optional string "" Message content to post above the embed - Allows Go templating, make sure to proper escape the template strings. See [here](https://github.com/Luzifer/discord-community/blob/5f004fdab066f16580f41076a4e6d8668fe743c9/twitch.go#L53-L71) for available data object.
if m . attrs . MustString ( "content" , ptrStringEmpty ) != "" {
if contentString , err = m . executeContentTemplate ( data ) ; err != nil {
log . WithError ( err ) . Error ( "executing stream schedule template" )
return
}
}
var managedMsg * discordgo . Message
if err = store . ReadWithLock ( m . id , func ( a moduleAttributeStore ) error {
mid , err := a . String ( "message_id" )
if err == errValueNotSet {
return nil
}
managedMsg , err = m . discord . ChannelMessage ( channelID , mid )
return errors . Wrap ( err , "fetching managed message" )
} ) ; err != nil {
log . WithError ( err ) . Error ( "Unable to fetch managed message for stream schedule" )
return
}
if managedMsg != nil {
2022-12-06 18:00:57 +00:00
var oldEmbed * discordgo . MessageEmbed
if len ( managedMsg . Embeds ) > 0 {
oldEmbed = managedMsg . Embeds [ 0 ]
}
2022-12-06 17:07:20 +00:00
2022-12-06 17:22:54 +00:00
if isDiscordMessageEmbedEqual ( oldEmbed , msgEmbed ) && strings . TrimSpace ( managedMsg . Content ) == strings . TrimSpace ( contentString ) {
2022-12-06 17:07:20 +00:00
log . Debug ( "Stream Schedule is up-to-date" )
return
}
_ , err = m . discord . ChannelMessageEditComplex ( & discordgo . MessageEdit {
Content : & contentString ,
Embed : msgEmbed ,
ID : managedMsg . ID ,
Channel : channelID ,
} )
} else {
managedMsg , err = m . discord . ChannelMessageSendComplex ( channelID , & discordgo . MessageSend {
Content : contentString ,
Embed : msgEmbed ,
} )
}
if err != nil {
log . WithError ( err ) . Error ( "Unable to announce streamplan" )
return
}
if err = store . Set ( m . id , "message_id" , managedMsg . ID ) ; err != nil {
log . WithError ( err ) . Error ( "Unable to store managed message id" )
return
}
log . Info ( "Updated Stream Schedule" )
}
func ( m modStreamSchedule ) assembleEmbed ( data * twitchStreamSchedule ) * discordgo . MessageEmbed {
2021-07-12 16:04:31 +00:00
msgEmbed := & discordgo . MessageEmbed {
2021-08-07 21:45:34 +00:00
// @attr embed_color optional int64 "0x2ECC71" Integer / HEX representation of the color for the embed
2021-07-31 18:58:11 +00:00
Color : int ( m . attrs . MustInt64 ( "embed_color" , ptrInt64 ( streamScheduleDefaultColor ) ) ) ,
2021-07-25 11:43:41 +00:00
// @attr embed_description optional string "" Description for the embed block
2021-08-07 20:14:01 +00:00
Description : strings . TrimSpace ( m . attrs . MustString ( "embed_description" , ptrStringEmpty ) ) ,
2021-07-12 16:04:31 +00:00
Fields : [ ] * discordgo . MessageEmbedField { } ,
2021-08-07 20:14:01 +00:00
Timestamp : time . Now ( ) . Format ( time . RFC3339 ) ,
2022-12-06 17:07:20 +00:00
Title : m . attrs . MustString ( "embed_title" , nil ) ,
Type : discordgo . EmbedTypeRich ,
2021-08-07 20:14:01 +00:00
}
if m . attrs . MustString ( "embed_thumbnail_url" , ptrStringEmpty ) != "" {
msgEmbed . Thumbnail = & discordgo . MessageEmbedThumbnail {
2021-08-07 17:26:12 +00:00
// @attr embed_thumbnail_url optional string "" Publically hosted image URL to use as thumbnail
2021-07-25 11:43:41 +00:00
URL : m . attrs . MustString ( "embed_thumbnail_url" , ptrStringEmpty ) ,
// @attr embed_thumbnail_width optional int64 "" Width of the thumbnail
Width : int ( m . attrs . MustInt64 ( "embed_thumbnail_width" , ptrInt64Zero ) ) ,
// @attr embed_thumbnail_height optional int64 "" Height of the thumbnail
Height : int ( m . attrs . MustInt64 ( "embed_thumbnail_height" , ptrInt64Zero ) ) ,
2021-08-07 20:14:01 +00:00
}
2021-07-12 16:04:31 +00:00
}
for _ , seg := range data . Data . Segments {
title := seg . Title
2022-02-11 21:41:59 +00:00
switch {
case seg . StartTime == nil || seg . CanceledUntil != nil :
// No start-time: We skip this entry
continue
case seg . Category != nil && seg . Title == "" :
// No title but category set: use category as title
title = seg . Category . Name
case seg . Category != nil && ! strings . Contains ( seg . Title , seg . Category . Name ) :
// Title and category set but category not part of title: Add it in braces
2021-07-12 16:04:31 +00:00
title = fmt . Sprintf ( "%s (%s)" , seg . Title , seg . Category . Name )
2022-02-11 21:41:59 +00:00
case seg . Category == nil && seg . Title == "" :
// Unnamed stream without category: don't display empty field
2021-07-12 17:08:10 +00:00
continue
}
2021-07-12 16:04:31 +00:00
msgEmbed . Fields = append ( msgEmbed . Fields , & discordgo . MessageEmbedField {
2021-08-27 21:19:44 +00:00
Name : m . formatTime ( * seg . StartTime ) ,
2022-03-04 18:43:41 +00:00
Value : strings . TrimSpace ( title ) ,
2021-07-12 16:04:31 +00:00
Inline : false ,
} )
2021-07-17 12:11:13 +00:00
2021-07-25 11:43:41 +00:00
// @attr schedule_entries optional int64 "5" How many schedule entries to add to the embed as fields
if len ( msgEmbed . Fields ) == int ( m . attrs . MustInt64 ( "schedule_entries" , defaultStreamScheduleEntries ) ) {
2021-07-17 12:11:13 +00:00
break
}
2021-07-12 16:04:31 +00:00
}
2022-12-06 17:07:20 +00:00
return msgEmbed
}
2021-07-12 17:08:10 +00:00
2022-12-06 17:07:20 +00:00
func ( m modStreamSchedule ) executeContentTemplate ( data * twitchStreamSchedule ) ( string , error ) {
2022-12-06 17:45:50 +00:00
fns := sprig . FuncMap ( )
2022-12-06 17:56:21 +00:00
fns [ "formatTime" ] = m . formatTime
2022-12-06 17:45:50 +00:00
2022-12-06 17:07:20 +00:00
tpl , err := template . New ( "streamschedule" ) .
2022-12-06 17:45:50 +00:00
Funcs ( fns ) .
2022-12-06 17:07:20 +00:00
Parse ( m . attrs . MustString ( "content" , ptrStringEmpty ) )
2021-07-12 16:04:31 +00:00
if err != nil {
2022-12-06 17:07:20 +00:00
return "" , errors . Wrap ( err , "parsing template" )
2021-07-12 16:04:31 +00:00
}
2021-07-12 17:08:10 +00:00
2022-12-06 17:07:20 +00:00
buf := new ( bytes . Buffer )
err = tpl . Execute ( buf , data . Data )
return buf . String ( ) , errors . Wrap ( err , "executing template" )
2021-07-12 17:08:10 +00:00
}
2021-08-27 21:19:44 +00:00
func ( m modStreamSchedule ) formatTime ( t time . Time ) string {
2021-08-27 21:26:29 +00:00
// @attr timezone optional string "UTC" Timezone to display the times in (e.g. `Europe/Berlin`)
2021-08-27 21:19:44 +00:00
tz , err := time . LoadLocation ( m . attrs . MustString ( "timezone" , ptrString ( "UTC" ) ) )
2021-07-12 17:08:10 +00:00
if err != nil {
2021-08-27 21:19:44 +00:00
log . WithError ( err ) . Fatal ( "Unable to load timezone" )
2021-07-12 17:08:10 +00:00
}
2021-08-27 21:19:44 +00:00
return localeStrftime (
t . In ( tz ) ,
2021-08-27 21:26:29 +00:00
// @attr time_format optional string "%b %d, %Y %I:%M %p" Time format in [limited strftime format](https://github.com/Luzifer/discord-community/blob/master/strftime.go) to use (e.g. `%a. %d.%m. %H:%M Uhr`)
2021-08-27 21:19:44 +00:00
m . attrs . MustString ( "time_format" , ptrString ( "%b %d, %Y %I:%M %p" ) ) ,
// @attr locale optional string "en_US" Locale to translate the date to ([supported locales](https://github.com/goodsign/monday/blob/24c0b92f25dca51152defe82cefc7f7fc1c92009/locale.go#L9-L49))
m . attrs . MustString ( "locale" , ptrString ( "en_US" ) ) ,
)
2021-07-12 17:08:10 +00:00
}