Compare commits
7 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
4a52ec3d33 |
|||
|
68c05816dd |
|||
|
e24ce38bb3 |
|||
|
e90f1f9921 |
|||
|
df8995ee9d |
|||
|
5d16a83723 |
|||
|
cad4942e66 |
18 changed files with 233 additions and 74 deletions
|
|
@ -1,5 +1,12 @@
|
||||||
[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
|
## v1.0.1 - 2025-08-31
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
|
|
@ -39,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
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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/
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -43,6 +44,10 @@ func main() {
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -6,6 +6,7 @@ require (
|
||||||
github.com/GeertJohan/go.rice v1.0.3
|
github.com/GeertJohan/go.rice v1.0.3
|
||||||
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
|
||||||
|
|
@ -13,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
|
||||||
|
|
@ -26,7 +28,7 @@ require (
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
8
go.sum
8
go.sum
|
|
@ -8,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=
|
||||||
|
|
@ -16,6 +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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
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=
|
||||||
|
|
@ -46,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=
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
30
internal/store/test_message.go
Normal file
30
internal/store/test_message.go
Normal 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
17
internal/test/fake.go
Normal 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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
@ -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,51 +24,50 @@ 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("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"),
|
|
||||||
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",
|
||||||
|
|
@ -72,10 +77,7 @@ func (ctrl *Controller) Messages(c echo.Context) error {
|
||||||
},
|
},
|
||||||
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")),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
50
internal/web/controller/chat/utils.go
Normal file
50
internal/web/controller/chat/utils.go
Normal 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
7
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue