yt/twitch fixes 2026
This commit is contained in:
@@ -48,7 +48,7 @@ This is the main StreamGoose component. It provides three main functions:
|
|||||||
- providing the unified message queue for the frontend side consumption;
|
- providing the unified message queue for the frontend side consumption;
|
||||||
- serving static files that make up the frontend (`index.html`, `script.js`, `style.css` and images) over the plain HTTP protocol.
|
- serving static files that make up the frontend (`index.html`, `script.js`, `style.css` and images) over the plain HTTP protocol.
|
||||||
|
|
||||||
Additionally, the server binary also launches the backends it knows about and opens a simple GUI window to show the Web content locally, feeding its unified output into the JS side via server-sent events. The latter part became possible thanks to the [webview_go](https://pkg.go.dev/centrifuge.hectabit.org/HectaBit/webview_go) library.
|
Additionally, the server binary also launches the backends it knows about and opens a simple GUI window to show the Web content locally, feeding its unified output into the JS side via server-sent events. The latter part became possible thanks to the [webview_go](https://github.com/webview/webview_go) library.
|
||||||
|
|
||||||
### Backends
|
### Backends
|
||||||
|
|
||||||
|
|||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
name: CI Pipeline
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.image }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
# Go 1.23 was the latest version as of 2024-08-31 but the action output a "warning":
|
||||||
|
# Failed to restore: "/usr/bin/tar" failed with error: The process '/usr/bin/tar' failed with exit code 2
|
||||||
|
# Go 1.22 didn't output the above "warning" so we're using this version.
|
||||||
|
# Go 1.14 made -mod=vendor default.
|
||||||
|
# Go 1.13 is the version set in go.mod.
|
||||||
|
include:
|
||||||
|
- { image: macos-14, go: '1.22' }
|
||||||
|
- { image: macos-14, go: '1.21' }
|
||||||
|
- { image: macos-14, go: '1.20' }
|
||||||
|
# Go 1.19 is the earliest version available for ARM64.
|
||||||
|
- { image: macos-14, go: '1.19' }
|
||||||
|
# macos-12 runner has x86_64 support which is needed for old versions of Go
|
||||||
|
- { image: macos-12, go: '1.18' }
|
||||||
|
- { image: macos-12, go: '1.17' }
|
||||||
|
- { image: macos-12, go: '1.16' }
|
||||||
|
- { image: macos-12, go: '1.15' }
|
||||||
|
- { image: macos-12, go: '1.14' }
|
||||||
|
- { image: macos-12, go: '1.13' }
|
||||||
|
- { image: ubuntu-22.04, go: '1.22', apt: libgtk-4-dev libwebkitgtk-6.0-dev }
|
||||||
|
- { image: ubuntu-22.04, go: '1.22', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
|
||||||
|
- { image: ubuntu-22.04, go: '1.22', apt: libgtk-3-dev libwebkit2gtk-4.0-dev }
|
||||||
|
- { image: ubuntu-22.04, go: '1.21', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
|
||||||
|
- { image: ubuntu-22.04, go: '1.20', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
|
||||||
|
- { image: ubuntu-22.04, go: '1.19', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
|
||||||
|
- { image: ubuntu-22.04, go: '1.18', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
|
||||||
|
- { image: ubuntu-22.04, go: '1.17', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
|
||||||
|
- { image: ubuntu-22.04, go: '1.16', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
|
||||||
|
- { image: ubuntu-22.04, go: '1.15', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
|
||||||
|
- { image: ubuntu-22.04, go: '1.14', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
|
||||||
|
- { image: ubuntu-22.04, go: '1.13', apt: libgtk-3-dev libwebkit2gtk-4.1-dev }
|
||||||
|
- { image: windows-2022, go: '1.22' }
|
||||||
|
- { image: windows-2022, go: '1.21' }
|
||||||
|
# Go 1.20 is the earliest version that is usable in this environment as of 2024-08-31.
|
||||||
|
# Compilation fails with older versions:
|
||||||
|
# [...]/bin/ld.exe: [...]\go-link-3209855222\000008.o: in function `x_cgo_thread_start':
|
||||||
|
# \\_\_\runtime\cgo/gcc_util.c:18: undefined reference to `__imp___iob_func'
|
||||||
|
- { image: windows-2022, go: '1.20' }
|
||||||
|
name: Build (${{ matrix.image }}, go ${{ matrix.go }})
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install dependencies
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.0-dev
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
- name: Build examples
|
||||||
|
run: >
|
||||||
|
go build
|
||||||
|
./examples/basic
|
||||||
|
./examples/bind
|
||||||
|
- name: Run tests
|
||||||
|
run: go test
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
Vendored
+15
@@ -0,0 +1,15 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
### Migrating from v0.1.1 to v0.10.0
|
||||||
|
|
||||||
|
1. `Webview.Open()` has been removed. Use other webview APIs to create a window, open a link and run main UI loop.
|
||||||
|
2. `Webview.Debug()` and `webview.Debugf()` have been removed. Use your favorite logging library to debug webview apps.
|
||||||
|
3. `Webview.Settings` struct has been removed. Title, URL and size are controlled via other API setters and can be updated at any time, not only when webview is created.
|
||||||
|
4. `Webview.Loop()` has been removed. Use `Run()` instead.
|
||||||
|
5. `WebView.Run()`, `WebView.Terminate()`, `WebView.SetTitle()`, `WebView.Dispatch()` stayed the same.
|
||||||
|
6. `WebView.Exit()` has been renamed to `WebView.Destroy()`
|
||||||
|
7. `WebView.SetColor()` and `WebView.SetFullScreen()` have been removed. Use `Window()` to get native window handle and probably write some Cgo code to adjust native window to your taste.
|
||||||
|
8. `Webview.Dialog` has been removed. But it is likely to be brought back as a standalone module.
|
||||||
|
9. `WebView.Eval()` remained the same.
|
||||||
|
10. `WebView.InjectCSS()` has been removed. Use eval to inject style tag with CSS inside.
|
||||||
|
11. `WebView.Bind()` kept the name, but changed the semantics. Only functions can be bound. Not the structs, like in Lorca.
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Serge Zaitsev
|
||||||
|
Copyright (c) 2020 webview
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
Vendored
+50
@@ -0,0 +1,50 @@
|
|||||||
|
# webview_go
|
||||||
|
|
||||||
|
[](https://godoc.org/github.com/webview/webview_go)
|
||||||
|
[](https://goreportcard.com/report/github.com/webview/webview_go)
|
||||||
|
|
||||||
|
Go language binding for the [webview library][webview].
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Versions <= 0.1.1 are available in the [old repository][webview].
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
See [Go package documentation][go-docs] for the Go API documentation, or simply read the source code.
|
||||||
|
|
||||||
|
Start with creating a new directory structure for your project.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir my-project && cd my-project
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a new Go module.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go mod init example.com/app
|
||||||
|
```
|
||||||
|
|
||||||
|
Save one of the example programs into your project directory.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -sSLo main.go "https://raw.githubusercontent.com/webview/webview_go/master/examples/basic/main.go"
|
||||||
|
```
|
||||||
|
|
||||||
|
Install dependencies.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get github.com/webview/webview_go
|
||||||
|
```
|
||||||
|
|
||||||
|
Build the example. On Windows, add `-ldflags="-H windowsgui"` to the command line.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
Calling `Eval()` or `Dispatch()` before `Run()` does not work because the webview instance has only been configured and not yet started.
|
||||||
|
|
||||||
|
[go-docs]: https://pkg.go.dev/github.com/webview/webview_go
|
||||||
|
[webview]: https://github.com/webview/webview
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import webview "github.com/webview/webview_go"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
w := webview.New(false)
|
||||||
|
defer w.Destroy()
|
||||||
|
w.SetTitle("Basic Example")
|
||||||
|
w.SetSize(480, 320, webview.HintNone)
|
||||||
|
w.SetHtml("Thanks for using webview!")
|
||||||
|
w.Run()
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import webview "github.com/webview/webview_go"
|
||||||
|
|
||||||
|
const html = `<button id="increment">Tap me</button>
|
||||||
|
<div>You tapped <span id="count">0</span> time(s).</div>
|
||||||
|
<script>
|
||||||
|
const [incrementElement, countElement] =
|
||||||
|
document.querySelectorAll("#increment, #count");
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
incrementElement.addEventListener("click", () => {
|
||||||
|
window.increment().then(result => {
|
||||||
|
countElement.textContent = result.count;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>`
|
||||||
|
|
||||||
|
type IncrementResult struct {
|
||||||
|
Count uint `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var count uint = 0
|
||||||
|
w := webview.New(false)
|
||||||
|
defer w.Destroy()
|
||||||
|
w.SetTitle("Bind Example")
|
||||||
|
w.SetSize(480, 320, webview.HintNone)
|
||||||
|
|
||||||
|
// A binding that increments a value and immediately returns the new value.
|
||||||
|
w.Bind("increment", func() IncrementResult {
|
||||||
|
count++
|
||||||
|
return IncrementResult{Count: count}
|
||||||
|
})
|
||||||
|
|
||||||
|
w.SetHtml(html)
|
||||||
|
w.Run()
|
||||||
|
}
|
||||||
Vendored
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#include "webview.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
struct binding_context {
|
||||||
|
webview_t w;
|
||||||
|
uintptr_t index;
|
||||||
|
};
|
||||||
|
|
||||||
|
void _webviewDispatchGoCallback(void *);
|
||||||
|
void _webviewBindingGoCallback(webview_t, char *, char *, uintptr_t);
|
||||||
|
|
||||||
|
static void _webview_dispatch_cb(webview_t w, void *arg) {
|
||||||
|
_webviewDispatchGoCallback(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _webview_binding_cb(const char *id, const char *req, void *arg) {
|
||||||
|
struct binding_context *ctx = (struct binding_context *) arg;
|
||||||
|
_webviewBindingGoCallback(ctx->w, (char *)id, (char *)req, ctx->index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CgoWebViewDispatch(webview_t w, uintptr_t arg) {
|
||||||
|
webview_dispatch(w, _webview_dispatch_cb, (void *)arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CgoWebViewBind(webview_t w, const char *name, uintptr_t index) {
|
||||||
|
struct binding_context *ctx = calloc(1, sizeof(struct binding_context));
|
||||||
|
ctx->w = w;
|
||||||
|
ctx->index = index;
|
||||||
|
webview_bind(w, name, _webview_binding_cb, (void *)ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CgoWebViewUnbind(webview_t w, const char *name) {
|
||||||
|
webview_unbind(w, name);
|
||||||
|
}
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
module github.com/webview/webview_go
|
||||||
|
|
||||||
|
go 1.13
|
||||||
Vendored
+27
@@ -0,0 +1,27 @@
|
|||||||
|
Copyright (C) Microsoft Corporation. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* The name of Microsoft Corporation, or the names of its contributors
|
||||||
|
may not be used to endorse or promote products derived from this
|
||||||
|
software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
+23568
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
|||||||
|
// Dummy file to allow vendoring.
|
||||||
|
package vendor
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Dummy file to allow vendoring.
|
||||||
|
package vendor
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1.0.1150.38
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Serge Zaitsev
|
||||||
|
Copyright (c) 2022 Steffen André Langnes
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Dummy file to allow vendoring.
|
||||||
|
package vendor
|
||||||
+3598
File diff suppressed because it is too large
Load Diff
+2
@@ -0,0 +1,2 @@
|
|||||||
|
// Dummy file to allow vendoring.
|
||||||
|
package vendor
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
fb6b17d826041411e6346cd9a785a5ceba7987c4
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
#include "webview.h"
|
||||||
Vendored
+331
@@ -0,0 +1,331 @@
|
|||||||
|
package webview
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -I${SRCDIR}/libs/webview/include
|
||||||
|
#cgo CXXFLAGS: -I${SRCDIR}/libs/webview/include -DWEBVIEW_STATIC
|
||||||
|
|
||||||
|
#cgo linux openbsd freebsd netbsd CXXFLAGS: -DWEBVIEW_GTK -std=c++11
|
||||||
|
#cgo linux openbsd freebsd netbsd LDFLAGS: -ldl
|
||||||
|
#cgo linux openbsd freebsd netbsd pkg-config: gtk+-3.0 webkit2gtk-4.0
|
||||||
|
|
||||||
|
#cgo darwin CXXFLAGS: -DWEBVIEW_COCOA -std=c++11
|
||||||
|
#cgo darwin LDFLAGS: -framework WebKit -ldl
|
||||||
|
|
||||||
|
#cgo windows CXXFLAGS: -DWEBVIEW_EDGE -std=c++14 -I${SRCDIR}/libs/mswebview2/include
|
||||||
|
#cgo windows LDFLAGS: -static -ladvapi32 -lole32 -lshell32 -lshlwapi -luser32 -lversion
|
||||||
|
|
||||||
|
#include "webview.h"
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
void CgoWebViewDispatch(webview_t w, uintptr_t arg);
|
||||||
|
void CgoWebViewBind(webview_t w, const char *name, uintptr_t index);
|
||||||
|
void CgoWebViewUnbind(webview_t w, const char *name);
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
_ "github.com/webview/webview_go/libs/mswebview2"
|
||||||
|
_ "github.com/webview/webview_go/libs/mswebview2/include"
|
||||||
|
_ "github.com/webview/webview_go/libs/webview"
|
||||||
|
_ "github.com/webview/webview_go/libs/webview/include"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Ensure that main.main is called from the main thread
|
||||||
|
runtime.LockOSThread()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hints are used to configure window sizing and resizing
|
||||||
|
type Hint int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Width and height are default size
|
||||||
|
HintNone = C.WEBVIEW_HINT_NONE
|
||||||
|
|
||||||
|
// Window size can not be changed by a user
|
||||||
|
HintFixed = C.WEBVIEW_HINT_FIXED
|
||||||
|
|
||||||
|
// Width and height are minimum bounds
|
||||||
|
HintMin = C.WEBVIEW_HINT_MIN
|
||||||
|
|
||||||
|
// Width and height are maximum bounds
|
||||||
|
HintMax = C.WEBVIEW_HINT_MAX
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebView interface {
|
||||||
|
|
||||||
|
// Run runs the main loop until it's terminated. After this function exits -
|
||||||
|
// you must destroy the webview.
|
||||||
|
Run()
|
||||||
|
|
||||||
|
// Terminate stops the main loop. It is safe to call this function from
|
||||||
|
// a background thread.
|
||||||
|
Terminate()
|
||||||
|
|
||||||
|
// Dispatch posts a function to be executed on the main thread. You normally
|
||||||
|
// do not need to call this function, unless you want to tweak the native
|
||||||
|
// window.
|
||||||
|
Dispatch(f func())
|
||||||
|
|
||||||
|
// Destroy destroys a webview and closes the native window.
|
||||||
|
Destroy()
|
||||||
|
|
||||||
|
// Window returns a native window handle pointer. When using GTK backend the
|
||||||
|
// pointer is GtkWindow pointer, when using Cocoa backend the pointer is
|
||||||
|
// NSWindow pointer, when using Win32 backend the pointer is HWND pointer.
|
||||||
|
Window() unsafe.Pointer
|
||||||
|
|
||||||
|
// SetTitle updates the title of the native window. Must be called from the UI
|
||||||
|
// thread.
|
||||||
|
SetTitle(title string)
|
||||||
|
|
||||||
|
// SetSize updates native window size. See Hint constants.
|
||||||
|
SetSize(w int, h int, hint Hint)
|
||||||
|
|
||||||
|
// Navigate navigates webview to the given URL. URL may be a properly encoded data.
|
||||||
|
// URI. Examples:
|
||||||
|
// w.Navigate("https://github.com/webview/webview")
|
||||||
|
// w.Navigate("data:text/html,%3Ch1%3EHello%3C%2Fh1%3E")
|
||||||
|
// w.Navigate("data:text/html;base64,PGgxPkhlbGxvPC9oMT4=")
|
||||||
|
Navigate(url string)
|
||||||
|
|
||||||
|
// SetHtml sets the webview HTML directly.
|
||||||
|
// Example: w.SetHtml(w, "<h1>Hello</h1>");
|
||||||
|
SetHtml(html string)
|
||||||
|
|
||||||
|
// Init injects JavaScript code at the initialization of the new page. Every
|
||||||
|
// time the webview will open a the new page - this initialization code will
|
||||||
|
// be executed. It is guaranteed that code is executed before window.onload.
|
||||||
|
Init(js string)
|
||||||
|
|
||||||
|
// Eval evaluates arbitrary JavaScript code. Evaluation happens asynchronously,
|
||||||
|
// also the result of the expression is ignored. Use RPC bindings if you want
|
||||||
|
// to receive notifications about the results of the evaluation.
|
||||||
|
Eval(js string)
|
||||||
|
|
||||||
|
// Bind binds a callback function so that it will appear under the given name
|
||||||
|
// as a global JavaScript function. Internally it uses webview_init().
|
||||||
|
// Callback receives a request string and a user-provided argument pointer.
|
||||||
|
// Request string is a JSON array of all the arguments passed to the
|
||||||
|
// JavaScript function.
|
||||||
|
//
|
||||||
|
// f must be a function
|
||||||
|
// f must return either value and error or just error
|
||||||
|
Bind(name string, f interface{}) error
|
||||||
|
|
||||||
|
// Removes a callback that was previously set by Bind.
|
||||||
|
Unbind(name string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type webview struct {
|
||||||
|
w C.webview_t
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
m sync.Mutex
|
||||||
|
index uintptr
|
||||||
|
dispatch = map[uintptr]func(){}
|
||||||
|
bindings = map[uintptr]func(id, req string) (interface{}, error){}
|
||||||
|
)
|
||||||
|
|
||||||
|
func boolToInt(b bool) C.int {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// New calls NewWindow to create a new window and a new webview instance. If debug
|
||||||
|
// is non-zero - developer tools will be enabled (if the platform supports them).
|
||||||
|
func New(debug bool) WebView { return NewWindow(debug, nil) }
|
||||||
|
|
||||||
|
// NewWindow creates a new webview instance. If debug is non-zero - developer
|
||||||
|
// tools will be enabled (if the platform supports them). Window parameter can be
|
||||||
|
// a pointer to the native window handle. If it's non-null - then child WebView is
|
||||||
|
// embedded into the given parent window. Otherwise a new window is created.
|
||||||
|
// Depending on the platform, a GtkWindow, NSWindow or HWND pointer can be passed
|
||||||
|
// here.
|
||||||
|
func NewWindow(debug bool, window unsafe.Pointer) WebView {
|
||||||
|
w := &webview{}
|
||||||
|
w.w = C.webview_create(boolToInt(debug), window)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Destroy() {
|
||||||
|
C.webview_destroy(w.w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Run() {
|
||||||
|
C.webview_run(w.w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Terminate() {
|
||||||
|
C.webview_terminate(w.w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Window() unsafe.Pointer {
|
||||||
|
return C.webview_get_window(w.w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Navigate(url string) {
|
||||||
|
s := C.CString(url)
|
||||||
|
defer C.free(unsafe.Pointer(s))
|
||||||
|
C.webview_navigate(w.w, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) SetHtml(html string) {
|
||||||
|
s := C.CString(html)
|
||||||
|
defer C.free(unsafe.Pointer(s))
|
||||||
|
C.webview_set_html(w.w, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) SetTitle(title string) {
|
||||||
|
s := C.CString(title)
|
||||||
|
defer C.free(unsafe.Pointer(s))
|
||||||
|
C.webview_set_title(w.w, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) SetSize(width int, height int, hint Hint) {
|
||||||
|
C.webview_set_size(w.w, C.int(width), C.int(height), C.webview_hint_t(hint))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Init(js string) {
|
||||||
|
s := C.CString(js)
|
||||||
|
defer C.free(unsafe.Pointer(s))
|
||||||
|
C.webview_init(w.w, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Eval(js string) {
|
||||||
|
s := C.CString(js)
|
||||||
|
defer C.free(unsafe.Pointer(s))
|
||||||
|
C.webview_eval(w.w, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Dispatch(f func()) {
|
||||||
|
m.Lock()
|
||||||
|
for ; dispatch[index] != nil; index++ {
|
||||||
|
}
|
||||||
|
dispatch[index] = f
|
||||||
|
m.Unlock()
|
||||||
|
C.CgoWebViewDispatch(w.w, C.uintptr_t(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
//export _webviewDispatchGoCallback
|
||||||
|
func _webviewDispatchGoCallback(index unsafe.Pointer) {
|
||||||
|
m.Lock()
|
||||||
|
f := dispatch[uintptr(index)]
|
||||||
|
delete(dispatch, uintptr(index))
|
||||||
|
m.Unlock()
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|
||||||
|
//export _webviewBindingGoCallback
|
||||||
|
func _webviewBindingGoCallback(w C.webview_t, id *C.char, req *C.char, index uintptr) {
|
||||||
|
m.Lock()
|
||||||
|
f := bindings[uintptr(index)]
|
||||||
|
m.Unlock()
|
||||||
|
jsString := func(v interface{}) string { b, _ := json.Marshal(v); return string(b) }
|
||||||
|
status, result := 0, ""
|
||||||
|
if res, err := f(C.GoString(id), C.GoString(req)); err != nil {
|
||||||
|
status = -1
|
||||||
|
result = jsString(err.Error())
|
||||||
|
} else if b, err := json.Marshal(res); err != nil {
|
||||||
|
status = -1
|
||||||
|
result = jsString(err.Error())
|
||||||
|
} else {
|
||||||
|
status = 0
|
||||||
|
result = string(b)
|
||||||
|
}
|
||||||
|
s := C.CString(result)
|
||||||
|
defer C.free(unsafe.Pointer(s))
|
||||||
|
C.webview_return(w, id, C.int(status), s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Bind(name string, f interface{}) error {
|
||||||
|
v := reflect.ValueOf(f)
|
||||||
|
// f must be a function
|
||||||
|
if v.Kind() != reflect.Func {
|
||||||
|
return errors.New("only functions can be bound")
|
||||||
|
}
|
||||||
|
// f must return either value and error or just error
|
||||||
|
if n := v.Type().NumOut(); n > 2 {
|
||||||
|
return errors.New("function may only return a value or a value+error")
|
||||||
|
}
|
||||||
|
|
||||||
|
binding := func(id, req string) (interface{}, error) {
|
||||||
|
raw := []json.RawMessage{}
|
||||||
|
if err := json.Unmarshal([]byte(req), &raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
isVariadic := v.Type().IsVariadic()
|
||||||
|
numIn := v.Type().NumIn()
|
||||||
|
if (isVariadic && len(raw) < numIn-1) || (!isVariadic && len(raw) != numIn) {
|
||||||
|
return nil, errors.New("function arguments mismatch")
|
||||||
|
}
|
||||||
|
args := []reflect.Value{}
|
||||||
|
for i := range raw {
|
||||||
|
var arg reflect.Value
|
||||||
|
if isVariadic && i >= numIn-1 {
|
||||||
|
arg = reflect.New(v.Type().In(numIn - 1).Elem())
|
||||||
|
} else {
|
||||||
|
arg = reflect.New(v.Type().In(i))
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw[i], arg.Interface()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args = append(args, arg.Elem())
|
||||||
|
}
|
||||||
|
errorType := reflect.TypeOf((*error)(nil)).Elem()
|
||||||
|
res := v.Call(args)
|
||||||
|
switch len(res) {
|
||||||
|
case 0:
|
||||||
|
// No results from the function, just return nil
|
||||||
|
return nil, nil
|
||||||
|
case 1:
|
||||||
|
// One result may be a value, or an error
|
||||||
|
if res[0].Type().Implements(errorType) {
|
||||||
|
if res[0].Interface() != nil {
|
||||||
|
return nil, res[0].Interface().(error)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return res[0].Interface(), nil
|
||||||
|
case 2:
|
||||||
|
// Two results: first one is value, second is error
|
||||||
|
if !res[1].Type().Implements(errorType) {
|
||||||
|
return nil, errors.New("second return value must be an error")
|
||||||
|
}
|
||||||
|
if res[1].Interface() == nil {
|
||||||
|
return res[0].Interface(), nil
|
||||||
|
}
|
||||||
|
return res[0].Interface(), res[1].Interface().(error)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unexpected number of return values")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
for ; bindings[index] != nil; index++ {
|
||||||
|
}
|
||||||
|
bindings[index] = binding
|
||||||
|
m.Unlock()
|
||||||
|
cname := C.CString(name)
|
||||||
|
defer C.free(unsafe.Pointer(cname))
|
||||||
|
C.CgoWebViewBind(w.w, cname, C.uintptr_t(index))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *webview) Unbind(name string) error {
|
||||||
|
cname := C.CString(name)
|
||||||
|
defer C.free(unsafe.Pointer(cname))
|
||||||
|
C.CgoWebViewUnbind(w.w, cname)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Vendored
+50
@@ -0,0 +1,50 @@
|
|||||||
|
package webview
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example() {
|
||||||
|
w := New(true)
|
||||||
|
defer w.Destroy()
|
||||||
|
w.SetTitle("Hello")
|
||||||
|
w.Bind("noop", func() string {
|
||||||
|
log.Println("hello")
|
||||||
|
return "hello"
|
||||||
|
})
|
||||||
|
w.Bind("add", func(a, b int) int {
|
||||||
|
return a + b
|
||||||
|
})
|
||||||
|
w.Bind("quit", func() {
|
||||||
|
w.Terminate()
|
||||||
|
})
|
||||||
|
w.SetHtml(`<!doctype html>
|
||||||
|
<html>
|
||||||
|
<body>hello</body>
|
||||||
|
<script>
|
||||||
|
window.onload = function() {
|
||||||
|
document.body.innerText = ` + "`hello, ${navigator.userAgent}`" + `;
|
||||||
|
noop().then(function(res) {
|
||||||
|
console.log('noop res', res);
|
||||||
|
add(1, 2).then(function(res) {
|
||||||
|
console.log('add res', res);
|
||||||
|
quit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
)`)
|
||||||
|
w.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
flag.Parse()
|
||||||
|
if testing.Verbose() {
|
||||||
|
Example()
|
||||||
|
}
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
+3
-1
@@ -5,9 +5,9 @@ go 1.23.1
|
|||||||
replace github.com/pajlada/go-twitch-pubsub => github.com/plugnburn/go-twitch-pubsub v0.0.0-20240922200010-4bf88de12fae
|
replace github.com/pajlada/go-twitch-pubsub => github.com/plugnburn/go-twitch-pubsub v0.0.0-20240922200010-4bf88de12fae
|
||||||
|
|
||||||
require (
|
require (
|
||||||
centrifuge.hectabit.org/HectaBit/webview_go v0.0.0-20240502074857-17ae24882d16
|
|
||||||
github.com/DaRealFreak/cloudflare-bp-go v1.0.4
|
github.com/DaRealFreak/cloudflare-bp-go v1.0.4
|
||||||
github.com/plugnburn/go-twitch-pubsub v0.0.0-20240922200010-4bf88de12fae
|
github.com/plugnburn/go-twitch-pubsub v0.0.0-20240922200010-4bf88de12fae
|
||||||
|
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -24,3 +24,5 @@ require (
|
|||||||
golang.org/x/tools v0.22.0 // indirect
|
golang.org/x/tools v0.22.0 // indirect
|
||||||
honnef.co/go/tools v0.4.7 // indirect
|
honnef.co/go/tools v0.4.7 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/webview/webview_go => ../external/webview_go
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
centrifuge.hectabit.org/HectaBit/webview_go v0.0.0-20240502074857-17ae24882d16 h1:gOFnYZ+G9R7Prc3Mn7z/v+LkPf/kQ2oUh5NCQd6n39k=
|
|
||||||
centrifuge.hectabit.org/HectaBit/webview_go v0.0.0-20240502074857-17ae24882d16/go.mod h1:cAr/pT090G1kcy9uuCEUbZyGhUQRGOo4KVnkocx8BMA=
|
|
||||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/DaRealFreak/cloudflare-bp-go v1.0.4 h1:33X8Z0YMV1DEVvL/kYLku+rjb4wF712+VIh3xBoifQ0=
|
github.com/DaRealFreak/cloudflare-bp-go v1.0.4 h1:33X8Z0YMV1DEVvL/kYLku+rjb4wF712+VIh3xBoifQ0=
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
//go:build gui && linux
|
||||||
|
/*
|
||||||
|
StreamGoose UI — Go reference implementation for Linux (GTK4 + WebKitGTK 6.0)
|
||||||
|
|
||||||
|
Created by Luxferre in 2024, updated in 2026, released into public domain
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo pkg-config: gtk4 webkitgtk-6.0
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
#include <webkit/webkit.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
static void activate(GtkApplication *app, gpointer user_data) {
|
||||||
|
const char *url = (const char *)user_data;
|
||||||
|
GtkWidget *window = gtk_application_window_new(app);
|
||||||
|
gtk_window_set_title(GTK_WINDOW(window), "StreamGoose");
|
||||||
|
gtk_window_set_default_size(GTK_WINDOW(window), 320, 720);
|
||||||
|
|
||||||
|
GtkWidget *webview = webkit_web_view_new();
|
||||||
|
webkit_web_view_load_uri(WEBKIT_WEB_VIEW(webview), url);
|
||||||
|
|
||||||
|
gtk_window_set_child(GTK_WINDOW(window), webview);
|
||||||
|
gtk_window_present(GTK_WINDOW(window));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void run_webview(const char *url) {
|
||||||
|
GtkApplication *app = gtk_application_new("org.luxferre.streamgoose", G_APPLICATION_DEFAULT_FLAGS);
|
||||||
|
g_signal_connect(app, "activate", G_CALLBACK(activate), (gpointer)url);
|
||||||
|
g_application_run(G_APPLICATION(app), 0, NULL);
|
||||||
|
g_object_unref(app);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* override GUI entry point */
|
||||||
|
func init() {
|
||||||
|
goose_start_gui = func(config map[string]interface{}) {
|
||||||
|
/* target server listening address */
|
||||||
|
target_addr := fmt.Sprintf("%s:%d", config["server_listen_ip"].(string), int(config["server_port"].(float64)))
|
||||||
|
url := "http://" + target_addr
|
||||||
|
|
||||||
|
curl := C.CString(url)
|
||||||
|
defer C.free(unsafe.Pointer(curl))
|
||||||
|
|
||||||
|
C.run_webview(curl)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build gui
|
//go:build gui && !linux
|
||||||
/*
|
/*
|
||||||
StreamGoose UI — Go reference implementation
|
StreamGoose UI — Go reference implementation
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"centrifuge.hectabit.org/HectaBit/webview_go" /* for UI launch */
|
"github.com/webview/webview_go" /* for UI launch */
|
||||||
)
|
)
|
||||||
|
|
||||||
/* override GUI entry point */
|
/* override GUI entry point */
|
||||||
+1
-1
@@ -109,7 +109,7 @@ func kk_process_message(rawmsg map[string]interface{}) {
|
|||||||
out_message := ChatMessage {
|
out_message := ChatMessage {
|
||||||
Type: "kk",
|
Type: "kk",
|
||||||
NativeId: rawmsg["id"].(string),
|
NativeId: rawmsg["id"].(string),
|
||||||
AuthorId: string(int64(author["id"].(float64))),
|
AuthorId: fmt.Sprintf("%d", int64(author["id"].(float64))),
|
||||||
Username: author["username"].(string),
|
Username: author["username"].(string),
|
||||||
Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"),
|
Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"),
|
||||||
Text: rawmsg["content"].(string),
|
Text: rawmsg["content"].(string),
|
||||||
|
|||||||
+111
-22
@@ -19,10 +19,13 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"bufio"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
/* Twitch badge URL cache */
|
/* Twitch badge URL cache */
|
||||||
var TW_BADGE_CACHE map[string]string
|
var TW_BADGE_CACHE map[string]string
|
||||||
|
var TW_USER_ICON_CACHE sync.Map
|
||||||
/* Twitch OAuth global cache */
|
/* Twitch OAuth global cache */
|
||||||
var TW_OAUTH_TOKEN string
|
var TW_OAUTH_TOKEN string
|
||||||
var TW_CLIENT_ID string
|
var TW_CLIENT_ID string
|
||||||
@@ -70,11 +73,36 @@ func tw_api_req(url string, data string) []byte {
|
|||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* User icon fetching and caching function */
|
||||||
|
func tw_get_user_icon(username string) string {
|
||||||
|
if icon, ok := TW_USER_ICON_CACHE.Load(username); ok {
|
||||||
|
return icon.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback to decapi.me due to Helix API 401 Token Scoping limitations on generic twitchapps auths */
|
||||||
|
resp, err := http.Get("https://decapi.me/twitch/avatar/" + username)
|
||||||
|
if err == nil {
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err == nil {
|
||||||
|
url := strings.TrimSpace(string(body))
|
||||||
|
if strings.HasPrefix(url, "https://") {
|
||||||
|
TW_USER_ICON_CACHE.Store(username, url)
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
TW_USER_ICON_CACHE.Store(username, "")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
/* badge cache population function */
|
/* badge cache population function */
|
||||||
func tw_fetch_badge_cache(channelname string) map[string]string {
|
func tw_fetch_badge_cache(channelname string) map[string]string {
|
||||||
readycache := make(map[string]string)
|
readycache := make(map[string]string)
|
||||||
var ustruct map[string]interface{}
|
var ustruct map[string]interface{}
|
||||||
/* get broadcaster ID from the channel name */
|
|
||||||
|
/* get broadcaster ID from the channel name via Helix API */
|
||||||
body := tw_api_req("/users?login=" + channelname, "")
|
body := tw_api_req("/users?login=" + channelname, "")
|
||||||
if len(body) > 0 {
|
if len(body) > 0 {
|
||||||
err := json.Unmarshal(body, &ustruct)
|
err := json.Unmarshal(body, &ustruct)
|
||||||
@@ -85,6 +113,7 @@ func tw_fetch_badge_cache(channelname string) map[string]string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* get the global badges */
|
/* get the global badges */
|
||||||
body = tw_api_req("/chat/badges/global", "")
|
body = tw_api_req("/chat/badges/global", "")
|
||||||
if len(body) > 0 {
|
if len(body) > 0 {
|
||||||
@@ -102,6 +131,7 @@ func tw_fetch_badge_cache(channelname string) map[string]string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if TW_BROADCASTER_ID != "" { /* also get the channel-specific badges */
|
if TW_BROADCASTER_ID != "" { /* also get the channel-specific badges */
|
||||||
body = tw_api_req(fmt.Sprintf("/chat/badges?broadcaster_id=%s", TW_BROADCASTER_ID), "")
|
body = tw_api_req(fmt.Sprintf("/chat/badges?broadcaster_id=%s", TW_BROADCASTER_ID), "")
|
||||||
if len(body) > 0 {
|
if len(body) > 0 {
|
||||||
@@ -153,42 +183,56 @@ func tw_handle_emotes(text string, emote_data string) string {
|
|||||||
|
|
||||||
/* main message procesing function */
|
/* main message procesing function */
|
||||||
func tw_process_message(rawmsg string) {
|
func tw_process_message(rawmsg string) {
|
||||||
|
if !strings.HasPrefix(rawmsg, "@") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
parts := strings.Split(rawmsg, " :") /* get raw message parts */
|
parts := strings.Split(rawmsg, " :") /* get raw message parts */
|
||||||
/* first part is metadata, second is a full IRC username,
|
/* first part is metadata, second is a full IRC sender+command, the rest is message */
|
||||||
the rest is the message ending with \r\n */
|
metadata_str := strings.TrimSpace(parts[0][1:]) // remove the leading '@'
|
||||||
metadata_str := strings.TrimSpace(parts[0])
|
|
||||||
msg_text := ""
|
msg_text := ""
|
||||||
if(len(parts) > 2) {
|
if(len(parts) > 2) {
|
||||||
msg_text = strings.Join(parts[2:], " :")
|
msg_text = strings.Join(parts[2:], " :")
|
||||||
msg_text = strings.ReplaceAll(msg_text, "\u0000", "") /* remove excess null bytes */
|
msg_text = strings.ReplaceAll(msg_text, "\u0000", "") /* remove excess null bytes */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
command := ""
|
||||||
|
if len(parts) > 1 {
|
||||||
|
subparts := strings.Fields(parts[1])
|
||||||
|
if len(subparts) > 1 {
|
||||||
|
command = subparts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
metadata := make(map[string]string) /* store parsed metadata here */
|
metadata := make(map[string]string) /* store parsed metadata here */
|
||||||
parts = strings.Split(metadata_str, ";")
|
mparts := strings.Split(metadata_str, ";")
|
||||||
for _, field := range parts { /* iterate over metadata fields */
|
for _, field := range mparts { /* iterate over metadata fields */
|
||||||
if strings.Contains(field, "=") {
|
if strings.Contains(field, "=") {
|
||||||
fieldkey, fieldval, _ := strings.Cut(field, "=")
|
fieldkey, fieldval, _ := strings.Cut(field, "=")
|
||||||
metadata[fieldkey] = fieldval
|
metadata[fieldkey] = fieldval
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* if the message came from a real user, this is the one we should process */
|
|
||||||
/* but first, check if this is a single-message deletion event */
|
if command == "CLEARMSG" {
|
||||||
if strings.HasPrefix(rawmsg, "@login=") && strings.Contains(rawmsg, " CLEARMSG ") {
|
|
||||||
/* prepare output message */
|
/* prepare output message */
|
||||||
out_message := ChatMessage {
|
out_message := ChatMessage {
|
||||||
Type: "delmsg",
|
Type: "delmsg",
|
||||||
NativeId: metadata["target-msg-id"],
|
NativeId: metadata["target-msg-id"],
|
||||||
}
|
}
|
||||||
/* send it to the message broker via the message_transport channel */
|
|
||||||
message_transport <- out_message
|
message_transport <- out_message
|
||||||
} else if strings.Contains(rawmsg, " CLEARCHAT ") { /* bulk message deletion event */
|
} else if command == "CLEARCHAT" {
|
||||||
/* prepare output message */
|
|
||||||
out_message := ChatMessage {
|
out_message := ChatMessage {
|
||||||
Type: "deluser",
|
Type: "deluser",
|
||||||
AuthorId: metadata["target-user-id"],
|
AuthorId: metadata["target-user-id"],
|
||||||
}
|
}
|
||||||
/* send it to the message broker via the message_transport channel */
|
|
||||||
message_transport <- out_message
|
message_transport <- out_message
|
||||||
} else if strings.HasPrefix(rawmsg, "@badge-info=") { /* normal message or usernotice */
|
} else if command == "PRIVMSG" || command == "USERNOTICE" {
|
||||||
|
/* normal message or usernotice */
|
||||||
|
/* Check if it's a valid message by confirming we have a message ID */
|
||||||
|
if metadata["id"] == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
/* convert the timestamp */
|
/* convert the timestamp */
|
||||||
ts, err := strconv.ParseInt(metadata["tmi-sent-ts"], 10, 0)
|
ts, err := strconv.ParseInt(metadata["tmi-sent-ts"], 10, 0)
|
||||||
if err != nil { /* fallback to current time if anything */
|
if err != nil { /* fallback to current time if anything */
|
||||||
@@ -216,6 +260,19 @@ func tw_process_message(rawmsg string) {
|
|||||||
Icon: "",
|
Icon: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback if display-name is empty
|
||||||
|
if out_message.Username == "" {
|
||||||
|
if len(parts) > 1 {
|
||||||
|
headerParts := strings.Split(parts[1], " ")
|
||||||
|
if len(headerParts) > 0 {
|
||||||
|
senderSplits := strings.Split(headerParts[0], "!")
|
||||||
|
if len(senderSplits) > 0 {
|
||||||
|
out_message.Username = senderSplits[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* process username badges */
|
/* process username badges */
|
||||||
if _, ok := metadata["badges"]; ok {
|
if _, ok := metadata["badges"]; ok {
|
||||||
badges := strings.Split(metadata["badges"], ",") /* they are comma-delimited */
|
badges := strings.Split(metadata["badges"], ",") /* they are comma-delimited */
|
||||||
@@ -250,10 +307,23 @@ func tw_process_message(rawmsg string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* fetch user icons asynchronously so we don't hold up IRC parser speed */
|
||||||
|
if out_message.Username != "" {
|
||||||
|
if icon, ok := TW_USER_ICON_CACHE.Load(out_message.Username); ok {
|
||||||
|
out_message.Icon = icon.(string)
|
||||||
|
message_transport <- out_message
|
||||||
|
} else {
|
||||||
|
go func(msg ChatMessage) {
|
||||||
|
msg.Icon = tw_get_user_icon(msg.Username)
|
||||||
|
message_transport <- msg
|
||||||
|
}(out_message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
/* finally, send it to the message broker via the message_transport channel */
|
/* finally, send it to the message broker via the message_transport channel */
|
||||||
message_transport <- out_message
|
message_transport <- out_message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* entry point */
|
/* entry point */
|
||||||
func twitch_run(config map[string]interface{}) {
|
func twitch_run(config map[string]interface{}) {
|
||||||
@@ -283,6 +353,21 @@ func twitch_run(config map[string]interface{}) {
|
|||||||
TW_CLIENT_ID = strings.TrimSpace(string(cid))
|
TW_CLIENT_ID = strings.TrimSpace(string(cid))
|
||||||
TW_OAUTH_TOKEN, _ = strings.CutPrefix(config["tw_token"].(string), "oauth:")
|
TW_OAUTH_TOKEN, _ = strings.CutPrefix(config["tw_token"].(string), "oauth:")
|
||||||
|
|
||||||
|
/* Automatically detect the correct Client-ID for the OAuth token */
|
||||||
|
req, _ := http.NewRequest("GET", "https://id.twitch.tv/oauth2/validate", nil)
|
||||||
|
req.Header.Set("Authorization", "OAuth " + TW_OAUTH_TOKEN)
|
||||||
|
client := &http.Client{}
|
||||||
|
if resp, err := client.Do(req); err == nil {
|
||||||
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
var vstruct map[string]interface{}
|
||||||
|
if err := json.Unmarshal(body, &vstruct); err == nil {
|
||||||
|
if autoCid, ok := vstruct["client_id"]; ok {
|
||||||
|
TW_CLIENT_ID = autoCid.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
/* load the Twitch badge cache */
|
/* load the Twitch badge cache */
|
||||||
chname, _ := strings.CutPrefix(config["tw_channel"].(string), "#")
|
chname, _ := strings.CutPrefix(config["tw_channel"].(string), "#")
|
||||||
TW_BADGE_CACHE = tw_fetch_badge_cache(chname)
|
TW_BADGE_CACHE = tw_fetch_badge_cache(chname)
|
||||||
@@ -294,26 +379,30 @@ func twitch_run(config map[string]interface{}) {
|
|||||||
log.Fatal("[twitch] Error connecting to Twitch: ", err)
|
log.Fatal("[twitch] Error connecting to Twitch: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tcpwritestr(conn, fmt.Sprintf("PASS %s\n", config["tw_token"]))
|
/* request advanced chat message capabilities BEFORE login/join */
|
||||||
tcpwritestr(conn, fmt.Sprintf("NICK %s\n", config["tw_nickname"]))
|
tcpwritestr(conn, "CAP REQ :twitch.tv/membership twitch.tv/tags twitch.tv/commands\r\n")
|
||||||
tcpwritestr(conn, fmt.Sprintf("JOIN %s\n", config["tw_channel"]))
|
tcpwritestr(conn, fmt.Sprintf("PASS %s\r\n", config["tw_token"]))
|
||||||
/* request advanced chat message capabilities */
|
tcpwritestr(conn, fmt.Sprintf("NICK %s\r\n", strings.ToLower(config["tw_nickname"].(string))))
|
||||||
tcpwritestr(conn, "CAP REQ :twitch.tv/membership twitch.tv/tags twitch.tv/commands\n")
|
tcpwritestr(conn, fmt.Sprintf("JOIN %s\r\n", config["tw_channel"]))
|
||||||
|
|
||||||
/* set up subscriptions and start the WebSocket event loop goroutine */
|
/* set up subscriptions and start the WebSocket event loop goroutine */
|
||||||
go tw_ws_pubsub_readloop()
|
go tw_ws_pubsub_readloop()
|
||||||
|
|
||||||
log.Println("StreamGoose Twitch Go backend connected")
|
log.Println("StreamGoose Twitch Go backend connected")
|
||||||
|
|
||||||
var rawmsg string
|
reader := bufio.NewReader(conn)
|
||||||
|
|
||||||
for { /* main loop */
|
for { /* main loop */
|
||||||
rawmsg = tcpreadstr(conn)
|
rawmsg, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("[twitch] Error reading from socket: ", err)
|
||||||
|
}
|
||||||
|
rawmsg = strings.TrimRight(rawmsg, "\r\n") // remove CRLF
|
||||||
if _, ok := config["debug"]; ok && config["debug"].(bool) == true {
|
if _, ok := config["debug"]; ok && config["debug"].(bool) == true {
|
||||||
log.Println(rawmsg)
|
log.Println(rawmsg)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(rawmsg, "PING") { /* respond to the PING message */
|
if strings.HasPrefix(rawmsg, "PING") { /* respond to the PING message */
|
||||||
tcpwritestr(conn, "PONG\n")
|
tcpwritestr(conn, "PONG :tmi.twitch.tv\r\n")
|
||||||
} else { /* run main procesing */
|
} else { /* run main procesing */
|
||||||
tw_process_message(rawmsg)
|
tw_process_message(rawmsg)
|
||||||
}
|
}
|
||||||
|
|||||||
+125
-22
@@ -2,6 +2,7 @@
|
|||||||
StreamGoose YouTube backend — Innertube API
|
StreamGoose YouTube backend — Innertube API
|
||||||
|
|
||||||
Created by Luxferre in 2024, released into public domain
|
Created by Luxferre in 2024, released into public domain
|
||||||
|
Updated in 2026 to implement more robust logic
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package main
|
package main
|
||||||
@@ -27,6 +28,7 @@ type YTIChatOptions struct {
|
|||||||
Continuation string `json:"continuation"` /* continuation key */
|
Continuation string `json:"continuation"` /* continuation key */
|
||||||
ClientName string `json:"clientName"` /* client name string */
|
ClientName string `json:"clientName"` /* client name string */
|
||||||
ClientVersion string `json:"clientVersion"` /* client version string */
|
ClientVersion string `json:"clientVersion"` /* client version string */
|
||||||
|
VisitorData string `json:"visitorData"` /* user tracking ID */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* API request helper function */
|
/* API request helper function */
|
||||||
@@ -36,8 +38,15 @@ func yt_inner_api_req(url string, data []byte) string {
|
|||||||
method = "POST"
|
method = "POST"
|
||||||
}
|
}
|
||||||
req, err := http.NewRequest(method, url, bytes.NewReader(data))
|
req, err := http.NewRequest(method, url, bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
req.Header.Set("User-Agent", YT_INNER_USERAGENT)
|
req.Header.Set("User-Agent", YT_INNER_USERAGENT)
|
||||||
req.Header.Set("Accept-Language", "en-US")
|
req.Header.Set("Accept-Language", "en-US")
|
||||||
|
if len(data) > 0 {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Origin", "https://www.youtube.com")
|
||||||
|
}
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
@@ -60,12 +69,20 @@ func yt_inner_runs_to_text(runs []interface{}) string {
|
|||||||
run := rrun.(map[string]interface{})
|
run := rrun.(map[string]interface{})
|
||||||
if _, ok := run["text"]; ok { /* normal text */
|
if _, ok := run["text"]; ok { /* normal text */
|
||||||
out_text += run["text"].(string)
|
out_text += run["text"].(string)
|
||||||
} else if _, ok := run["emoji"]; ok { /* this is where the fun begins */
|
} else if _, ok := run["emoji"]; ok { /* emoji handling */
|
||||||
emoji := run["emoji"].(map[string]interface{})
|
emoji := run["emoji"].(map[string]interface{})
|
||||||
if _, ok := emoji["isCustomEmoji"]; ok && emoji["isCustomEmoji"].(bool) == true { /* custom emoji */
|
if isCustom, ok := emoji["isCustomEmoji"]; ok && isCustom.(bool) == true { /* custom emoji */
|
||||||
/* TODO: out_text += " #YTEMOTE-" + emoji["image"]["thumbnails"][0]["url"] + "# " */
|
/* dynamically fetch emoji thumbnail */
|
||||||
if len(emoji["shortcuts"].([]interface{})) > 0 {
|
if image, imgOk := emoji["image"].(map[string]interface{}); imgOk {
|
||||||
out_text += " " + emoji["shortcuts"].([]interface{})[0].(string) + " "
|
if thumbnails, thOk := image["thumbnails"].([]interface{}); thOk && len(thumbnails) > 0 {
|
||||||
|
thumb := thumbnails[0].(map[string]interface{})
|
||||||
|
out_text += fmt.Sprintf(" <img src=\"%s\" class=\"yt-emote\" /> ", thumb["url"].(string))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* fallback to text shortcut */
|
||||||
|
if shortcuts, scOk := emoji["shortcuts"].([]interface{}); scOk && len(shortcuts) > 0 {
|
||||||
|
out_text += " " + shortcuts[0].(string) + " "
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else { /* standard Unicode emoji, pass as text */
|
} else { /* standard Unicode emoji, pass as text */
|
||||||
out_text += emoji["emojiId"].(string)
|
out_text += emoji["emojiId"].(string)
|
||||||
@@ -77,6 +94,30 @@ func yt_inner_runs_to_text(runs []interface{}) string {
|
|||||||
|
|
||||||
/* convert inner chat action into a public chat item */
|
/* convert inner chat action into a public chat item */
|
||||||
func yt_inner_action_to_chatitem(action map[string]interface{}) ChatMessage {
|
func yt_inner_action_to_chatitem(action map[string]interface{}) ChatMessage {
|
||||||
|
/* Handle Message Deletions */
|
||||||
|
if _, ok := action["markChatItemAsDeletedAction"]; ok {
|
||||||
|
delAction := action["markChatItemAsDeletedAction"].(map[string]interface{})
|
||||||
|
targetId := delAction["targetItemId"].(string)
|
||||||
|
return ChatMessage{
|
||||||
|
Type: "yt^delmsg",
|
||||||
|
NativeId: targetId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := action["markChatItemsByAuthorAsDeletedAction"]; ok {
|
||||||
|
delAction := action["markChatItemsByAuthorAsDeletedAction"].(map[string]interface{})
|
||||||
|
authorId := delAction["deletedChannelId"].(string)
|
||||||
|
return ChatMessage{
|
||||||
|
Type: "yt^deluser",
|
||||||
|
AuthorId: authorId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle Normal Additions */
|
||||||
|
if _, ok := action["addChatItemAction"]; !ok {
|
||||||
|
return ChatMessage{}
|
||||||
|
}
|
||||||
|
|
||||||
item := action["addChatItemAction"].(map[string]interface{})["item"].(map[string]interface{})
|
item := action["addChatItemAction"].(map[string]interface{})["item"].(map[string]interface{})
|
||||||
/* get the renderer */
|
/* get the renderer */
|
||||||
var renderer map[string]interface{}
|
var renderer map[string]interface{}
|
||||||
@@ -91,9 +132,11 @@ func yt_inner_action_to_chatitem(action map[string]interface{}) ChatMessage {
|
|||||||
} else {
|
} else {
|
||||||
renderer = nil
|
renderer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if renderer == nil { /* invalid renderer field */
|
if renderer == nil { /* invalid renderer field */
|
||||||
return ChatMessage{}
|
return ChatMessage{}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* here, message text is composed as a series of individual runs */
|
/* here, message text is composed as a series of individual runs */
|
||||||
var mruns []interface{}
|
var mruns []interface{}
|
||||||
if _, ok := renderer["message"]; ok {
|
if _, ok := renderer["message"]; ok {
|
||||||
@@ -114,6 +157,7 @@ func yt_inner_action_to_chatitem(action map[string]interface{}) ChatMessage {
|
|||||||
author_icon = thumbnails[0].(map[string]interface{})["url"].(string)
|
author_icon = thumbnails[0].(map[string]interface{})["url"].(string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fetch the timestamp (microseconds) */
|
/* fetch the timestamp (microseconds) */
|
||||||
ts, err := strconv.ParseInt(renderer["timestampUsec"].(string), 10, 0)
|
ts, err := strconv.ParseInt(renderer["timestampUsec"].(string), 10, 0)
|
||||||
if err != nil { /* fallback to current time if anything */
|
if err != nil { /* fallback to current time if anything */
|
||||||
@@ -125,13 +169,17 @@ func yt_inner_action_to_chatitem(action map[string]interface{}) ChatMessage {
|
|||||||
out_message := ChatMessage {
|
out_message := ChatMessage {
|
||||||
Type: "yt",
|
Type: "yt",
|
||||||
NativeId: renderer["id"].(string),
|
NativeId: renderer["id"].(string),
|
||||||
AuthorId: renderer["authorExternalChannelId"].(string),
|
|
||||||
Username: author_name,
|
Username: author_name,
|
||||||
Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"),
|
Timestamp: sent_dt.UTC().Format("2006-01-02T15:04:05Z"),
|
||||||
Text: yt_inner_runs_to_text(mruns),
|
Text: yt_inner_runs_to_text(mruns),
|
||||||
Icon: author_icon,
|
Icon: author_icon,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Optional field due to structure */
|
||||||
|
if _, ok := renderer["authorExternalChannelId"]; ok {
|
||||||
|
out_message.AuthorId = renderer["authorExternalChannelId"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
/* check roles */
|
/* check roles */
|
||||||
if _, ok := renderer["authorBadges"]; ok {
|
if _, ok := renderer["authorBadges"]; ok {
|
||||||
for _, entry := range renderer["authorBadges"].([]interface{}) {
|
for _, entry := range renderer["authorBadges"].([]interface{}) {
|
||||||
@@ -151,6 +199,7 @@ func yt_inner_action_to_chatitem(action map[string]interface{}) ChatMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := renderer["purchaseAmountText"]; ok {
|
if _, ok := renderer["purchaseAmountText"]; ok {
|
||||||
out_message.Type += "^sponsor"
|
out_message.Type += "^sponsor"
|
||||||
}
|
}
|
||||||
@@ -169,63 +218,118 @@ func yt_inner_get_chat_options(video_id string) YTIChatOptions {
|
|||||||
}
|
}
|
||||||
raw_data := yt_inner_api_req(live_url, nil)
|
raw_data := yt_inner_api_req(live_url, nil)
|
||||||
opts.ClientName = "WEB"
|
opts.ClientName = "WEB"
|
||||||
r, _ := regexp.Compile("\"INNERTUBE_API_KEY\":\"(.+?)\"")
|
|
||||||
|
r, _ := regexp.Compile("\"INNERTUBE_API_KEY\":\"([^\"]+)\"")
|
||||||
rmatch := r.FindStringSubmatch(raw_data)
|
rmatch := r.FindStringSubmatch(raw_data)
|
||||||
if len(rmatch) > 0 {
|
if len(rmatch) > 0 {
|
||||||
opts.Key = rmatch[1]
|
opts.Key = rmatch[1]
|
||||||
} else {
|
} else {
|
||||||
log.Fatal("[youtube] Cannot connect to the YouTube livechat (innertube key)")
|
log.Fatal("[youtube] Cannot connect to the YouTube livechat (innertube key)")
|
||||||
}
|
}
|
||||||
r, _ = regexp.Compile("\"continuation\":\"(.+?)\"")
|
|
||||||
|
r, _ = regexp.Compile("\"continuation\":\"([^\"]+)\"")
|
||||||
rmatch = r.FindStringSubmatch(raw_data)
|
rmatch = r.FindStringSubmatch(raw_data)
|
||||||
if len(rmatch) > 0 {
|
if len(rmatch) > 0 {
|
||||||
opts.Continuation = rmatch[1]
|
opts.Continuation = rmatch[1]
|
||||||
} else {
|
} else {
|
||||||
log.Fatal("[youtube] Cannot connect to the YouTube livechat (continuation)")
|
log.Fatal("[youtube] Cannot connect to the YouTube livechat (continuation)")
|
||||||
}
|
}
|
||||||
r, _ = regexp.Compile("\"clientVersion\":\"(.+?)\"")
|
|
||||||
|
r, _ = regexp.Compile("\"clientVersion\":\"([^\"]+)\"")
|
||||||
rmatch = r.FindStringSubmatch(raw_data)
|
rmatch = r.FindStringSubmatch(raw_data)
|
||||||
if len(rmatch) > 0 {
|
if len(rmatch) > 0 {
|
||||||
opts.ClientVersion = rmatch[1]
|
opts.ClientVersion = rmatch[1]
|
||||||
} else {
|
} else {
|
||||||
log.Fatal("[youtube] Cannot connect to the YouTube livechat (clientversion)")
|
opts.ClientVersion = "2.20260206.01.00" // Fallback to a known youtubei.js version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r, _ = regexp.Compile("\"visitorData\":\"([^\"]+)\"")
|
||||||
|
rmatch = r.FindStringSubmatch(raw_data)
|
||||||
|
if len(rmatch) > 0 {
|
||||||
|
opts.VisitorData = rmatch[1]
|
||||||
|
}
|
||||||
|
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* recursive function to find the next continuation token in the payload */
|
||||||
|
func find_continuation(node interface{}) string {
|
||||||
|
switch n := node.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
if cont, ok := n["continuation"]; ok {
|
||||||
|
if str, isStr := cont.(string); isStr {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, v := range n {
|
||||||
|
if res := find_continuation(v); res != "" {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for _, v := range n {
|
||||||
|
if res := find_continuation(v); res != "" {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
/* fetch chat data using the chat options */
|
/* fetch chat data using the chat options */
|
||||||
/* return the list of ChatMessage items */
|
/* return the list of ChatMessage items */
|
||||||
/* should update the options structure */
|
/* updates the options structure with the next continuation */
|
||||||
func yt_inner_get_chat_data(opts *YTIChatOptions) []ChatMessage {
|
func yt_inner_get_chat_data(opts *YTIChatOptions) []ChatMessage {
|
||||||
jsondata := make(map[string]interface{})
|
jsondata := make(map[string]interface{})
|
||||||
jsondata["context"] = make(map[string]interface{})
|
jsondata["context"] = make(map[string]interface{})
|
||||||
jsondata["continuation"] = opts.Continuation
|
jsondata["continuation"] = opts.Continuation
|
||||||
jsondata["context"].(map[string]interface{})["client"] = make(map[string]string)
|
|
||||||
jsondata["context"].(map[string]interface{})["client"].(map[string]string)["userAgent"] = YT_INNER_USERAGENT
|
clientMap := make(map[string]interface{})
|
||||||
jsondata["context"].(map[string]interface{})["client"].(map[string]string)["clientName"] = opts.ClientName
|
clientMap["hl"] = "en"
|
||||||
jsondata["context"].(map[string]interface{})["client"].(map[string]string)["clientVersion"] = opts.ClientVersion
|
clientMap["gl"] = "US"
|
||||||
|
clientMap["userAgent"] = YT_INNER_USERAGENT
|
||||||
|
clientMap["clientName"] = opts.ClientName
|
||||||
|
clientMap["clientVersion"] = opts.ClientVersion
|
||||||
|
clientMap["timeZone"] = "UTC"
|
||||||
|
if opts.VisitorData != "" {
|
||||||
|
clientMap["visitorData"] = opts.VisitorData
|
||||||
|
}
|
||||||
|
|
||||||
|
jsondata["context"].(map[string]interface{})["client"] = clientMap
|
||||||
jsonstr, _ := json.Marshal(&jsondata)
|
jsonstr, _ := json.Marshal(&jsondata)
|
||||||
|
|
||||||
/* post the marshalled string and get the result into chatdata */
|
/* post the marshalled string and get the result into chatdata */
|
||||||
target_url := fmt.Sprintf("https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key=%s", opts.Key)
|
target_url := fmt.Sprintf("https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key=%s", opts.Key)
|
||||||
rawresp := yt_inner_api_req(target_url, jsonstr)
|
rawresp := yt_inner_api_req(target_url, jsonstr)
|
||||||
|
|
||||||
var chatdata map[string]interface{}
|
var chatdata map[string]interface{}
|
||||||
err := json.Unmarshal([]byte(rawresp), &chatdata)
|
err := json.Unmarshal([]byte(rawresp), &chatdata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
/* Don't panic here on unmarshal errors, might be a temporary hiccup */
|
||||||
|
log.Println("[youtube] error decoding JSON continuation:", err)
|
||||||
|
return []ChatMessage{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var chatitems []ChatMessage
|
var chatitems []ChatMessage
|
||||||
/* process the upper structure */
|
|
||||||
|
/* Look for the next continuation token aggressively */
|
||||||
|
if cd, ok := chatdata["continuationContents"]; ok {
|
||||||
|
nextCont := find_continuation(cd)
|
||||||
|
if nextCont != "" {
|
||||||
|
opts.Continuation = nextCont
|
||||||
|
} else {
|
||||||
|
log.Println("[youtube] warning: could not find next continuation token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* process the upper structure for actions */
|
||||||
if _, ok := chatdata["continuationContents"]; ok {
|
if _, ok := chatdata["continuationContents"]; ok {
|
||||||
cc := chatdata["continuationContents"].(map[string]interface{})
|
cc := chatdata["continuationContents"].(map[string]interface{})
|
||||||
if _, ok := cc["liveChatContinuation"]; ok {
|
if _, ok := cc["liveChatContinuation"]; ok {
|
||||||
lcc := cc["liveChatContinuation"].(map[string]interface{})
|
lcc := cc["liveChatContinuation"].(map[string]interface{})
|
||||||
if lcc["actions"] != nil {
|
if lcc["actions"] != nil {
|
||||||
actions := lcc["actions"].([]interface{})
|
actions := lcc["actions"].([]interface{})
|
||||||
conts := lcc["continuations"].([]interface{})
|
|
||||||
icd := conts[0].(map[string]interface{})["invalidationContinuationData"].(map[string]interface{})
|
|
||||||
opts.Continuation = icd["continuation"].(string)
|
|
||||||
for _, action := range actions { /* iterate through the actions */
|
for _, action := range actions { /* iterate through the actions */
|
||||||
if _, ok := action.(map[string]interface{})["addChatItemAction"]; ok {
|
|
||||||
chatitem := yt_inner_action_to_chatitem(action.(map[string]interface{}))
|
chatitem := yt_inner_action_to_chatitem(action.(map[string]interface{}))
|
||||||
if chatitem.Type != "" {
|
if chatitem.Type != "" {
|
||||||
chatitems = append(chatitems, chatitem)
|
chatitems = append(chatitems, chatitem)
|
||||||
@@ -234,6 +338,5 @@ func yt_inner_get_chat_data(opts *YTIChatOptions) []ChatMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return chatitems
|
return chatitems
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user