From 1e064ab5e4d36d4ee6e001a23ee9428795007c9b Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Wed, 27 Aug 2025 16:24:20 +0200 Subject: [PATCH] add twitch message handler --- backend/controller/chat/controller.go | 11 ++++++- .../webhook/owncast/chat_message.go | 6 ++-- backend/store/owncast_message.go | 2 +- backend/store/twitch_message.go | 27 ++++++++++++++++ backend/webhook/{ => owncast}/message.go | 0 cmd/server/server.go | 5 +++ config.ini.example | 12 ++++++++ config/config.go | 20 ++++++------ frontend/img/owncast.png | Bin 0 -> 1506 bytes frontend/img/twitch.png | Bin 0 -> 382 bytes frontend/scss/main.scss | 6 ++++ go.mod | 2 ++ go.sum | 4 +++ twitch/irc_client.go | 29 ++++++++++++++++++ 14 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 backend/store/twitch_message.go rename backend/webhook/{ => owncast}/message.go (100%) create mode 100644 config.ini.example create mode 100644 frontend/img/owncast.png create mode 100644 frontend/img/twitch.png create mode 100644 twitch/irc_client.go diff --git a/backend/controller/chat/controller.go b/backend/controller/chat/controller.go index a04e5e8..de12a59 100644 --- a/backend/controller/chat/controller.go +++ b/backend/controller/chat/controller.go @@ -37,8 +37,13 @@ func (ctrl *Controller) Messages(c echo.Context) error { Map(store.GetMessageStore().All(), func(message store.MessageInterface) Node { var containerStyle Node var userStyle Node + var originIcon Node if message.Origin() == store.MessageOriginOwncast { + } + + switch message.Origin() { + case store.MessageOriginOwncast: msg := message.(store.OwncastMessage) containerStyle = StyleAttr(fmt.Sprintf( @@ -50,6 +55,10 @@ func (ctrl *Controller) Messages(c echo.Context) error { "color: var(--theme-color-users-%d)", msg.WebhookMessage.User.DisplayColor, )) + + originIcon = Img(Src(view.Asset("static/img/owncast.png")), Class("message-origin")) + case store.MessageOriginTwitch: + originIcon = Img(Src(view.Asset("static/img/twitch.png")), Class("message-origin")) } return Div( @@ -59,7 +68,7 @@ func (ctrl *Controller) Messages(c echo.Context) error { Div( Class("message-user"), userStyle, - Text(message.Author()), + Group([]Node{originIcon, Text(message.Author())}), ), Div( Class("message-body"), diff --git a/backend/controller/webhook/owncast/chat_message.go b/backend/controller/webhook/owncast/chat_message.go index cb9b7c1..b453f1c 100644 --- a/backend/controller/webhook/owncast/chat_message.go +++ b/backend/controller/webhook/owncast/chat_message.go @@ -1,11 +1,13 @@ package owncast import ( + "fmt" "net/http" "github.com/labstack/echo/v4" "gitnet.fr/deblan/owncast-webhook/backend/store" - "gitnet.fr/deblan/owncast-webhook/backend/webhook" + webhook "gitnet.fr/deblan/owncast-webhook/backend/webhook/owncast" + "gitnet.fr/deblan/owncast-webhook/config" ) type Controller struct { @@ -14,7 +16,7 @@ type Controller struct { func New(e *echo.Echo) *Controller { c := Controller{} - e.POST("/webhook/owncast/chat_message", c.ChatMessage) + e.POST(fmt.Sprintf("/webhook/%s/owncast/chat_message", config.Get().Server.WebhookSecret), c.ChatMessage) return &c } diff --git a/backend/store/owncast_message.go b/backend/store/owncast_message.go index 70a0ce2..0d0cc08 100644 --- a/backend/store/owncast_message.go +++ b/backend/store/owncast_message.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "gitnet.fr/deblan/owncast-webhook/backend/webhook" + webhook "gitnet.fr/deblan/owncast-webhook/backend/webhook/owncast" "gitnet.fr/deblan/owncast-webhook/config" ) diff --git a/backend/store/twitch_message.go b/backend/store/twitch_message.go new file mode 100644 index 0000000..9c53062 --- /dev/null +++ b/backend/store/twitch_message.go @@ -0,0 +1,27 @@ +package store + +import "github.com/gempir/go-twitch-irc/v4" + +type TwitchMessage struct { + Message twitch.PrivateMessage +} + +func (o TwitchMessage) ID() string { + return o.Message.ID +} + +func (o TwitchMessage) Visible() bool { + return true +} + +func (o TwitchMessage) Origin() MessageOrigin { + return MessageOriginTwitch +} + +func (o TwitchMessage) Author() string { + return o.Message.User.DisplayName +} + +func (o TwitchMessage) Content() string { + return o.Message.Message +} diff --git a/backend/webhook/message.go b/backend/webhook/owncast/message.go similarity index 100% rename from backend/webhook/message.go rename to backend/webhook/owncast/message.go diff --git a/cmd/server/server.go b/cmd/server/server.go index 39e6356..485b7d8 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -14,6 +14,7 @@ import ( "github.com/labstack/echo/v4/middleware" "gitnet.fr/deblan/owncast-webhook/backend/router" "gitnet.fr/deblan/owncast-webhook/config" + "gitnet.fr/deblan/owncast-webhook/twitch" ) type TemplateRenderer struct { @@ -42,6 +43,10 @@ func main() { e.Use(middleware.Logger()) router.RegisterControllers(e) + if conf.Twitch.Enable { + twitch.IrcClient() + } + if err := e.Start(fmt.Sprintf("%s:%d", conf.Server.Address, conf.Server.Port)); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatal(err) } diff --git a/config.ini.example b/config.ini.example new file mode 100644 index 0000000..9be2b6d --- /dev/null +++ b/config.ini.example @@ -0,0 +1,12 @@ +[server] +port = 1926 +address = "0.0.0.0" +base_url = "https://api.example.com" +webhook_secret = "86f9ac485ffaf1c24f03e3fd441f65d9" + +[owncast] +base_url = "https://live.example.com" + +[twitch] +enable = false +channel = "username" diff --git a/config/config.go b/config/config.go index 9a65f42..9b020a2 100644 --- a/config/config.go +++ b/config/config.go @@ -8,18 +8,17 @@ import ( type Config struct { Server struct { - BaseUrl string - Address string - Port int + BaseUrl string + Address string + WebhookSecret string + Port int } Owncast struct { BaseUrl string } Twitch struct { - ClientId string - ClientSecret string - WebhookUrl string - WebhookSecret string + Enable bool + Channel string } } @@ -43,11 +42,10 @@ func (c *Config) Load(file string) { config.Server.Address = cfg.Section("server").Key("address").String() config.Server.Port, _ = cfg.Section("server").Key("port").Int() config.Server.BaseUrl = cfg.Section("server").Key("base_url").String() + config.Server.WebhookSecret = cfg.Section("server").Key("webhook_secret").String() config.Owncast.BaseUrl = cfg.Section("owncast").Key("base_url").String() - config.Twitch.ClientId = cfg.Section("twitch").Key("client_id").String() - config.Twitch.ClientSecret = cfg.Section("twitch").Key("client_secret").String() - config.Twitch.WebhookSecret = cfg.Section("twitch").Key("webhook_secret").String() - config.Twitch.WebhookUrl = cfg.Section("twitch").Key("webhook_url").String() + config.Twitch.Enable = cfg.Section("twitch").Key("enable").MustBool(false) + config.Twitch.Channel = cfg.Section("twitch").Key("channel").String() } diff --git a/frontend/img/owncast.png b/frontend/img/owncast.png new file mode 100644 index 0000000000000000000000000000000000000000..a7f1de98c96afc62fa936d752b85a3eb4240d63e GIT binary patch literal 1506 zcmV<81s(c{P)vMfB)3`r)pDEi_CnwPV4H3 z?&G(+(_v292QdVg9D+Sp&R0O7M*8K>gzhM%+Q2!klvoXj05fB>8J8s`9sp8mpl@zU z>8hgH5|QIk!K!p(Fwu;?fDfpFzPKr&E2g)mt}f$lkT{-MyBauy8u<3a=!#S$OM~?Q+F_f!ifCt7o)3?;fcX$J!|J+wa(;KpfdZ&#e={K1@0OYCC?rlB*8SnHe@GliDJ*=0& z0myjMr}ir~@aK`KYzcSmXfitk9KuV7gtJ5DR^rG5}-hZ!JdMZ zF!Ij0gUH(G0^b$~0HQ+MDAF)(1a)a7eE1sTT2tgG(g#2d;0AY_!QzAOjG@ z%FZ}Mkjl6r*PMPy7ir6*#{w`<%QFo8v&KP1{{9tLB26C1I9C@t9R++aurBUj z{(dSFZ`Gffzuk|mf8N9j2F-$@&UnNHoehkq8?@qi^;Da7$8u|BB6m&s&;`oRhciyk z_w>=p-YH(=-~2vLUy|R}u&aPbrU#sH4=|N(^6frQt;caceUaY2(!LT02zGbUfd>76 zhyGZoH_d(Kel6QF@mCejxWd;fG4MbB3k@wr@%nOS+G?E)~Ol9~Ga>rdA4a4FcjK(k?!fZ;$6m>=kc6afoU zj--txH#m;stAG-K&-g4eGv>?J1`DkLOblk9107WPyXfkC9G_xlOPg^vW*=aTH?ssI z16o?>B(N=*p_ioN4f_E248R*kXMcbG`c{#2ju(HJ(V7_jf#;!`3^1S}gz#ENvG{vA zRLKusWQY@52k6BN1Aqmo@XSPqv4zZ>nVoKE55WQe17a-;+7)QH7Ed6XIr2>wRcn4CK&fRSuPS6ESPbblIdKD)eN=_ z#>I=c;_LPD2TD2*=|<;tjO2cbeLQ{fp-c2LvrWM~Rm|+ea=xX)n;D%I((tB9b)a=+ zyaIgFW-Je^EaNm_A%pXR*@(H5)~}}f{s&0fMrdDPH$)D=FhEs@^PQPpnxshs720>t zgQ>;vZ{FPsbPL!H4)TUxH24BIhl*0a`TBYvuNHg1Y#hfcl;7u#{eM?u`TZ*&MK1$? zQAK<1vLChh_lJ)*5_;I+0N{PW)xfp^+Xfg(Cw&)MD)QcEy^o#uQ4uII6WV^8U8u#F z-@jc1@rD^+q-GaI(g_mBo3SVG9NnZ91}{P_ECiQ2cO7t?!EnJq{-2|OJLx7ZQ)2;u z)NBz$R~hUCWPnz|zQBuglTVB;(CEbMIHWg$AwV9p59mJr2R;sL-SVkFpa1{>07*qo IM6N<$f(~`wQUCw| literal 0 HcmV?d00001 diff --git a/frontend/img/twitch.png b/frontend/img/twitch.png new file mode 100644 index 0000000000000000000000000000000000000000..7bb99fa75bb0518fddba960f26f336525b2567a3 GIT binary patch literal 382 zcmV-^0fGLBP)R8AnCz^IL_(cV_EF?Mzw!6R5(u(BA#Bft&}>lX4gJeK79nOO#b z8aA$+M}Q9lOamd*(SZedmH~+J$r+un9_6ncirgfQO?fVW4S6m=Te<3Kv9ERaD+Ab~ z56}m&9$)|_RU?`@y8o*Mw=L`ixF3M<_&JZK?&7Bbl>EL=x&bpV3= zp_B6h=;V%G&I+KFpTvXmumF*jD&j0`b literal 0 HcmV?d00001 diff --git a/frontend/scss/main.scss b/frontend/scss/main.scss index 91ccb26..2bee2f2 100644 --- a/frontend/scss/main.scss +++ b/frontend/scss/main.scss @@ -36,6 +36,12 @@ border-radius: 5px; border: 2px solid #00ced1; padding: 10px; + + .message-origin { + height: 15px; + vertical-align: middle; + margin-right: 4px; + } .message-user { color: #58d63c; diff --git a/go.mod b/go.mod index 0d15abe..5aefb9e 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.0 require ( github.com/GeertJohan/go.rice v1.0.3 github.com/a-h/templ v0.2.778 + github.com/gempir/go-twitch-irc/v4 v4.2.0 github.com/go-playground/validator v9.31.0+incompatible github.com/labstack/echo/v4 v4.12.0 gopkg.in/ini.v1 v1.67.0 @@ -23,6 +24,7 @@ require ( github.com/stretchr/testify v1.9.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/yassinebenaid/godump v0.11.1 // indirect golang.org/x/crypto v0.27.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.25.0 // indirect diff --git a/go.sum b/go.sum index 3a306d4..da1c362 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/daaku/go.zipexe v1.0.2 h1:Zg55YLYTr7M9wjKn8SY/WcpuuEi+kR2u4E8RhvpyXmk github.com/daaku/go.zipexe v1.0.2/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gempir/go-twitch-irc/v4 v4.2.0 h1:OCeff+1aH4CZIOxgKOJ8dQjh+1ppC6sLWrXOcpGZyq4= +github.com/gempir/go-twitch-irc/v4 v4.2.0/go.mod h1:QsOMMAk470uxQ7EYD9GJBGAVqM/jDrXBNbuePfTauzg= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -40,6 +42,8 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/yassinebenaid/godump v0.11.1 h1:SPujx/XaYqGDfmNh7JI3dOyCUVrG0bG2duhO3Eh2EhI= +github.com/yassinebenaid/godump v0.11.1/go.mod h1:dc/0w8wmg6kVIvNGAzbKH1Oa54dXQx8SNKh4dPRyW44= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= diff --git a/twitch/irc_client.go b/twitch/irc_client.go new file mode 100644 index 0000000..ca37952 --- /dev/null +++ b/twitch/irc_client.go @@ -0,0 +1,29 @@ +package twitch + +import ( + "log" + + tw "github.com/gempir/go-twitch-irc/v4" + "gitnet.fr/deblan/owncast-webhook/backend/store" + "gitnet.fr/deblan/owncast-webhook/config" +) + +func IrcClient() { + client := tw.NewAnonymousClient() + + client.OnPrivateMessage(func(message tw.PrivateMessage) { + store.GetMessageStore().Add(store.TwitchMessage{ + Message: message, + }) + }) + + client.Join(config.Get().Twitch.Channel) + + go func() { + err := client.Connect() + + if err != nil { + log.Fatal(err.Error()) + } + }() +}