Compare commits

...

14 commits

Author SHA1 Message Date
4a52ec3d33
feat(chat): add effect and improve faker
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
2025-09-07 20:22:16 +02:00
68c05816dd
feat(chat): add effect and improve faker
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
2025-09-07 20:14:05 +02:00
e24ce38bb3
update changelog
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
ci/woodpecker/tag/build Pipeline is pending approval
ci/woodpecker/release/build Pipeline is pending approval
2025-09-07 15:50:16 +02:00
e90f1f9921
Merge branch 'feature/websocket' into develop
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
2025-09-07 15:48:35 +02:00
df8995ee9d
doc: add websocket doc 2025-09-07 15:48:33 +02:00
5d16a83723
feat(chat): add websocket to update messages 2025-09-07 15:46:23 +02:00
cad4942e66
feat(chat): add test mode 2025-09-07 15:45:51 +02:00
52e0f16052
update changelog
Some checks are pending
ci/woodpecker/tag/build Pipeline is pending approval
ci/woodpecker/release/build Pipeline is pending approval
ci/woodpecker/push/build Pipeline was successful
2025-08-31 11:27:47 +02:00
689abf2ca7
fix: fix path of assets 2025-08-31 11:26:57 +02:00
f73222a1c9
style: function order
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
2025-08-30 00:31:51 +02:00
52e6deaf9d
remove templ 2025-08-30 00:31:03 +02:00
3b1168ac88
update readme
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
2025-08-29 14:56:49 +02:00
c98d08fd17
update readme
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
2025-08-29 14:56:25 +02:00
8ba80fe469
update readme
Some checks are pending
ci/woodpecker/push/build Pipeline is pending approval
2025-08-29 14:55:29 +02:00
19 changed files with 264 additions and 114 deletions

View file

@ -1,5 +1,18 @@
[Unreleased] [Unreleased]
## v1.1.0 - 2025-09-07
### Added
- feat(chat): use websocket to update messages
- feat(chat): add css animation
## v1.0.1 - 2025-08-31
### Fixed
- fix: fix path of assets
## v1.0.0 - 2025-08-29 ## v1.0.0 - 2025-08-29
First release! 🎉 First release! 🎉

View file

@ -6,6 +6,12 @@ It implements the webhook triggered when a new message is posted in the chat. It
## Installation ## Installation
### Pre-compiled package
Go to [releases](https://gitnet.fr/deblan/owncast-webhook/releases) and download the [latest version](https://gitnet.fr/deblan/owncast-webhook/releases/download/latest/owncast-webhook).
### From source
Requirements: nodejs (tested with >=22), golang (tested with >=1.24) Requirements: nodejs (tested with >=22), golang (tested with >=1.24)
``` ```
@ -33,6 +39,8 @@ Add a webhook (Integration > Webhooks):
You can embed the chat using this URL: `{server.base_url}/chat/messages` You can embed the chat using this URL: `{server.base_url}/chat/messages`
If you use a reverse proxy, configure `{server.base_url}/ws.*` as weboscket.
## Usage ## Usage
``` ```

View file

@ -1,40 +1,51 @@
const isChat = () => { const isChat = () => {
return document.querySelector('#chat') !== null return getChat() !== null
} }
const getChat = () => { const getChat = () => {
return document.querySelector('#chat') return document.querySelector('#chat')
} }
const updateChat = () => { const createWebSocketConnection = () => {
fetch(document.location.href) const protocol = location.protocol === 'https:' ? 'wss' : 'ws'
.then((response) => response.text())
.then((html) => {
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
const nextMessages = doc.querySelectorAll('.message')
const currentMessages = getChat().querySelectorAll('.message') return new WebSocket(`${protocol}://${window.location.hostname}:${window.location.port}/ws/chat/messages`)
const messagesContainer = getChat().querySelector('.messages') }
nextMessages.forEach((nextMessage) => { const isInViewport = (element) => {
let add = true const rect = element.getBoundingClientRect()
currentMessages.forEach((currentMessage) => { return (rect.top - 90) > 0
if (currentMessage.id === nextMessage.id) { }
add = false
const runChat = () => {
const ws = createWebSocketConnection()
ws.addEventListener('close', runChat)
ws.addEventListener('message', (event) => {
const items = JSON.parse(event.data)
if (items == null || !items.length) {
return
} }
const container = getChat().querySelector('.messages')
items.forEach((item) => {
const message = document.createElement('div')
message.innerHTML = item
container.appendChild(message)
}) })
if (add) { getChat().querySelectorAll('.message').forEach((message) => {
messagesContainer.appendChild(nextMessage) if (!isInViewport(message)) {
message.classList.toggle('animate__fadeInUp', false)
message.classList.toggle('animate__fadeOutUp', true)
} }
}) })
messagesContainer.scrollTo(0, messagesContainer.scrollHeight)
window.setTimeout(updateChat, 500)
}) })
} }
export {isChat, updateChat} export {isChat, runChat}

View file

@ -1,6 +1,6 @@
import '../scss/main.scss' import '../scss/main.scss'
import {isChat, updateChat} from './lib/chat.js' import {isChat, runChat} from './lib/chat.js'
if (isChat()) { if (isChat()) {
updateChat() runChat()
} }

View file

@ -1,3 +1,5 @@
@import "~animate.css/animate.css";
:root { :root {
--color-owncast-user-0: #ff717b; --color-owncast-user-0: #ff717b;
--color-owncast-user-1: #f4e413; --color-owncast-user-1: #f4e413;
@ -16,19 +18,24 @@
--theme-color-users-5: var(--color-owncast-user-5); --theme-color-users-5: var(--color-owncast-user-5);
--theme-color-users-6: var(--color-owncast-user-6); --theme-color-users-6: var(--color-owncast-user-6);
--theme-color-users-7: var(--color-owncast-user-7); --theme-color-users-7: var(--color-owncast-user-7);
--animate-duration: 500ms;
} }
#chat { #chat {
background: transparent; background: transparent;
margin: 0; margin: 0;
font-family: monospace; font-family: monospace;
height: calc(100vh - 10px);
/* border: 5px solid #254779; */
.messages { .messages {
max-height: calc(100vh - 20px);
overflow: hidden; overflow: hidden;
font-size: 13px; font-size: 13px;
display: flex;
align-self: end;
flex-direction: column;
position: fixed;
bottom: 10px;
width: 100%;
.message { .message {
background: #000000cc; background: #000000cc;

View file

@ -4,8 +4,6 @@ import (
"embed" "embed"
"encoding/json" "encoding/json"
"github.com/a-h/templ"
"github.com/labstack/echo/v4"
. "maragu.dev/gomponents" . "maragu.dev/gomponents"
. "maragu.dev/gomponents/html" . "maragu.dev/gomponents/html"
) )
@ -17,35 +15,9 @@ var (
entrypoints map[string]map[string]map[string][]string entrypoints map[string]map[string]map[string][]string
) )
func Render(ctx echo.Context, statusCode int, t templ.Component) error {
buf := templ.GetBuffer()
defer templ.ReleaseBuffer(buf)
if err := t.Render(ctx.Request().Context(), buf); err != nil {
return err
}
return ctx.HTML(statusCode, buf.String())
}
func Asset(name string) string {
if manifest == nil {
value, _ := statics.ReadFile("static/manifest.json")
json.Unmarshal(value, &manifest)
}
path, ok := manifest[name]
if !ok {
return ""
}
return path
}
func entrypointFiles(app, category string) []string { func entrypointFiles(app, category string) []string {
if entrypoints == nil { if entrypoints == nil {
value, _ := statics.ReadFile("static/entrypoints.json") value, _ := statics.ReadFile("dist/entrypoints.json")
json.Unmarshal(value, &entrypoints) json.Unmarshal(value, &entrypoints)
} }
@ -64,6 +36,21 @@ func entrypointFiles(app, category string) []string {
return files return files
} }
func Asset(name string) string {
if manifest == nil {
value, _ := statics.ReadFile("dist/manifest.json")
json.Unmarshal(value, &manifest)
}
path, ok := manifest[name]
if !ok {
return ""
}
return path
}
func EntrypointJs(app string) []Node { func EntrypointJs(app string) []Node {
files := entrypointFiles(app, "js") files := entrypointFiles(app, "js")
var results []Node var results []Node

View file

@ -6,6 +6,7 @@ sn=owncastwh
st="Owncast Webhook" st="Owncast Webhook"
while true; do while true; do
screen -X -S "$sn" quit
rm -f cmd/server/rice-box.go rm -f cmd/server/rice-box.go
./node_modules/.bin/webpack ./node_modules/.bin/webpack
screen -S "$sn" -d -m go run ./cmd/server/ screen -S "$sn" -d -m go run ./cmd/server/

View file

@ -14,6 +14,7 @@ import (
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
"gitnet.fr/deblan/owncast-webhook/internal/client/twitch" "gitnet.fr/deblan/owncast-webhook/internal/client/twitch"
"gitnet.fr/deblan/owncast-webhook/internal/config" "gitnet.fr/deblan/owncast-webhook/internal/config"
"gitnet.fr/deblan/owncast-webhook/internal/test"
"gitnet.fr/deblan/owncast-webhook/internal/web/router" "gitnet.fr/deblan/owncast-webhook/internal/web/router"
) )
@ -36,13 +37,17 @@ func main() {
e := echo.New() e := echo.New()
e.Validator = &AppValidator{validator: validator.New()} e.Validator = &AppValidator{validator: validator.New()}
e.Static("/static", "static") e.Static("/dist", "dist")
assetHandler := http.FileServer(rice.MustFindBox("../../assets/dist").HTTPBox()) assetHandler := http.FileServer(rice.MustFindBox("../../assets/dist").HTTPBox())
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler))) e.GET("/dist/*", echo.WrapHandler(http.StripPrefix("/dist/", assetHandler)))
e.Use(middleware.Logger()) e.Use(middleware.Logger())
router.RegisterControllers(e) router.RegisterControllers(e)
if conf.Test.Enable {
test.GenerateFakeMessages()
}
if conf.Twitch.Enable { if conf.Twitch.Enable {
twitch.IrcClient() twitch.IrcClient()
} }

View file

@ -10,3 +10,7 @@ base_url = "https://live.example.com"
[twitch] [twitch]
enable = false enable = false
channel = "username" channel = "username"
; Add fake messages to test
[test]
enable = false

6
go.mod
View file

@ -4,9 +4,9 @@ go 1.23.0
require ( require (
github.com/GeertJohan/go.rice v1.0.3 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/gempir/go-twitch-irc/v4 v4.2.0
github.com/go-playground/validator v9.31.0+incompatible github.com/go-playground/validator v9.31.0+incompatible
github.com/gorilla/websocket v1.5.3
github.com/labstack/echo/v4 v4.12.0 github.com/labstack/echo/v4 v4.12.0
gopkg.in/ini.v1 v1.67.0 gopkg.in/ini.v1 v1.67.0
maragu.dev/gomponents v1.2.0 maragu.dev/gomponents v1.2.0
@ -14,6 +14,7 @@ require (
require ( require (
github.com/daaku/go.zipexe v1.0.2 // indirect github.com/daaku/go.zipexe v1.0.2 // indirect
github.com/go-faker/faker/v4 v4.6.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
@ -24,11 +25,10 @@ require (
github.com/stretchr/testify v1.9.0 // indirect github.com/stretchr/testify v1.9.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // 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/crypto v0.27.0 // indirect
golang.org/x/net v0.28.0 // indirect golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.25.0 // indirect golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
) )

14
go.sum
View file

@ -1,8 +1,6 @@
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI= github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI=
github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4= github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4=
github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM=
github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/daaku/go.zipexe v1.0.2 h1:Zg55YLYTr7M9wjKn8SY/WcpuuEi+kR2u4E8RhvpyXmk= 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/daaku/go.zipexe v1.0.2/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8=
@ -10,6 +8,8 @@ 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/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 h1:OCeff+1aH4CZIOxgKOJ8dQjh+1ppC6sLWrXOcpGZyq4=
github.com/gempir/go-twitch-irc/v4 v4.2.0/go.mod h1:QsOMMAk470uxQ7EYD9GJBGAVqM/jDrXBNbuePfTauzg= github.com/gempir/go-twitch-irc/v4 v4.2.0/go.mod h1:QsOMMAk470uxQ7EYD9GJBGAVqM/jDrXBNbuePfTauzg=
github.com/go-faker/faker/v4 v4.6.1 h1:xUyVpAjEtB04l6XFY0V/29oR332rOSPWV4lU8RwDt4k=
github.com/go-faker/faker/v4 v4.6.1/go.mod h1:arSdxNCSt7mOhdk8tEolvHeIJ7eX4OX80wXjKKvkKBY=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 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/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@ -18,8 +18,8 @@ github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp
github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
@ -42,8 +42,6 @@ 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.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 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/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 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
@ -52,8 +50,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=

View file

@ -16,6 +16,9 @@ type Config struct {
Owncast struct { Owncast struct {
BaseUrl string BaseUrl string
} }
Test struct {
Enable bool
}
Twitch struct { Twitch struct {
Enable bool Enable bool
Channel string Channel string
@ -45,6 +48,7 @@ func (c *Config) Load(file string) {
config.Server.WebhookSecret = cfg.Section("server").Key("webhook_secret").String() config.Server.WebhookSecret = cfg.Section("server").Key("webhook_secret").String()
config.Owncast.BaseUrl = cfg.Section("owncast").Key("base_url").String() config.Owncast.BaseUrl = cfg.Section("owncast").Key("base_url").String()
config.Test.Enable = cfg.Section("test").Key("enable").MustBool(false)
config.Twitch.Enable = cfg.Section("twitch").Key("enable").MustBool(false) config.Twitch.Enable = cfg.Section("twitch").Key("enable").MustBool(false)
config.Twitch.Channel = cfg.Section("twitch").Key("channel").String() config.Twitch.Channel = cfg.Section("twitch").Key("channel").String()

View file

@ -7,6 +7,7 @@ var messageStore *MessageStore
const ( const (
MessageOriginOwncast MessageOrigin = iota MessageOriginOwncast MessageOrigin = iota
MessageOriginTwitch MessageOriginTwitch
MessageOriginTest
) )
type MessageStore struct { type MessageStore struct {
@ -21,6 +22,10 @@ func (s *MessageStore) All() []MessageInterface {
return s.messages return s.messages
} }
func (s *MessageStore) Clear() {
s.messages = []MessageInterface{}
}
func GetMessageStore() *MessageStore { func GetMessageStore() *MessageStore {
if messageStore == nil { if messageStore == nil {
messageStore = new(MessageStore) messageStore = new(MessageStore)

View file

@ -0,0 +1,30 @@
package store
import (
"github.com/gempir/go-twitch-irc/v4"
"github.com/go-faker/faker/v4"
)
type TestMessage struct {
Message twitch.PrivateMessage
}
func (o TestMessage) ID() string {
return faker.Sentence()
}
func (o TestMessage) Visible() bool {
return true
}
func (o TestMessage) Origin() MessageOrigin {
return MessageOriginTest
}
func (o TestMessage) Author() string {
return faker.Username()
}
func (o TestMessage) Content() string {
return faker.Sentence()
}

17
internal/test/fake.go Normal file
View file

@ -0,0 +1,17 @@
package test
import (
"math/rand"
"time"
"gitnet.fr/deblan/owncast-webhook/internal/store"
)
func GenerateFakeMessages() {
go func() {
for {
store.GetMessageStore().Add(store.TestMessage{})
time.Sleep(time.Duration(rand.Intn(3000-100)+100) * time.Millisecond)
}
}()
}

View file

@ -1,8 +1,10 @@
package chat package chat
import ( import (
"fmt" "bytes"
"encoding/json"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"gitnet.fr/deblan/owncast-webhook/assets" "gitnet.fr/deblan/owncast-webhook/assets"
"gitnet.fr/deblan/owncast-webhook/internal/store" "gitnet.fr/deblan/owncast-webhook/internal/store"
@ -11,6 +13,10 @@ import (
. "maragu.dev/gomponents/html" . "maragu.dev/gomponents/html"
) )
var (
upgrader = websocket.Upgrader{}
)
type Controller struct { type Controller struct {
} }
@ -18,64 +24,60 @@ func New(e *echo.Echo) *Controller {
c := Controller{} c := Controller{}
e.GET("/chat/messages", c.Messages) e.GET("/chat/messages", c.Messages)
e.GET("/ws/chat/messages", c.WebsocketMessages)
return &c return &c
} }
func (ctrl *Controller) WebsocketMessages(c echo.Context) error {
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer ws.Close()
storage := store.GetMessageStore()
for {
messages := storage.All()
if len(messages) == 0 {
continue
}
storage.Clear()
var items []string
for _, message := range messages {
var buff bytes.Buffer
msg := CreateMessageView(message)
msg.Render(&buff)
items = append(items, buff.String())
}
j, _ := json.Marshal(items)
ws.WriteMessage(websocket.TextMessage, j)
}
return nil
}
func (ctrl *Controller) Messages(c echo.Context) error { func (ctrl *Controller) Messages(c echo.Context) error {
createMessage := func(message store.MessageInterface) Node {
var containerStyle Node
var userStyle Node
var originIcon Node
switch message.Origin() {
case store.MessageOriginOwncast:
msg := message.(store.OwncastMessage)
containerStyle = StyleAttr(fmt.Sprintf(
"border-color: var(--theme-color-users-%d)",
msg.WebhookMessage.User.DisplayColor,
))
userStyle = StyleAttr(fmt.Sprintf(
"color: var(--theme-color-users-%d)",
msg.WebhookMessage.User.DisplayColor,
))
originIcon = Img(Src(assets.Asset("static/img/owncast.png")), Class("message-origin"))
case store.MessageOriginTwitch:
originIcon = Img(Src(assets.Asset("static/img/twitch.png")), Class("message-origin"))
}
return Div(
Class("message"),
ID(message.ID()),
containerStyle,
Div(
Class("message-user"),
userStyle,
Group([]Node{originIcon, Text(message.Author())}),
),
Div(
Class("message-body"),
Raw(message.Content()),
),
)
}
page := HTML5(HTML5Props{ page := HTML5(HTML5Props{
Title: "Chat", Title: "Chat",
Language: "fr", Language: "fr",
Head: []Node{ Head: []Node{
Group(assets.EntrypointCss("main")), Group(assets.EntrypointCss("main")),
Link(Rel("icon"), Type("image/x-icon"), Href(assets.Asset("static/img/favicon.png"))), Link(Rel("icon"), Type("image/x-icon"), Href(assets.Asset("dist/img/favicon.png"))),
}, },
Body: []Node{ Body: []Node{
ID("chat"), ID("chat"),
Div( Div(Class("messages")),
Class("messages"),
Map(store.GetMessageStore().All(), createMessage),
),
Group(assets.EntrypointJs("main")), Group(assets.EntrypointJs("main")),
}, },
}) })

View file

@ -0,0 +1,50 @@
package chat
import (
"fmt"
"gitnet.fr/deblan/owncast-webhook/assets"
"gitnet.fr/deblan/owncast-webhook/internal/store"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func CreateMessageView(message store.MessageInterface) Node {
var containerStyle Node
var userStyle Node
var originIcon Node
switch message.Origin() {
case store.MessageOriginOwncast:
msg := message.(store.OwncastMessage)
containerStyle = StyleAttr(fmt.Sprintf(
"border-color: var(--theme-color-users-%d)",
msg.WebhookMessage.User.DisplayColor,
))
userStyle = StyleAttr(fmt.Sprintf(
"color: var(--theme-color-users-%d)",
msg.WebhookMessage.User.DisplayColor,
))
originIcon = Img(Src(assets.Asset("dist/img/owncast.png")), Class("message-origin"))
case store.MessageOriginTwitch:
originIcon = Img(Src(assets.Asset("dist/img/twitch.png")), Class("message-origin"))
}
return Div(
Class("message animate__animated animate__fadeInUp"),
ID(message.ID()),
containerStyle,
Div(
Class("message-user"),
userStyle,
Group([]Node{originIcon, Text(message.Author())}),
),
Div(
Class("message-body"),
Raw(message.Content()),
),
)
}

7
package-lock.json generated
View file

@ -6,6 +6,7 @@
"": { "": {
"dependencies": { "dependencies": {
"@symfony/webpack-encore": "github:symfony/webpack-encore", "@symfony/webpack-encore": "github:symfony/webpack-encore",
"animate.css": "^4.1.1",
"raw-loader": "^4.0.2" "raw-loader": "^4.0.2"
}, },
"devDependencies": { "devDependencies": {
@ -2734,6 +2735,12 @@
"ajv": "^6.9.1" "ajv": "^6.9.1"
} }
}, },
"node_modules/animate.css": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
"integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==",
"license": "MIT"
},
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",

View file

@ -1,6 +1,7 @@
{ {
"dependencies": { "dependencies": {
"@symfony/webpack-encore": "github:symfony/webpack-encore", "@symfony/webpack-encore": "github:symfony/webpack-encore",
"animate.css": "^4.1.1",
"raw-loader": "^4.0.2" "raw-loader": "^4.0.2"
}, },
"devDependencies": { "devDependencies": {