backend for twitch chat widgets
This commit is contained in:
parent
09c11f11a1
commit
28b9492cb3
11 changed files with 725 additions and 94 deletions
11
colour/colour.go
Normal file
11
colour/colour.go
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
package colour
|
||||||
|
|
||||||
|
var Reset = "\033[0m"
|
||||||
|
var Red = "\033[31m"
|
||||||
|
var Green = "\033[32m"
|
||||||
|
var Yellow = "\033[33m"
|
||||||
|
var Blue = "\033[34m"
|
||||||
|
var Purple = "\033[35m"
|
||||||
|
var Cyan = "\033[36m"
|
||||||
|
var Gray = "\033[37m"
|
||||||
|
var White = "\033[97m"
|
||||||
339
twitch/api.go
339
twitch/api.go
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"codeberg.org/arimelody/ari-stream-tools/colour"
|
||||||
"codeberg.org/arimelody/ari-stream-tools/twitch/api"
|
"codeberg.org/arimelody/ari-stream-tools/twitch/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -76,14 +77,20 @@ func (srv *Service) handleNotification(payload *api.EventSubPayload) error {
|
||||||
|
|
||||||
switch payload.Subscription.Type {
|
switch payload.Subscription.Type {
|
||||||
|
|
||||||
case "channel.follow":
|
case api.CHANNEL_FOLLOW:
|
||||||
var event api.FollowEvent
|
var event api.FollowEvent
|
||||||
err := json.Unmarshal(jsonData, &event)
|
err := json.Unmarshal(jsonData, &event)
|
||||||
if err != nil { return fmt.Errorf("Failed to cast to api.FollowEvent: %v", err) }
|
if err != nil { return fmt.Errorf("Failed to cast to api.FollowEvent: %v", err) }
|
||||||
log.Printf("New follow: %s", event.UserName)
|
log.Printf("%sFollowed: %s%s", colour.Green, event.UserName, colour.Reset)
|
||||||
|
|
||||||
srv.labels.LatestFollower.Text = event.UserName
|
srv.labels.LatestFollower.Text = event.UserName
|
||||||
srv.labels.LatestFollower.C <- event.UserName
|
srv.labels.LatestFollower.C <- event.UserName
|
||||||
|
|
||||||
|
srv.SystemChatC <- &systemChatMessage{
|
||||||
|
Text: fmt.Sprintf("New Follow: %s", event.UserName),
|
||||||
|
Type: SYSTEM_CHAT_FOLLOW,
|
||||||
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(
|
if err := os.WriteFile(
|
||||||
path.Join(DATA_PATH, "state", LABEL_LATEST_FOLLOWER),
|
path.Join(DATA_PATH, "state", LABEL_LATEST_FOLLOWER),
|
||||||
[]byte(event.UserName), 0640,
|
[]byte(event.UserName), 0640,
|
||||||
|
|
@ -91,14 +98,46 @@ func (srv *Service) handleNotification(payload *api.EventSubPayload) error {
|
||||||
return fmt.Errorf("Failed to write %s state: %v", LABEL_LATEST_FOLLOWER, err)
|
return fmt.Errorf("Failed to write %s state: %v", LABEL_LATEST_FOLLOWER, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "channel.subscribe":
|
case api.CHANNEL_SUBSCRIBE:
|
||||||
var event api.SubscribeEvent
|
var event api.SubscribeEvent
|
||||||
err := json.Unmarshal(jsonData, &event)
|
err := json.Unmarshal(jsonData, &event)
|
||||||
if err != nil { return fmt.Errorf("Failed to cast to api.SubscribeEvent: %v", err) }
|
if err != nil { return fmt.Errorf("Failed to cast to api.SubscribeEvent: %v", err) }
|
||||||
log.Printf("New subscription: %s", event.UserName)
|
log.Printf("%sSubscribed: %s%s", colour.Purple, event.UserName, colour.Reset)
|
||||||
|
|
||||||
srv.labels.LatestSubscriber.Text = event.UserName
|
srv.labels.LatestSubscriber.Text = event.UserName
|
||||||
srv.labels.LatestSubscriber.C <- event.UserName
|
srv.labels.LatestSubscriber.C <- event.UserName
|
||||||
|
|
||||||
|
srv.SystemChatC <- &systemChatMessage{
|
||||||
|
Text: fmt.Sprintf("New Sub: %s", event.UserName),
|
||||||
|
Type: SYSTEM_CHAT_SUBSCRIBE,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(
|
||||||
|
path.Join(DATA_PATH, "state", LABEL_LATEST_SUBSCRIBER),
|
||||||
|
[]byte(event.UserName), 0640,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("Failed to write %s state: %v", LABEL_LATEST_SUBSCRIBER, err)
|
||||||
|
}
|
||||||
|
case api.CHANNEL_SUBSCRIPTION_MESSAGE:
|
||||||
|
var event api.SubscribeMessageEvent
|
||||||
|
err := json.Unmarshal(jsonData, &event)
|
||||||
|
if err != nil { return fmt.Errorf("Failed to cast to api.SubscribeMessageEvent: %v", err) }
|
||||||
|
log.Printf("%sResub: %s x%d%s", colour.Purple, event.UserName, event.CumulativeMonths, colour.Reset)
|
||||||
|
if event.StreakMonths > 1 {
|
||||||
|
log.Printf("%s is on a %d-month streak!", event.UserName, event.StreakMonths)
|
||||||
|
}
|
||||||
|
if len(event.Message.Text) > 0 {
|
||||||
|
log.Printf("%s: %s", event.UserLogin, event.Message.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.labels.LatestSubscriber.Text = event.UserName
|
||||||
|
srv.labels.LatestSubscriber.C <- event.UserName
|
||||||
|
|
||||||
|
srv.SystemChatC <- &systemChatMessage{
|
||||||
|
Text: fmt.Sprintf("Resub: %s x%d", event.UserName, event.CumulativeMonths),
|
||||||
|
Type: SYSTEM_CHAT_SUBSCRIBE,
|
||||||
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(
|
if err := os.WriteFile(
|
||||||
path.Join(DATA_PATH, "state", LABEL_LATEST_SUBSCRIBER),
|
path.Join(DATA_PATH, "state", LABEL_LATEST_SUBSCRIBER),
|
||||||
[]byte(event.UserName), 0640,
|
[]byte(event.UserName), 0640,
|
||||||
|
|
@ -106,14 +145,19 @@ func (srv *Service) handleNotification(payload *api.EventSubPayload) error {
|
||||||
return fmt.Errorf("Failed to write %s state: %v", LABEL_LATEST_SUBSCRIBER, err)
|
return fmt.Errorf("Failed to write %s state: %v", LABEL_LATEST_SUBSCRIBER, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "channel.cheer":
|
case api.CHANNEL_CHEER:
|
||||||
var event api.CheerEvent
|
var event api.CheerEvent
|
||||||
err := json.Unmarshal(jsonData, &event)
|
err := json.Unmarshal(jsonData, &event)
|
||||||
if err != nil { return fmt.Errorf("Failed to cast to api.CheerEvent: %v", err) }
|
if err != nil { return fmt.Errorf("Failed to cast to api.CheerEvent: %v", err) }
|
||||||
log.Printf("%s cheered x%d bits: %s", event.UserName, event.Bits, event.Message)
|
log.Printf("%s%s cheered x%d bits:%s %s", colour.Cyan, event.UserName, event.Bits, colour.Reset, event.Message)
|
||||||
srv.labels.LatestCheer.Text = event.UserName
|
srv.labels.LatestCheer.Text = event.UserName
|
||||||
srv.labels.LatestCheer.C <- event.UserName
|
srv.labels.LatestCheer.C <- event.UserName
|
||||||
|
|
||||||
|
srv.SystemChatC <- &systemChatMessage{
|
||||||
|
Text: fmt.Sprintf("Cheer x%d %s: %s", event.Bits, event.UserName, event.Message),
|
||||||
|
Type: SYSTEM_CHAT_CHEER,
|
||||||
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(
|
if err := os.WriteFile(
|
||||||
path.Join(DATA_PATH, "state", LABEL_LATEST_CHEER),
|
path.Join(DATA_PATH, "state", LABEL_LATEST_CHEER),
|
||||||
[]byte(event.UserName), 0640,
|
[]byte(event.UserName), 0640,
|
||||||
|
|
@ -121,54 +165,117 @@ func (srv *Service) handleNotification(payload *api.EventSubPayload) error {
|
||||||
return fmt.Errorf("Failed to write %s state: %v", LABEL_LATEST_CHEER, err)
|
return fmt.Errorf("Failed to write %s state: %v", LABEL_LATEST_CHEER, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "channel.raid":
|
case api.CHANNEL_RAID:
|
||||||
var event api.RaidEvent
|
var event api.RaidEvent
|
||||||
err := json.Unmarshal(jsonData, &event)
|
err := json.Unmarshal(jsonData, &event)
|
||||||
if err != nil { return fmt.Errorf("Failed to cast to api.RaidEvent: %v", err) }
|
if err != nil { return fmt.Errorf("Failed to cast to api.RaidEvent: %v", err) }
|
||||||
log.Printf("%s is now raiding with %d viewers!", event.FromUserName, event.Viewers)
|
log.Printf("%sRAID: %s with %d viewers!%s", colour.Red, event.FromUserName, event.Viewers, colour.Reset)
|
||||||
|
|
||||||
case "channel.channel_points_custom_reward_redemption.add":
|
srv.SystemChatC <- &systemChatMessage{
|
||||||
|
Text: fmt.Sprintf("%s raiding with %d viewers!", event.FromUserName, event.Viewers),
|
||||||
|
Type: SYSTEM_CHAT_RAID,
|
||||||
|
}
|
||||||
|
|
||||||
|
case api.CHANNEL_POINTS_CUSTOM_REWARD_REDEMPTION_ADD:
|
||||||
var event api.ChannelPointCustomRewardRedeemEvent
|
var event api.ChannelPointCustomRewardRedeemEvent
|
||||||
err := json.Unmarshal(jsonData, &event)
|
err := json.Unmarshal(jsonData, &event)
|
||||||
if err != nil { return fmt.Errorf("Failed to cast to api.ChannelPointCustomRewardRedeemEvent: %v", err) }
|
if err != nil { return fmt.Errorf("Failed to cast to api.ChannelPointCustomRewardRedeemEvent: %v", err) }
|
||||||
log.Printf(
|
log.Printf(
|
||||||
"%s just redeemed %s for %d channel points.",
|
"%s redeemed %s for %d channel points.",
|
||||||
event.UserName,
|
event.UserName,
|
||||||
event.Reward.Title,
|
event.Reward.Title,
|
||||||
event.Reward.Cost,
|
event.Reward.Cost,
|
||||||
)
|
)
|
||||||
|
|
||||||
case "channel.shoutout.create":
|
srv.SystemChatC <- &systemChatMessage{
|
||||||
|
Text: fmt.Sprintf("%s redeemed %s", event.UserName, event.Reward.Title),
|
||||||
|
Type: SYSTEM_CHAT_POINT_REDEEM,
|
||||||
|
}
|
||||||
|
|
||||||
|
case api.SHOUTOUT_CREATE:
|
||||||
var event api.ShoutoutCreate
|
var event api.ShoutoutCreate
|
||||||
err := json.Unmarshal(jsonData, &event)
|
err := json.Unmarshal(jsonData, &event)
|
||||||
if err != nil { return fmt.Errorf("Failed to cast to api.ShoutoutCreate: %v", err) }
|
if err != nil { return fmt.Errorf("Failed to cast to api.ShoutoutCreate: %v", err) }
|
||||||
log.Printf(
|
log.Printf("%s%s shouted out %s!%s", colour.Yellow, event.FromUserName, event.ToUserName, colour.Reset)
|
||||||
"%s gave a shoutout to %s.",
|
|
||||||
event.FromUserName,
|
|
||||||
event.ToUserName,
|
|
||||||
)
|
|
||||||
|
|
||||||
case "channel.chat.message":
|
srv.SystemChatC <- &systemChatMessage{
|
||||||
|
Text: fmt.Sprintf("Shoutout %s!", event.ToUserName),
|
||||||
|
Type: SYSTEM_CHAT_SHOUTOUT,
|
||||||
|
}
|
||||||
|
|
||||||
|
case api.CHANNEL_CHAT_MESSAGE:
|
||||||
var event api.ChatEvent
|
var event api.ChatEvent
|
||||||
err := json.Unmarshal(jsonData, &event)
|
err := json.Unmarshal(jsonData, &event)
|
||||||
if err != nil { return fmt.Errorf("Failed to cast to api.ChatEvent: %v", err) }
|
if err != nil { return fmt.Errorf("Failed to cast to api.ChatEvent: %v", err) }
|
||||||
if event.Cheer != nil { return nil }
|
|
||||||
log.Printf(
|
|
||||||
"[%s] %s: %s",
|
|
||||||
event.MessageID,
|
|
||||||
event.ChatterLogin,
|
|
||||||
event.Message.Text,
|
|
||||||
)
|
|
||||||
|
|
||||||
case "channel.chat.message_delete":
|
if event.Cheer != nil { return nil }
|
||||||
|
var modifier ChatMessageModifier = CHAT_MODIFIER_NONE
|
||||||
|
|
||||||
|
if event.MessageType == api.MESSAGE_CHANNEL_POINTS_HIGHLIGHTED {
|
||||||
|
modifier = CHAT_MODIFIER_HIGHLIGHT
|
||||||
|
log.Printf("Highlighted from %s: %s", event.ChatterLogin, event.Message.Text)
|
||||||
|
} else {
|
||||||
|
log.Printf("%s: %s", event.ChatterLogin, event.Message.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.ChatC <- &chatMessage{
|
||||||
|
ID: event.MessageID,
|
||||||
|
Username: event.ChatterLogin,
|
||||||
|
Colour: event.Color,
|
||||||
|
Text: event.Message.Text,
|
||||||
|
Fragments: event.Message.Fragments,
|
||||||
|
Modifier: modifier,
|
||||||
|
}
|
||||||
|
|
||||||
|
case api.CHANNEL_CHAT_MESSAGE_DELETE:
|
||||||
var event api.ChatDeleteEvent
|
var event api.ChatDeleteEvent
|
||||||
err := json.Unmarshal(jsonData, &event)
|
err := json.Unmarshal(jsonData, &event)
|
||||||
if err != nil { return fmt.Errorf("Failed to cast to api.ChatDeleteEvent: %v", err) }
|
if err != nil { return fmt.Errorf("Failed to cast to api.ChatDeleteEvent: %v", err) }
|
||||||
log.Printf(
|
log.Printf("Message %s deleted.", event.TargetLogin)
|
||||||
"Message %s by %s deleted.",
|
srv.DeleteChatC <- &deleteChatMessage{
|
||||||
event.MessageID,
|
Username: "",
|
||||||
event.TargetLogin,
|
MessageID: event.MessageID,
|
||||||
)
|
}
|
||||||
|
|
||||||
|
case api.CHANNEL_CHAT_NOTIFICATION:
|
||||||
|
var event api.ChatNotificationEvent
|
||||||
|
err := json.Unmarshal(jsonData, &event)
|
||||||
|
if err != nil { return fmt.Errorf("Failed to cast to api.ChatNotificationEvent: %v", err) }
|
||||||
|
switch event.NoticeType {
|
||||||
|
case api.NOTICE_ANNOUNCEMENT:
|
||||||
|
log.Printf("%sANNOUNCEMENT from %s:%s %s", colour.Yellow, event.ChatterName, colour.Reset, event.Message.Text)
|
||||||
|
srv.ChatC <- &chatMessage{
|
||||||
|
ID: event.MessageID,
|
||||||
|
Username: event.ChatterName,
|
||||||
|
Colour: event.Color,
|
||||||
|
Text: event.Message.Text,
|
||||||
|
Fragments: event.Message.Fragments,
|
||||||
|
|
||||||
|
Modifier: CHAT_MODIFIER_ANNOUNCEMENT,
|
||||||
|
AnnouncementColour: event.Announcement.Color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case api.CHANNEL_CHAT_CLEAR:
|
||||||
|
log.Printf("Chat cleared.")
|
||||||
|
srv.DeleteChatC <- &deleteChatMessage{
|
||||||
|
Username: "*",
|
||||||
|
MessageID: "*",
|
||||||
|
}
|
||||||
|
case api.CHANNEL_CHAT_CLEAR_USER_MESSAGES:
|
||||||
|
var event api.ChatClearUserMessagesEvent
|
||||||
|
err := json.Unmarshal(jsonData, &event)
|
||||||
|
if err != nil { return fmt.Errorf("Failed to cast to api.ChatClearUserMessagesEvent: %v", err) }
|
||||||
|
log.Printf("Messages from %s cleared.", event.TargetName)
|
||||||
|
srv.DeleteChatC <- &deleteChatMessage{
|
||||||
|
Username: event.TargetName,
|
||||||
|
MessageID: "*",
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: polls (creation and results)
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Printf("Unhandled message type %s.", payload.Subscription.Type)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,7 +284,7 @@ func (srv *Service) handleNotification(payload *api.EventSubPayload) error {
|
||||||
|
|
||||||
func (srv *Service) subscribeToEvent(
|
func (srv *Service) subscribeToEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
subscriptionType string,
|
subscriptionType api.SubscriptionType,
|
||||||
version string,
|
version string,
|
||||||
condition map[string]string,
|
condition map[string]string,
|
||||||
sessionID string,
|
sessionID string,
|
||||||
|
|
@ -189,7 +296,7 @@ func (srv *Service) subscribeToEvent(
|
||||||
}
|
}
|
||||||
|
|
||||||
Request struct {
|
Request struct {
|
||||||
Type string `json:"type"`
|
Type api.SubscriptionType `json:"type"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Condition map[string]string `json:"condition"`
|
Condition map[string]string `json:"condition"`
|
||||||
Transport Transport `json:"transport"`
|
Transport Transport `json:"transport"`
|
||||||
|
|
@ -247,90 +354,154 @@ func (srv *Service) subscribeToEvent(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calls subscribeToEvent with helpful logging
|
||||||
|
func (srv *Service) subscribeToEventHelper(
|
||||||
|
ctx context.Context,
|
||||||
|
subscriptionType api.SubscriptionType,
|
||||||
|
version string,
|
||||||
|
condition map[string]string,
|
||||||
|
sessionID string,
|
||||||
|
) {
|
||||||
|
if err := srv.subscribeToEvent(ctx, subscriptionType, version, condition, sessionID); err != nil {
|
||||||
|
log.Printf("Failed to subscribe to %s: %v", subscriptionType, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (srv *Service) subscribeToDefaultEvents(ctx context.Context) {
|
func (srv *Service) subscribeToDefaultEvents(ctx context.Context) {
|
||||||
// channel.follow
|
// Channel Updates
|
||||||
if err := srv.subscribeToEvent(
|
srv.subscribeToEventHelper(
|
||||||
ctx, "channel.follow", "2",
|
ctx, api.CHANNEL_UPDATE, "2",
|
||||||
|
map[string]string{
|
||||||
|
"broadcaster_user_id": srv.channelID,
|
||||||
|
},
|
||||||
|
srv.eventSubSession.ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Follows
|
||||||
|
srv.subscribeToEventHelper(
|
||||||
|
ctx, api.CHANNEL_FOLLOW, "2",
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"broadcaster_user_id": srv.channelID,
|
"broadcaster_user_id": srv.channelID,
|
||||||
"moderator_user_id": srv.channelID,
|
"moderator_user_id": srv.channelID,
|
||||||
},
|
},
|
||||||
srv.eventSubSession.ID,
|
srv.eventSubSession.ID,
|
||||||
); err != nil {
|
)
|
||||||
log.Printf("Failed to subscribe to channel.follow: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// channel.subscribe
|
// Subscriptions
|
||||||
if err := srv.subscribeToEvent(
|
srv.subscribeToEventHelper(
|
||||||
ctx, "channel.subscribe", "1",
|
ctx, api.CHANNEL_SUBSCRIBE, "1",
|
||||||
map[string]string{ "broadcaster_user_id": srv.channelID },
|
map[string]string{ "broadcaster_user_id": srv.channelID },
|
||||||
srv.eventSubSession.ID,
|
srv.eventSubSession.ID,
|
||||||
); err != nil {
|
)
|
||||||
log.Printf("Failed to subscribe to channel.subscribe: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// channel.cheer
|
// Subscription Message
|
||||||
if err := srv.subscribeToEvent(
|
srv.subscribeToEventHelper(
|
||||||
ctx, "channel.cheer", "1",
|
ctx, api.CHANNEL_SUBSCRIPTION_MESSAGE, "1",
|
||||||
map[string]string{ "broadcaster_user_id": srv.channelID },
|
map[string]string{ "broadcaster_user_id": srv.channelID },
|
||||||
srv.eventSubSession.ID,
|
srv.eventSubSession.ID,
|
||||||
); err != nil {
|
)
|
||||||
log.Printf("Failed to subscribe to channel.cheer: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// channel.raid
|
// Gift Subscriptions
|
||||||
if err := srv.subscribeToEvent(
|
srv.subscribeToEventHelper(
|
||||||
ctx, "channel.raid", "1",
|
ctx, api.CHANNEL_SUBSCRIPTION_GIFT, "1",
|
||||||
|
map[string]string{ "broadcaster_user_id": srv.channelID },
|
||||||
|
srv.eventSubSession.ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cheer
|
||||||
|
srv.subscribeToEventHelper(
|
||||||
|
ctx, api.CHANNEL_CHEER, "1",
|
||||||
|
map[string]string{ "broadcaster_user_id": srv.channelID },
|
||||||
|
srv.eventSubSession.ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Raid
|
||||||
|
srv.subscribeToEventHelper(
|
||||||
|
ctx, api.CHANNEL_RAID, "1",
|
||||||
map[string]string{ "to_broadcaster_user_id": srv.channelID },
|
map[string]string{ "to_broadcaster_user_id": srv.channelID },
|
||||||
srv.eventSubSession.ID,
|
srv.eventSubSession.ID,
|
||||||
); err != nil {
|
)
|
||||||
log.Printf("Failed to subscribe to channel.raid: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// channel.channel_points_custom_reward_redemption.add
|
// Channel Point Redeems
|
||||||
if err := srv.subscribeToEvent(
|
srv.subscribeToEventHelper(
|
||||||
ctx, "channel.channel_points_custom_reward_redemption.add", "1",
|
ctx, api.CHANNEL_POINTS_CUSTOM_REWARD_REDEMPTION_ADD, "1",
|
||||||
map[string]string{ "broadcaster_user_id": srv.channelID },
|
map[string]string{ "broadcaster_user_id": srv.channelID },
|
||||||
srv.eventSubSession.ID,
|
srv.eventSubSession.ID,
|
||||||
); err != nil {
|
)
|
||||||
log.Printf("Failed to subscribe to channel.channel_points_custom_reward_redemption.add: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// channel.shoutout.create
|
// Shoutouts
|
||||||
if err := srv.subscribeToEvent(
|
srv.subscribeToEventHelper(
|
||||||
ctx, "channel.shoutout.create", "1",
|
ctx, api.SHOUTOUT_CREATE, "1",
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"broadcaster_user_id": srv.channelID,
|
"broadcaster_user_id": srv.channelID,
|
||||||
"moderator_user_id": srv.channelID,
|
"moderator_user_id": srv.channelID,
|
||||||
},
|
},
|
||||||
srv.eventSubSession.ID,
|
srv.eventSubSession.ID,
|
||||||
); err != nil {
|
)
|
||||||
log.Printf("Failed to subscribe to channel.shoutout.create: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// channel.chat.message
|
// Chat Messages
|
||||||
if err := srv.subscribeToEvent(
|
srv.subscribeToEventHelper(
|
||||||
ctx, "channel.chat.message", "1",
|
ctx, api.CHANNEL_CHAT_MESSAGE, "1",
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"broadcaster_user_id": srv.channelID,
|
"broadcaster_user_id": srv.channelID,
|
||||||
"user_id": srv.channelID,
|
"user_id": srv.channelID,
|
||||||
},
|
},
|
||||||
srv.eventSubSession.ID,
|
srv.eventSubSession.ID,
|
||||||
); err != nil {
|
)
|
||||||
log.Printf("Failed to subscribe to channel.chat.message: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// channel.chat.message_delete
|
// Chat Message Deletions
|
||||||
if err := srv.subscribeToEvent(
|
srv.subscribeToEventHelper(
|
||||||
ctx, "channel.chat.message_delete", "1",
|
ctx, api.CHANNEL_CHAT_MESSAGE_DELETE, "1",
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"broadcaster_user_id": srv.channelID,
|
"broadcaster_user_id": srv.channelID,
|
||||||
"user_id": srv.channelID,
|
"user_id": srv.channelID,
|
||||||
},
|
},
|
||||||
srv.eventSubSession.ID,
|
srv.eventSubSession.ID,
|
||||||
); err != nil {
|
)
|
||||||
log.Printf("Failed to subscribe to channel.chat.message_delete: %v", err)
|
|
||||||
}
|
// Chat Notifications
|
||||||
|
srv.subscribeToEventHelper(
|
||||||
|
ctx, api.CHANNEL_CHAT_NOTIFICATION, "1",
|
||||||
|
map[string]string{
|
||||||
|
"broadcaster_user_id": srv.channelID,
|
||||||
|
"user_id": srv.channelID,
|
||||||
|
},
|
||||||
|
srv.eventSubSession.ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Chat Clear
|
||||||
|
srv.subscribeToEventHelper(
|
||||||
|
ctx, api.CHANNEL_CHAT_CLEAR, "1",
|
||||||
|
map[string]string{
|
||||||
|
"broadcaster_user_id": srv.channelID,
|
||||||
|
"user_id": srv.channelID,
|
||||||
|
},
|
||||||
|
srv.eventSubSession.ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clear User Messages
|
||||||
|
srv.subscribeToEventHelper(
|
||||||
|
ctx, api.CHANNEL_CHAT_CLEAR_USER_MESSAGES, "1",
|
||||||
|
map[string]string{
|
||||||
|
"broadcaster_user_id": srv.channelID,
|
||||||
|
"user_id": srv.channelID,
|
||||||
|
},
|
||||||
|
srv.eventSubSession.ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Polls (Starting)
|
||||||
|
srv.subscribeToEventHelper(
|
||||||
|
ctx, api.CHANNEL_POLL_BEGIN, "1",
|
||||||
|
map[string]string{ "broadcaster_user_id": srv.channelID },
|
||||||
|
srv.eventSubSession.ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Polls (Ending)
|
||||||
|
srv.subscribeToEventHelper(
|
||||||
|
ctx, api.CHANNEL_POLL_END, "1",
|
||||||
|
map[string]string{ "broadcaster_user_id": srv.channelID },
|
||||||
|
srv.eventSubSession.ID,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|
@ -383,6 +554,10 @@ func (srv *Service) getFollowers(
|
||||||
return resData, nil
|
return resData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unfortunately, this function is very unreliable for pulling chronological
|
||||||
|
// subscription records. Twitch API does not currently provide a mechanism for
|
||||||
|
// fetching the latest subscriber; this will need to be tracked manually.
|
||||||
|
/*
|
||||||
type (
|
type (
|
||||||
Subscriber struct {
|
Subscriber struct {
|
||||||
BroadcasterID string `json:"broadcaster_id"`
|
BroadcasterID string `json:"broadcaster_id"`
|
||||||
|
|
@ -409,10 +584,6 @@ type (
|
||||||
Points int `json:"points"`
|
Points int `json:"points"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// Unfortunately, this function is very unreliable for pulling chronological
|
|
||||||
// subscription records. Twitch API does not currently provide a mechanism for
|
|
||||||
// fetching the latest subscriber; this will need to be tracked manually.
|
|
||||||
/*
|
|
||||||
func (srv *Service) getSubscribers(
|
func (srv *Service) getSubscribers(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
broadcasterID string,
|
broadcasterID string,
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ type (
|
||||||
EventSubSubscription struct {
|
EventSubSubscription struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Type string `json:"type"`
|
Type SubscriptionType `json:"type"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Cost int `json:"cost"`
|
Cost int `json:"cost"`
|
||||||
Condition any `json:"condition"`
|
Condition any `json:"condition"`
|
||||||
|
|
|
||||||
10
twitch/api/chat-message.go
Normal file
10
twitch/api/chat-message.go
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
type ChatMessageType string
|
||||||
|
|
||||||
|
const MESSAGE_TEXT ChatMessageType = "text"
|
||||||
|
const MESSAGE_CHANNEL_POINTS_HIGHLIGHTED ChatMessageType = "channel_points_highlighted"
|
||||||
|
const MESSAGE_CHANNEL_POINTS_SUB_ONLY ChatMessageType = "channel_points_sub_only"
|
||||||
|
const MESSAGE_USER_INTRO ChatMessageType = "user_intro"
|
||||||
|
const MESSAGE_POWER_UPS_MESSAGE_EFFECT ChatMessageType = "power_ups_message_effect"
|
||||||
|
const MESSAGE_POWER_UPS_GIGANTIFIED_EMOTE ChatMessageType = "power_ups_gigantified_emote"
|
||||||
|
|
@ -22,6 +22,24 @@ type (
|
||||||
IsGift bool `json:"is_gift"`
|
IsGift bool `json:"is_gift"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SubscribeMessageEmote struct {
|
||||||
|
Begin int `json:"begin"`
|
||||||
|
End int `json:"end"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
SubscribeMessageContent struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Emotes []SubscribeMessageEmote `json:"emotes"`
|
||||||
|
}
|
||||||
|
SubscribeMessageEvent struct {
|
||||||
|
ChannelEvent
|
||||||
|
Tier string `json:"tier"`
|
||||||
|
Message SubscribeMessageContent `json:"message"`
|
||||||
|
CumulativeMonths int `json:"cumulative_months"`
|
||||||
|
StreakMonths int `json:"streak_months"`
|
||||||
|
DurationMonths int `json:"duration_months"`
|
||||||
|
}
|
||||||
|
|
||||||
CheerEvent struct {
|
CheerEvent struct {
|
||||||
ChannelEvent
|
ChannelEvent
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
|
@ -97,11 +115,11 @@ type (
|
||||||
}
|
}
|
||||||
ChatMessageFragment struct {
|
ChatMessageFragment struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text,omitempty"`
|
||||||
Cheermote *ChatMessageCheermote `json:"cheermote"`
|
Cheermote *ChatMessageCheermote `json:"cheermote,omitempty"`
|
||||||
Emote *ChatMessageEmote `json:"emote"`
|
Emote *ChatMessageEmote `json:"emote,omitempty"`
|
||||||
Mention *ChatMessageMention `json:"mention"`
|
Mention *ChatMessageMention `json:"mention,omitempty"`
|
||||||
Gif *ChatMessageGif `json:"gif"`
|
Gif *ChatMessageGif `json:"gif,omitempty"`
|
||||||
}
|
}
|
||||||
ChatMessage struct {
|
ChatMessage struct {
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
|
|
@ -143,7 +161,7 @@ type (
|
||||||
|
|
||||||
MessageID string `json:"message_id"`
|
MessageID string `json:"message_id"`
|
||||||
Message ChatMessage `json:"message"`
|
Message ChatMessage `json:"message"`
|
||||||
MessageType string `json:"message_type"`
|
MessageType ChatMessageType `json:"message_type"`
|
||||||
|
|
||||||
Badges []ChatBadge `json:"badges"`
|
Badges []ChatBadge `json:"badges"`
|
||||||
Cheer *ChatCheer `json:"cheer"`
|
Cheer *ChatCheer `json:"cheer"`
|
||||||
|
|
@ -163,7 +181,7 @@ type (
|
||||||
IsSourceOnly bool `json:"is_source_only"`
|
IsSourceOnly bool `json:"is_source_only"`
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatDeleteEvent struct {
|
ChatClearUserMessagesEvent struct {
|
||||||
BroadcasterID string `json:"broadcaster_user_id"`
|
BroadcasterID string `json:"broadcaster_user_id"`
|
||||||
BroadcasterLogin string `json:"broadcaster_user_login"`
|
BroadcasterLogin string `json:"broadcaster_user_login"`
|
||||||
BroadcasterName string `json:"broadcaster_user_name"`
|
BroadcasterName string `json:"broadcaster_user_name"`
|
||||||
|
|
@ -171,7 +189,54 @@ type (
|
||||||
TargetID string `json:"target_user_id"`
|
TargetID string `json:"target_user_id"`
|
||||||
TargetLogin string `json:"target_user_login"`
|
TargetLogin string `json:"target_user_login"`
|
||||||
TargetName string `json:"target_user_name"`
|
TargetName string `json:"target_user_name"`
|
||||||
|
}
|
||||||
|
ChatDeleteEvent struct {
|
||||||
|
ChatClearUserMessagesEvent
|
||||||
MessageID string `json:"message_id"`
|
MessageID string `json:"message_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChatAnnouncement struct {
|
||||||
|
Color string `json:"color"`
|
||||||
|
}
|
||||||
|
CharityDonationAmount struct {
|
||||||
|
Value int `json:"value"`
|
||||||
|
DecimalPlace int `json:"decimal_place"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
}
|
||||||
|
CharityDonation struct {
|
||||||
|
CharityName string `json:"charity_name"`
|
||||||
|
Amount CharityDonationAmount `json:"amount"`
|
||||||
|
}
|
||||||
|
WatchStreak struct {
|
||||||
|
StreakCount int `json:"streak_count"`
|
||||||
|
ChannelPointsAwarded int `json:"channel_points_awarded"`
|
||||||
|
}
|
||||||
|
Modiversary struct {
|
||||||
|
Months int `json:"months"`
|
||||||
|
}
|
||||||
|
ChatNotificationEvent struct {
|
||||||
|
BroadcasterID string `json:"broadcaster_user_id"`
|
||||||
|
BroadcasterLogin string `json:"broadcaster_user_login"`
|
||||||
|
BroadcasterName string `json:"broadcaster_user_name"`
|
||||||
|
|
||||||
|
ChatterID string `json:"chatter_user_id"`
|
||||||
|
ChatterName string `json:"chatter_user_name"`
|
||||||
|
ChatterIsAnonymous bool `json:"chatter_is_anonymous"`
|
||||||
|
|
||||||
|
Color string `json:"color"`
|
||||||
|
Badges []ChatBadge `json:"badges"`
|
||||||
|
|
||||||
|
// The message Twitch shows in the chat room for this notice.
|
||||||
|
SystemMessage string `json:"system_message"`
|
||||||
|
|
||||||
|
MessageID string `json:"message_id"`
|
||||||
|
Message ChatMessage `json:"message"`
|
||||||
|
|
||||||
|
NoticeType ChatNoticeType `json:"notice_type"`
|
||||||
|
|
||||||
|
Announcement ChatAnnouncement `json:"announcement"`
|
||||||
|
CharityDonation CharityDonation `json:"charity_donation"`
|
||||||
|
WatchStreak WatchStreak `json:"watch_streak"`
|
||||||
|
Modiversary Modiversary `json:"modiversary"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
29
twitch/api/notice.go
Normal file
29
twitch/api/notice.go
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
type ChatNoticeType string
|
||||||
|
|
||||||
|
const NOTICE_SUB ChatNoticeType = "sub"
|
||||||
|
const NOTICE_RESUB ChatNoticeType = "resub"
|
||||||
|
const NOTICE_SUB_GIFT ChatNoticeType = "sub_gift"
|
||||||
|
const NOTICE_COMMUNITY_SUB_GIFT ChatNoticeType = "community_sub_gift"
|
||||||
|
const NOTICE_GIFT_PAID_UPGRADE ChatNoticeType = "gift_paid_upgrade"
|
||||||
|
const NOTICE_PRIME_PAID_UPGRADE ChatNoticeType = "prime_paid_upgrade"
|
||||||
|
const NOTICE_RAID ChatNoticeType = "raid"
|
||||||
|
const NOTICE_UNRAID ChatNoticeType = "unraid"
|
||||||
|
const NOTICE_PAY_IT_FORWARD ChatNoticeType = "pay_it_forward"
|
||||||
|
const NOTICE_ANNOUNCEMENT ChatNoticeType = "announcement"
|
||||||
|
const NOTICE_BITS_BADGE_TIER ChatNoticeType = "bits_badge_tier"
|
||||||
|
const NOTICE_CHARITY_DONATION ChatNoticeType = "charity_donation"
|
||||||
|
const NOTICE_WATCH_STREAK ChatNoticeType = "watch_streak"
|
||||||
|
const NOTICE_MODIVERSARY ChatNoticeType = "modiversary"
|
||||||
|
const NOTICE_SHARED_CHAT_SUB ChatNoticeType = "shared_chat_sub"
|
||||||
|
const NOTICE_SHARED_CHAT_RESUB ChatNoticeType = "shared_chat_resub"
|
||||||
|
const NOTICE_SHARED_CHAT_SUB_GIFT ChatNoticeType = "shared_chat_sub_gift"
|
||||||
|
const NOTICE_SHARED_CHAT_COMMUNITY_SUB_GIFT ChatNoticeType = "shared_chat_community_sub_gift"
|
||||||
|
const NOTICE_SHARED_CHAT_GIFT_PAID_UPGRADE ChatNoticeType = "shared_chat_gift_paid_upgrade"
|
||||||
|
const NOTICE_SHARED_CHAT_PRIME_PAID_UPGRADE ChatNoticeType = "shared_chat_prime_paid_upgrade"
|
||||||
|
const NOTICE_SHARED_CHAT_RAID ChatNoticeType = "shared_chat_raid"
|
||||||
|
const NOTICE_SHARED_CHAT_PAY_IT_FORWARD ChatNoticeType = "shared_chat_pay_it_forward"
|
||||||
|
const NOTICE_SHARED_CHAT_ANNOUNCEMENT ChatNoticeType = "shared_chat_announcement"
|
||||||
|
const NOTICE_SHARED_CHAT_MODIVERSARY ChatNoticeType = "shared_chat_modiversary"
|
||||||
|
const NOTICE_UNKNOWN ChatNoticeType = "unknown"
|
||||||
206
twitch/api/subscriptions.go
Normal file
206
twitch/api/subscriptions.go
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
type SubscriptionType string
|
||||||
|
|
||||||
|
// Automod
|
||||||
|
|
||||||
|
// Version 1 - A user is notified if a message is caught by automod for review.
|
||||||
|
const AUTOMOD_MESSAGE_HOLD SubscriptionType = "automod.message.hold"
|
||||||
|
// Version 2 - A user is notified if a message is caught by automod for review. Only public blocked terms trigger notifications, not private ones.
|
||||||
|
const AUTOMOD_MESSAGE_HOLD_V2 SubscriptionType = "automod.message.hold"
|
||||||
|
// Version 1 - A message in the automod queue had its status changed.
|
||||||
|
const AUTOMOD_MESSAGE_UPDATE SubscriptionType = "automod.message.update"
|
||||||
|
// Version 2 - A message in the automod queue had its status changed. Only public blocked terms trigger notifications, not private ones.
|
||||||
|
const AUTOMOD_MESSAGE_UPDATE_V2 SubscriptionType = "automod.message.update"
|
||||||
|
// Version 1 - A notification is sent when a broadcaster’s automod settings are updated.
|
||||||
|
const AUTOMOD_SETTINGS_UPDATE SubscriptionType = "automod.settings.update"
|
||||||
|
// Version 1 - A notification is sent when a broadcaster’s automod terms are updated. Changes to private terms are not sent.
|
||||||
|
const AUTOMOD_TERMS_UPDATE SubscriptionType = "automod.terms.update"
|
||||||
|
|
||||||
|
// Channel
|
||||||
|
|
||||||
|
// Version 1 - A notification is sent whenever Bits are used on a channel.
|
||||||
|
const CHANNEL_BITS_USE SubscriptionType = "channel.bits.use"
|
||||||
|
// Version 2 - A broadcaster updates their channel properties e.g., category, title, content classification labels, broadcast, or language.
|
||||||
|
const CHANNEL_UPDATE SubscriptionType = "channel.update"
|
||||||
|
// Version 2 - A specified channel receives a follow.
|
||||||
|
const CHANNEL_FOLLOW SubscriptionType = "channel.follow"
|
||||||
|
// Version 1 - A midroll commercial break has started running.
|
||||||
|
const CHANNEL_AD_BREAK_BEGIN SubscriptionType = "channel.ad_break.begin"
|
||||||
|
|
||||||
|
// Chat
|
||||||
|
|
||||||
|
// Version 1 - A moderator or bot has cleared all messages from the chat room.
|
||||||
|
const CHANNEL_CHAT_CLEAR SubscriptionType = "channel.chat.clear"
|
||||||
|
// Version 1 - A moderator or bot has cleared all messages from a specific user.
|
||||||
|
const CHANNEL_CHAT_CLEAR_USER_MESSAGES SubscriptionType = "channel.chat.clear_user_messages"
|
||||||
|
// Version 1 - Any user sends a message to a specific chat room.
|
||||||
|
const CHANNEL_CHAT_MESSAGE SubscriptionType = "channel.chat.message"
|
||||||
|
// Version 1 - A moderator has removed a specific message.
|
||||||
|
const CHANNEL_CHAT_MESSAGE_DELETE SubscriptionType = "channel.chat.message_delete"
|
||||||
|
// Version 1 - A notification for when an event that appears in chat has occurred.
|
||||||
|
const CHANNEL_CHAT_NOTIFICATION SubscriptionType = "channel.chat.notification"
|
||||||
|
// Version 1 - A notification for when a broadcaster’s chat settings are updated.
|
||||||
|
const CHANNEL_CHAT_SETTINGS_UPDATE SubscriptionType = "channel.chat_settings.update"
|
||||||
|
// Version 1 - A user is notified if their message is caught by automod.
|
||||||
|
const CHANNEL_CHAT_USER_MESSAGE_HOLD SubscriptionType = "channel.chat.user_message_hold"
|
||||||
|
// Version 1 - A user is notified if their message’s automod status is updated.
|
||||||
|
const CHANNEL_CHAT_USER_MESSAGE_UPDATE SubscriptionType = "channel.chat.user_message_update"
|
||||||
|
|
||||||
|
// Shared Chat
|
||||||
|
|
||||||
|
// Version 1 - A notification when a channel becomes active in an active shared chat session.
|
||||||
|
const CHANNEL_SHARED_CHAT_SESSION_BEGIN SubscriptionType = "channel.shared_chat.begin"
|
||||||
|
// Version 1 - A notification when the active shared chat session the channel is in changes.
|
||||||
|
const CHANNEL_SHARED_CHAT_SESSION_UPDATE SubscriptionType = "channel.shared_chat.update"
|
||||||
|
// Version 1 - A notification when a channel leaves a shared chat session or the session ends.
|
||||||
|
const CHANNEL_SHARED_CHAT_SESSION_END SubscriptionType = "channel.shared_chat.end"
|
||||||
|
|
||||||
|
// Subscriptions
|
||||||
|
|
||||||
|
// Version 1 - A notification is sent when a specified channel receives a subscriber. This does not include resubscribes.
|
||||||
|
const CHANNEL_SUBSCRIBE SubscriptionType = "channel.subscribe"
|
||||||
|
// Version 1 - A notification when a subscription to the specified channel ends.
|
||||||
|
const CHANNEL_SUBSCRIPTION_END SubscriptionType = "channel.subscription.end"
|
||||||
|
// Version 1 - A notification when a viewer gives a gift subscription to one or more users in the specified channel.
|
||||||
|
const CHANNEL_SUBSCRIPTION_GIFT SubscriptionType = "channel.subscription.gift"
|
||||||
|
// Version 1 - A notification when a user sends a resubscription chat message in a specific channel.
|
||||||
|
const CHANNEL_SUBSCRIPTION_MESSAGE SubscriptionType = "channel.subscription.message"
|
||||||
|
|
||||||
|
// Version 1 - A user cheers on the specified channel.
|
||||||
|
const CHANNEL_CHEER SubscriptionType = "channel.cheer"
|
||||||
|
// Version 1 - A broadcaster raids another broadcaster’s channel.
|
||||||
|
const CHANNEL_RAID SubscriptionType = "channel.raid"
|
||||||
|
|
||||||
|
// Version 1 - A viewer is banned from the specified channel.
|
||||||
|
const CHANNEL_BAN SubscriptionType = "channel.ban"
|
||||||
|
// Version 1 - A viewer is unbanned from the specified channel.
|
||||||
|
const CHANNEL_UNBAN SubscriptionType = "channel.unban"
|
||||||
|
// Version 1 - A user creates an unban request.
|
||||||
|
const CHANNEL_UNBAN_REQUEST_CREATE SubscriptionType = "channel.unban_request.create"
|
||||||
|
// Version 1 - An unban request has been resolved.
|
||||||
|
const CHANNEL_UNBAN_REQUEST_RESOLVE SubscriptionType = "channel.unban_request.resolve"
|
||||||
|
|
||||||
|
// Version 1 - A moderator performs a moderation action in a channel.
|
||||||
|
const CHANNEL_MODERATE SubscriptionType = "channel.moderate"
|
||||||
|
// Version 2 - A moderator performs a moderation action in a channel. Includes warnings.
|
||||||
|
const CHANNEL_MODERATE_V2 SubscriptionType = "channel.moderate"
|
||||||
|
// Version 1 - Moderator privileges were added to a user on a specified channel.
|
||||||
|
const CHANNEL_MODERATOR_ADD SubscriptionType = "channel.moderator.add"
|
||||||
|
// Version 1 - Moderator privileges were removed from a user on a specified channel.
|
||||||
|
const CHANNEL_MODERATOR_REMOVE SubscriptionType = "channel.moderator.remove"
|
||||||
|
|
||||||
|
// Version beta - The host began a new Guest Star session.
|
||||||
|
const CHANNEL_GUEST_STAR_SESSION_BEGIN SubscriptionType = "channel.guest_star_session.begin"
|
||||||
|
// Version beta - A running Guest Star session has ended.
|
||||||
|
const CHANNEL_GUEST_STAR_SESSION_END SubscriptionType = "channel.guest_star_session.end"
|
||||||
|
// Version beta - A guest or a slot is updated in an active Guest Star session.
|
||||||
|
const CHANNEL_GUEST_STAR_GUEST_UPDATE SubscriptionType = "channel.guest_star_guest.update"
|
||||||
|
// Version beta - The host preferences for Guest Star have been updated.
|
||||||
|
const CHANNEL_GUEST_STAR_SETTINGS_UPDATE SubscriptionType = "channel.guest_star_settings.update"
|
||||||
|
|
||||||
|
// Version 1 - A viewer has redeemed an automatic channel points reward on the specified channel.
|
||||||
|
const CHANNEL_POINTS_AUTOMATIC_REWARD_REDEMPTION_ADD SubscriptionType = "channel.channel_points_automatic_reward_redemption.add"
|
||||||
|
// Version 2 - A viewer has redeemed an automatic channel points reward on the specified channel.
|
||||||
|
const CHANNEL_POINTS_AUTOMATIC_REWARD_REDEMPTION_ADD_V2 SubscriptionType = "channel.channel_points_automatic_reward_redemption.add"
|
||||||
|
// Version 1 - A custom channel points reward has been created for the specified channel.
|
||||||
|
const CHANNEL_POINTS_CUSTOM_REWARD_ADD SubscriptionType = "channel.channel_points_custom_reward.add"
|
||||||
|
// Version 1 - A custom channel points reward has been updated for the specified channel.
|
||||||
|
const CHANNEL_POINTS_CUSTOM_REWARD_UPDATE SubscriptionType = "channel.channel_points_custom_reward.update"
|
||||||
|
// Version 1 - A custom channel points reward has been removed from the specified channel.
|
||||||
|
const CHANNEL_POINTS_CUSTOM_REWARD_REMOVE SubscriptionType = "channel.channel_points_custom_reward.remove"
|
||||||
|
// Version 1 - A viewer has redeemed a custom channel points reward on the specified channel.
|
||||||
|
const CHANNEL_POINTS_CUSTOM_REWARD_REDEMPTION_ADD SubscriptionType = "channel.channel_points_custom_reward_redemption.add"
|
||||||
|
// Version 1 - A redemption of a channel points custom reward has been updated for the specified channel.
|
||||||
|
const CHANNEL_POINTS_CUSTOM_REWARD_REDEMPTION_UPDATE SubscriptionType = "channel.channel_points_custom_reward_redemption.update"
|
||||||
|
// Version 1 - A viewer has redeemed a custom Power-up on the specified channel.
|
||||||
|
const CHANNEL_CUSTOM_POWER_UPS_REDEMPTION_ADD SubscriptionType = "channel.custom_power_up_redemption.add"
|
||||||
|
|
||||||
|
// Version 1 - A poll started on a specified channel.
|
||||||
|
const CHANNEL_POLL_BEGIN SubscriptionType = "channel.poll.begin"
|
||||||
|
// Version 1 - Users respond to a poll on a specified channel.
|
||||||
|
const CHANNEL_POLL_PROGRESS SubscriptionType = "channel.poll.progress"
|
||||||
|
// Version 1 - A poll ended on a specified channel.
|
||||||
|
const CHANNEL_POLL_END SubscriptionType = "channel.poll.end"
|
||||||
|
|
||||||
|
// Version 1 - A Prediction started on a specified channel.
|
||||||
|
const CHANNEL_PREDICTION_BEGIN SubscriptionType = "channel.prediction.begin"
|
||||||
|
// Version 1 - Users participated in a Prediction on a specified channel.
|
||||||
|
const CHANNEL_PREDICTION_PROGRESS SubscriptionType = "channel.prediction.progress"
|
||||||
|
// Version 1 - A Prediction was locked on a specified channel.
|
||||||
|
const CHANNEL_PREDICTION_LOCK SubscriptionType = "channel.prediction.lock"
|
||||||
|
// Version 1 - A Prediction ended on a specified channel.
|
||||||
|
const CHANNEL_PREDICTION_END SubscriptionType = "channel.prediction.end"
|
||||||
|
|
||||||
|
// Version 1 - A chat message has been sent by a suspicious user.
|
||||||
|
const CHANNEL_SUSPICIOUS_USER_MESSAGE SubscriptionType = "channel.suspicious_user.message"
|
||||||
|
// Version 1 - A suspicious user has been updated.
|
||||||
|
const CHANNEL_SUSPICIOUS_USER_UPDATE SubscriptionType = "channel.suspicious_user.update"
|
||||||
|
|
||||||
|
// Version 1 - A VIP is added to the channel.
|
||||||
|
const CHANNEL_VIP_ADD SubscriptionType = "channel.vip.add"
|
||||||
|
// Version 1 - A VIP is removed from the channel.
|
||||||
|
const CHANNEL_VIP_REMOVE SubscriptionType = "channel.vip.remove"
|
||||||
|
|
||||||
|
// Version 1 - A user acknowledges a warning. Broadcasters and moderators can see the warning’s details.
|
||||||
|
const CHANNEL_WARNING_ACKNOWLEDGEMENT SubscriptionType = "channel.warning.acknowledge"
|
||||||
|
// Version 1 - A user is sent a warning. Broadcasters and moderators can see the warning’s details.
|
||||||
|
const CHANNEL_WARNING_SEND SubscriptionType = "channel.warning.send"
|
||||||
|
|
||||||
|
// Version 1 - Sends an event notification when a user donates to the broadcaster’s charity campaign.
|
||||||
|
const CHARITY_DONATION SubscriptionType = "channel.charity_campaign.donate"
|
||||||
|
// Version 1 - Sends an event notification when the broadcaster starts a charity campaign.
|
||||||
|
const CHARITY_CAMPAIGN_START SubscriptionType = "channel.charity_campaign.start"
|
||||||
|
// Version 1 - Sends an event notification when progress is made towards the campaign’s goal or when the broadcaster changes the fundraising goal.
|
||||||
|
const CHARITY_CAMPAIGN_PROGRESS SubscriptionType = "channel.charity_campaign.progress"
|
||||||
|
// Version 1 - Sends an event notification when the broadcaster stops a charity campaign.
|
||||||
|
const CHARITY_CAMPAIGN_STOP SubscriptionType = "channel.charity_campaign.stop"
|
||||||
|
|
||||||
|
// Version 1 - Sends a notification when EventSub disables a shard due to the status of the underlying transport changing.
|
||||||
|
const CONDUIT_SHARD_DISABLED SubscriptionType = "conduit.shard.disabled"
|
||||||
|
|
||||||
|
// Version 1 - An entitlement for a Drop is granted to a user.
|
||||||
|
const DROP_ENTITLEMENT_GRANT SubscriptionType = "drop.entitlement.grant"
|
||||||
|
|
||||||
|
// Version 1 - A Bits transaction occurred for a specified Twitch Extension.
|
||||||
|
const EXTENSION_BITS_TRANSACTION_CREATE SubscriptionType = "extension.bits_transaction.create"
|
||||||
|
|
||||||
|
// Version 1 - Get notified when a broadcaster begins a goal.
|
||||||
|
const GOAL_BEGIN SubscriptionType = "channel.goal.begin"
|
||||||
|
// Version 1 - Get notified when progress (either positive or negative) is made towards a broadcaster’s goal.
|
||||||
|
const GOAL_PROGRESS SubscriptionType = "channel.goal.progress"
|
||||||
|
// Version 1 - Get notified when a broadcaster ends a goal.
|
||||||
|
const GOAL_END SubscriptionType = "channel.goal.end"
|
||||||
|
|
||||||
|
// Version 2 - A Hype Train begins on the specified channel.
|
||||||
|
const HYPE_TRAIN_BEGIN SubscriptionType = "channel.hype_train.begin"
|
||||||
|
// Version 2 - A Hype Train makes progress on the specified channel.
|
||||||
|
const HYPE_TRAIN_PROGRESS SubscriptionType = "channel.hype_train.progress"
|
||||||
|
// Version 2 - A Hype Train ends on the specified channel.
|
||||||
|
const HYPE_TRAIN_END SubscriptionType = "channel.hype_train.end"
|
||||||
|
|
||||||
|
// Version 1 - Sends a notification when the broadcaster activates Shield Mode.
|
||||||
|
const SHIELD_MODE_BEGIN SubscriptionType = "channel.shield_mode.begin"
|
||||||
|
// Version 1 - Sends a notification when the broadcaster deactivates Shield Mode.
|
||||||
|
const SHIELD_MODE_END SubscriptionType = "channel.shield_mode.end"
|
||||||
|
|
||||||
|
// Version 1 - Sends a notification when the specified broadcaster sends a Shoutout.
|
||||||
|
const SHOUTOUT_CREATE SubscriptionType = "channel.shoutout.create"
|
||||||
|
// Version 1 - Sends a notification when the specified broadcaster receives a Shoutout.
|
||||||
|
const SHOUTOUT_RECEIVED SubscriptionType = "channel.shoutout.receive"
|
||||||
|
|
||||||
|
// Version 1 - The specified broadcaster starts a stream.
|
||||||
|
const STREAM_ONLINE SubscriptionType = "stream.online"
|
||||||
|
// Version 1 - The specified broadcaster stops a stream.
|
||||||
|
const STREAM_OFFLINE SubscriptionType = "stream.offline"
|
||||||
|
|
||||||
|
// Version 1 - A user’s authorization has been granted to your client id.
|
||||||
|
const USER_AUTHORIZATION_GRANT SubscriptionType = "user.authorization.grant"
|
||||||
|
// Version 1 - A user’s authorization has been revoked for your client id.
|
||||||
|
const USER_AUTHORIZATION_REVOKE SubscriptionType = "user.authorization.revoke"
|
||||||
|
|
||||||
|
// Version 1 - A user has updated their account.
|
||||||
|
const USER_UPDATE SubscriptionType = "user.update"
|
||||||
|
|
||||||
|
// Version 1 - A user receives a whisper.
|
||||||
|
const WHISPER_RECEIVED SubscriptionType = "user.whisper.message"
|
||||||
14
twitch/pages/chat.html
Normal file
14
twitch/pages/chat.html
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Twitch Chat</title>
|
||||||
|
<link rel="stylesheet" href="/public/fonts/inter/inter.css">
|
||||||
|
<link rel="stylesheet" href="/twitch/public/chat.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="chat"></div>
|
||||||
|
<script type="module" src="/twitch/public/chat.js" defer></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
0
twitch/public/chat.css
Normal file
0
twitch/public/chat.css
Normal file
38
twitch/public/chat.js
Normal file
38
twitch/public/chat.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
const chatContainer = document.getElementById("chat");
|
||||||
|
|
||||||
|
async function sync() {
|
||||||
|
let eventSource;
|
||||||
|
try {
|
||||||
|
eventSource = new EventSource("/twitch/sse/chat");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to connect to event source", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSource.addEventListener("error", () => {
|
||||||
|
eventSource.close();
|
||||||
|
console.error("Connection lost, or an error has occurred.");
|
||||||
|
console.log("Attempting to reconnect...");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
sync();
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.addEventListener("chat", event => {
|
||||||
|
console.log(`[${chat}] ${event.data}`);
|
||||||
|
const messageEl = document.createElement("p");
|
||||||
|
messageEl.innerText = event.data;
|
||||||
|
chatContainer.appendChild(messageEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.addEventListener("open", () => {
|
||||||
|
console.log("Connected.");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function twitchEmoteURL(emoteID) {
|
||||||
|
return `https://static-cdn.jtvnw.net/emoticons/v2/${emoteID}/default/dark/1.0#e=0`
|
||||||
|
}
|
||||||
|
|
||||||
|
sync();
|
||||||
|
|
@ -39,6 +39,26 @@ type (
|
||||||
LatestCheer *twitchLabel
|
LatestCheer *twitchLabel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChatMessageModifier string
|
||||||
|
chatMessage struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
Colour string `json:"colour,omitempty"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
Fragments []api.ChatMessageFragment `json:"fragments,omitempty"`
|
||||||
|
Modifier ChatMessageModifier `json:"modifier,omitempty"`
|
||||||
|
AnnouncementColour string `json:"announcement_colour,omitempty"`
|
||||||
|
}
|
||||||
|
SystemChatMessageType string
|
||||||
|
systemChatMessage struct {
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
Type SystemChatMessageType `json:"type,omitempty"`
|
||||||
|
}
|
||||||
|
deleteChatMessage struct {
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
MessageID string `json:"message_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
ServiceOptions struct {
|
ServiceOptions struct {
|
||||||
Host string
|
Host string
|
||||||
Port int16
|
Port int16
|
||||||
|
|
@ -63,6 +83,13 @@ type (
|
||||||
|
|
||||||
eventSubSession *api.EventSubSession
|
eventSubSession *api.EventSubSession
|
||||||
labels *twitchLabels
|
labels *twitchLabels
|
||||||
|
|
||||||
|
ChatC chan *chatMessage
|
||||||
|
ChatBroadcast broadcast.BroadcastChannel[*chatMessage]
|
||||||
|
SystemChatC chan *systemChatMessage
|
||||||
|
SystemChatBroadcast broadcast.BroadcastChannel[*systemChatMessage]
|
||||||
|
DeleteChatC chan *deleteChatMessage
|
||||||
|
DeleteChatBroadcast broadcast.BroadcastChannel[*deleteChatMessage]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -70,6 +97,20 @@ const (
|
||||||
LABEL_LATEST_FOLLOWER string = "latest-follower"
|
LABEL_LATEST_FOLLOWER string = "latest-follower"
|
||||||
LABEL_LATEST_SUBSCRIBER string = "latest-subscriber"
|
LABEL_LATEST_SUBSCRIBER string = "latest-subscriber"
|
||||||
LABEL_LATEST_CHEER string = "latest-cheer"
|
LABEL_LATEST_CHEER string = "latest-cheer"
|
||||||
|
|
||||||
|
CHAT_MODIFIER_NONE ChatMessageModifier = ""
|
||||||
|
CHAT_MODIFIER_SUBSCRIBE ChatMessageModifier = "subscribe"
|
||||||
|
CHAT_MODIFIER_CHEER ChatMessageModifier = "cheer"
|
||||||
|
CHAT_MODIFIER_HIGHLIGHT ChatMessageModifier = "highlight"
|
||||||
|
CHAT_MODIFIER_ANNOUNCEMENT ChatMessageModifier = "announcement"
|
||||||
|
|
||||||
|
SYSTEM_CHAT_FOLLOW SystemChatMessageType = "follow"
|
||||||
|
SYSTEM_CHAT_SUBSCRIBE SystemChatMessageType = "subscribe"
|
||||||
|
SYSTEM_CHAT_CHEER SystemChatMessageType = "cheer"
|
||||||
|
SYSTEM_CHAT_RAID SystemChatMessageType = "raid"
|
||||||
|
SYSTEM_CHAT_POLL SystemChatMessageType = "poll"
|
||||||
|
SYSTEM_CHAT_SHOUTOUT SystemChatMessageType = "shoutout"
|
||||||
|
SYSTEM_CHAT_POINT_REDEEM SystemChatMessageType = "point-redeem"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DATA_PATH string = path.Join(config.CONFIG_DIR, "twitch")
|
var DATA_PATH string = path.Join(config.CONFIG_DIR, "twitch")
|
||||||
|
|
@ -99,6 +140,16 @@ func New(ctx context.Context, opts ServiceOptions) (*Service, error) {
|
||||||
latestCheerBroadcast := broadcast.NewBroadcastChannel(
|
latestCheerBroadcast := broadcast.NewBroadcastChannel(
|
||||||
ctx, latestCheerC)
|
ctx, latestCheerC)
|
||||||
|
|
||||||
|
chatC := make(chan *chatMessage)
|
||||||
|
chatBroadcast := broadcast.NewBroadcastChannel(
|
||||||
|
ctx, chatC)
|
||||||
|
systemChatC := make(chan *systemChatMessage)
|
||||||
|
systemChatBroadcast := broadcast.NewBroadcastChannel(
|
||||||
|
ctx, systemChatC)
|
||||||
|
deleteChatC := make(chan *deleteChatMessage)
|
||||||
|
deleteChatBroadcast := broadcast.NewBroadcastChannel(
|
||||||
|
ctx, deleteChatC)
|
||||||
|
|
||||||
srv := &Service{
|
srv := &Service{
|
||||||
host: opts.Host,
|
host: opts.Host,
|
||||||
port: opts.Port,
|
port: opts.Port,
|
||||||
|
|
@ -138,6 +189,13 @@ func New(ctx context.Context, opts ServiceOptions) (*Service, error) {
|
||||||
},
|
},
|
||||||
RedirectURL: fmt.Sprintf("http://%s:%d/twitch/auth", opts.Host, opts.Port),
|
RedirectURL: fmt.Sprintf("http://%s:%d/twitch/auth", opts.Host, opts.Port),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ChatC: chatC,
|
||||||
|
ChatBroadcast: chatBroadcast,
|
||||||
|
SystemChatC: systemChatC,
|
||||||
|
SystemChatBroadcast: systemChatBroadcast,
|
||||||
|
DeleteChatC: deleteChatC,
|
||||||
|
DeleteChatBroadcast: deleteChatBroadcast,
|
||||||
}
|
}
|
||||||
|
|
||||||
if authFile, err := os.OpenFile(AUTH_FILEPATH, os.O_RDONLY, 0600); err != nil {
|
if authFile, err := os.OpenFile(AUTH_FILEPATH, os.O_RDONLY, 0600); err != nil {
|
||||||
|
|
@ -203,7 +261,7 @@ func (srv *Service) BindRoutes(group *gin.RouterGroup) {
|
||||||
go srv.start(ctx)
|
go srv.start(ctx)
|
||||||
})
|
})
|
||||||
|
|
||||||
group.GET("/sse/:label", func(ctx *gin.Context) {
|
group.GET("/sse/label/:label", func(ctx *gin.Context) {
|
||||||
ctx.Header("connection", "keep-alive")
|
ctx.Header("connection", "keep-alive")
|
||||||
|
|
||||||
labelParam := ctx.Param("label")
|
labelParam := ctx.Param("label")
|
||||||
|
|
@ -250,6 +308,35 @@ func (srv *Service) BindRoutes(group *gin.RouterGroup) {
|
||||||
|
|
||||||
http.ServeFileFS(ctx.Writer, ctx.Request, pagesFS, "pages/labels.html")
|
http.ServeFileFS(ctx.Writer, ctx.Request, pagesFS, "pages/labels.html")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
group.GET("/sse/chat", func(ctx *gin.Context) {
|
||||||
|
ctx.Header("connection", "keep-alive")
|
||||||
|
|
||||||
|
chatUpdate := srv.ChatBroadcast.Subscribe()
|
||||||
|
defer srv.ChatBroadcast.Cancel(chatUpdate)
|
||||||
|
systemChatUpdate := srv.SystemChatBroadcast.Subscribe()
|
||||||
|
defer srv.SystemChatBroadcast.Cancel(systemChatUpdate)
|
||||||
|
deleteChatUpdate := srv.DeleteChatBroadcast.Subscribe()
|
||||||
|
defer srv.DeleteChatBroadcast.Cancel(deleteChatUpdate)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(10 * time.Millisecond)
|
||||||
|
ctx.Stream(func(w io.Writer) bool {
|
||||||
|
select {
|
||||||
|
case update := <-chatUpdate:
|
||||||
|
ctx.SSEvent("chat", update)
|
||||||
|
case update := <-systemChatUpdate:
|
||||||
|
ctx.SSEvent("system", update)
|
||||||
|
case update := <-deleteChatUpdate:
|
||||||
|
ctx.SSEvent("delete", update)
|
||||||
|
case <-ticker.C:
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
group.GET("/chat", func(ctx *gin.Context) {
|
||||||
|
http.ServeFileFS(ctx.Writer, ctx.Request, pagesFS, "pages/chat.html")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *Service) Run(ctx context.Context) {
|
func (srv *Service) Run(ctx context.Context) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue