A pooled implementation of Twitch PubSub
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

266 lines
9.3 KiB

package twpubsub
import (
"encoding/json"
"raccatta.cc/twpubsub/internal/errors"
)
// ErrUnsupportedTopic is returned for topics that can't be decoded, because
// their format is unknown.
var ErrUnsupportedTopic = errors.New("Unsupported topic")
type (
// "community-points-user-v1.$userid"
// CommunityPointsChannel is used for "channel-points-channel-v1.$userid"
CommunityPointsChannel struct {
Type string `json:"type"`
Data json.RawMessage `json:"data"`
}
CommunityPointsRewardRedeemed struct {
Timestamp string `json:"timestamp"`
Redemption CommunityPointsRedemption `json:"redemption"`
UpdatedReward UpdatedReward `json:"updated_reward"`
}
CommunityPointsRedemption struct {
ID string `json:"id"`
ChannelID string `json:"channel_id"`
RedeemedAt string `json:"redeemed_at"` // YYYY-MM-DDTHH:MM:SS:UUUUUUUUUZ
Status string `json:"status"` // "UNFULFILLED"
Reward CommunityPointsReward `json:"reward"`
User TwitchBasicUser `json:"user"`
}
CommunityPointsReward struct {
ID string `json:"id"`
ChannelID string `json:"channel_id"`
Title string `json:"title"` // Same text as the button you press
Prompt string `json:"prompt"`
BackgroundColor string `json:"background_color"`
Cost int `json:"cost"`
IsEnabled bool `json:"is_enabled"`
IsInStock bool `json:"is_in_stock"`
IsPaused bool `json:"is_paused"`
IsSubOnly bool `json:"is_sub_only"`
IsUserInputRequired bool `json:"is_user_input_required"`
ShouldSkipRequestQueue bool `json:"should_redemptions_skip_request_queue"`
MaxPerStream struct {
IsEnabled bool `json:"is_enabled"`
MaxPerStream int `json:"max_per_stream"`
} `json:"max_per_stream"`
DefaultImage TwitchBasicImage `json:"default_image"`
Image TwitchBasicImage `json:"image"`
}
UpdatedReward struct {
ID string `json:"id"`
ChannelID string `json:"channel_id"`
Title string `json:"title"`
Prompt string `json:"prompt"`
Cost int `json:"cost"`
IsUserInputRequired bool `json:"is_user_input_required"`
IsSubOnly bool `json:"is_sub_only"`
Image TwitchBasicImage `json:"image"`
DefaultImage TwitchBasicImage `json:"default_image"`
BackgroundColor string `json:"background_color"`
IsEnabled bool `json:"is_enabled"`
IsPaused bool `json:"is_paused"`
IsInStock bool `json:"is_in_stock"`
MaxPerStream struct {
IsEnabled bool `json:"is_enabled"`
MaxPerStream int `json:"max_per_stream"`
} `json:"max_per_stream"`
ShouldRedemptionsSkipRequestQueue bool `json:"should_redemptions_skip_request_queue"`
UpdatedForIndicatorAt string `json:"updated_for_indicator_at"`
RedemptionsRedeemedCurrentStream int `json:"redemptions_redeemed_current_stream"`
//CooldownExpiresAt *string `json:"cooldown_expires_at"`
//TemplateID *int `json:"template_id"`
//MaxPerUserPerStream struct {
// IsEnabled bool `json:"is_enabled"`
// MaxPerUserPerStream int `json:"max_per_user_per_stream"`
//} `json:"max_per_user_per_stream"`
//GlobalCooldown struct {
// IsEnabled bool `json:"is_enabled"`
// GlobalCooldownSeconds int `json:"global_cooldown_seconds"`
//} `json:"global_cooldown"`
}
)
func (cpc CommunityPointsChannel) DecodeAs(v interface{}) error {
return json.Unmarshal(cpc.Data, v)
}
type (
// VideoPlaybackByID is used for "video-playback-by-id.$userid"
VideoPlaybackByID struct {
Type string `json:"type"` // viewcount
ServerTime float32 `json:"server_time"` // 1578261964.189628
// Type == viewcount
Viewers int `json:"viewers"` // 781
// Type == stream-up
PlayDelay int `json:"play_delay"`
}
// presence.53381086
Presence struct {
Type string `json:"type"` // presence
Data PresenceData `json:"data"`
}
Activity struct {
ChannelDisplayName string `json:"channel_display_name"` // 'Cirno_TV',
ChannelID string `json:"channel_id"` // '24761645',
ChannelLogin string `json:"channel_login"` // 'cirno_tv',
Game string `json:"game"` // 'Granny: Chapter Two',
GameID string `json:"game_id"` // '515177',
StreamID string `json:"stream_id"` // '36590887504',
Type string `json:"type"` // 'watching'
}
PresenceData struct {
Activities []Activity `json:"activities"`
Activity Activity `json:"activity"`
Availability string `json:"availability"` // online
Index int `json:"index"` // 21164
UpdateAt int `json:"updated_at"` // 1578262046
UserID int `json:"user_id"` // 53381086
UserLogin string `json:"user_login"` // cbdg
}
// StreamChatroom is used for "stream-chat-room-v1.$userid" topics
StreamChatroom struct {
// chat_rich_embed
Type string `json:"type"` // host_target_change and host_target_change_v2
Data struct {
ChannelID string `json:"channel_id"`
ChannelLogin string `json:"channel_login"`
TargetChannelID string `json:"target_channel_id"`
TargetChannelLogin string `json:"target_channel_login"`
PreviousTargetChannelID string `json:"previous_target_channel_id"`
NumViewers int `json:"num_viewers"`
} `json:"data"`
}
// BroadcastSettingsUpdate is used to update the stream title and category
// using the "broadcast-settings-update.$userid" topic
BroadcastSettingsUpdate struct {
ChannelID string `json:"channel_id"` //"channel_id":"261583209"
Channel string `json:"channel"` //"channel":"maiman"
Type string `json:"type"` //"type":"broadcast_settings_update"
OldStatus string `json:"old_status"`
OldGameID int `json:"old_game_id"`
OldGame string `json:"old_game"`
// These are null if there is no update(?)
Status *string `json:"status"`
Game *string `json:"game"`
GameID *int `json:"game_id"`
}
UserBasic struct {
ID string `json:"id"`
Login string `json:"login"`
DisplayName string `json:"display_name"`
}
DashboardActivity struct {
ID string `json:"id"`
Timestamp string `json:"timestamp"`
AlertStatus string `json:"alert_status"`
Type string `json:"type"`
// follow
Follower UserBasic `json:"follower"`
// prime_subscription | subscription | resubscription_sharing | prime_resubscription_sharing
Subscriber UserBasic `json:"subscriber"`
SubCumTenure int `json:"subscription_cumulative_tenure_months"`
SubMessage string `json:"subscription_custom_message_text"`
SubMultiDuration int `json:"subscription_multi_month_duration"`
SubMultiTenure int `json:"subscription_multi_month_tenure"`
SubscriptionTier string `json:"subscription_tier"`
// ... subscription_custom_message_fragments // [{"emoticon":{"id":"x","set_id":"y"},"text":"emoteCode"}]
// host_start
Host UserBasic `json:"host"`
HostingViewerCount int `json:"hosting_viewer_count"`
// raiding
Raider UserBasic `json:"raider"`
RaidingViewerCount int `json:"raiding_viewer_count"`
Status int `json:"status"`
// bits_usage
BitsUser UserBasic `json:"bits_user"`
BitsAnonymous bool `json:"bits_anonymous"`
BitsAmount bool `json:"bits_amount"`
}
)
// Decode decodes a raw Twitch PubSub message into a typed event.
func Decode(raw []byte) (evt interface{}, err error) {
var m TwitchMessage
if err = json.Unmarshal(raw, &m); err != nil {
return
}
topic, _ := extractTopic(m.Topic)
switch topic {
case "dashboard-activity-feed":
var da DashboardActivity
if err = m.DecodeAs(&da); err != nil {
err = errors.Wrap("DashboardActivityFeed", err)
return
}
evt = da
return
case "broadcast-settings-update":
var bcu BroadcastSettingsUpdate
if err = m.DecodeAs(&bcu); err != nil {
err = errors.Wrap("BroadcastSettingsUpdate", err)
return
}
evt = bcu
return
case "stream-chat-room-v1":
return
case "community-points-channel-v1":
var cpc CommunityPointsChannel
if err = m.DecodeAs(&cpc); err != nil {
err = errors.Wrap("CommunityPointsChannel", err)
return
} else if cpc.Type == "custom-reward-updated" {
} else if cpc.Type == "automatic-reward-updated" {
} else if cpc.Type == "community-goal-updated" {
} else if cpc.Type == "reward-redeemed" {
var cpr CommunityPointsRewardRedeemed
if err = cpc.DecodeAs(&cpr); err != nil {
err = errors.Wrap("CommunityPointsRewardRedeemed", err)
return
}
evt = cpr
return
}
err = errors.Newf("CommunityPointsChannel: unsupported type %q", cpc.Type)
return
case "video-playback-by-id":
var vp VideoPlaybackByID
if err = m.DecodeAs(&vp); err != nil {
err = errors.Wrap("VideoPlaybackByID", err)
return
}
evt = vp
return
}
err = errors.Wrap(topic, ErrUnsupportedTopic)
return
}
// Unwrap removes the TwitchMessage layer from a message.
func Unwrap(raw []byte) (string, error) {
var m TwitchMessage
err := json.Unmarshal(raw, &m)
return m.Message, err
}