backend for twitch chat widgets

This commit is contained in:
ari melody 2026-09-02 22:32:45 +01:00
parent 09c11f11a1
commit 28b9492cb3
Signed by: ari
GPG key ID: 60B5F0386E3DDB7E
11 changed files with 725 additions and 94 deletions

View file

@ -38,6 +38,26 @@ type (
LatestSubscriber *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 {
Host string
@ -63,6 +83,13 @@ type (
eventSubSession *api.EventSubSession
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_SUBSCRIBER string = "latest-subscriber"
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")
@ -98,6 +139,16 @@ func New(ctx context.Context, opts ServiceOptions) (*Service, error) {
latestCheerC := make(chan string)
latestCheerBroadcast := broadcast.NewBroadcastChannel(
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{
host: opts.Host,
@ -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),
},
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 {
@ -203,7 +261,7 @@ func (srv *Service) BindRoutes(group *gin.RouterGroup) {
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")
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")
})
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) {