Created
August 13, 2025 23:35
-
-
Save geopelia/f180453a154c903f53f2e6e4f357f03a to your computer and use it in GitHub Desktop.
Golang client to connect OBS using Websockets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "crypto/sha256" | |
| "encoding/base64" | |
| "encoding/json" | |
| "log" | |
| "net/url" | |
| "os" | |
| "time" | |
| "github.com/google/uuid" | |
| "github.com/gorilla/websocket" | |
| "github.com/joho/godotenv" | |
| ) | |
| const ( | |
| Hello = 0 | |
| Identify = 1 | |
| Identified = 2 | |
| Reidentify = 3 | |
| Event = 5 | |
| Request = 6 | |
| RequestResponse = 7 | |
| RequestBatch = 8 | |
| RequestBatchResponse = 9 | |
| ) | |
| type AuthData struct { | |
| Challenge string `json:"challenge"` | |
| Salt string `json:"salt"` | |
| } | |
| type HelloDataStruct struct { | |
| Auth AuthData `json:"authentication"` | |
| ObsWebSocketVersion string `json:"obsWebSocketVersion"` | |
| RpcVersion int8 `json:"rpcVersion"` | |
| } | |
| type HelloStruct struct { | |
| MainMessage | |
| HelloDataStruct `json:"d"` | |
| } | |
| type IdentifyDataStruct struct { | |
| Autentication string `json:"authentication"` | |
| RpcVersion int8 `json:"rpcVersion"` | |
| } | |
| type IdentifyStruct struct { | |
| MainMessage | |
| IdentifyDataStruct `json:"d"` | |
| } | |
| type IdentifiedDataStruct struct { | |
| RpcVersion int8 `json:"negotiatedRpcVersion"` | |
| } | |
| type IdentifiedStruct struct { | |
| MainMessage | |
| IdentifiedDataStruct `json:"d"` | |
| } | |
| type EventDataStruct struct { | |
| EventType string `json:"eventType"` | |
| EventIntent int32 `json:"eventIntent"` | |
| EventData any `json:"eventData,omitempty"` | |
| } | |
| type EventStruct struct { | |
| MainMessage | |
| EventDataStruct `json:"d"` | |
| } | |
| type RequestDataStruct struct { | |
| RequestType string `json:"requestType"` | |
| RequestId string `json:"requestId"` | |
| RequestData any `json:"requestData,omitempty"` | |
| } | |
| type RequestStruct struct { | |
| MainMessage | |
| RequestDataStruct `json:"d"` | |
| } | |
| type RequestStatus struct { | |
| Result bool `json:"result"` | |
| Code int32 `json:"code"` | |
| Comment string `json:"comment,omitempty"` | |
| } | |
| type RequestResponseDataStruc struct { | |
| RequestType string `json:"requestType"` | |
| RequestId string `json:"requestId"` | |
| ResponseData any `json:"responseData,omitempty"` | |
| RequestStatus `json:"requestStatus"` | |
| } | |
| type RequestResponseStruct struct { | |
| MainMessage | |
| RequestResponseDataStruc `json:"d"` | |
| } | |
| type MainMessage struct { | |
| OpCode int8 `json:"op"` | |
| } | |
| func generatePassword(authInfo AuthData) string { | |
| pass := os.Getenv("PASS") | |
| first := pass + authInfo.Salt | |
| base64Secret := hash256AndEncode(first) | |
| second := base64Secret + authInfo.Challenge | |
| base64Secret = hash256AndEncode(second) | |
| return base64Secret | |
| } | |
| func hash256AndEncode(phrase string) string { | |
| hasher := sha256.New() | |
| hasher.Write([]byte(phrase)) | |
| return base64.StdEncoding.EncodeToString(hasher.Sum(nil)) | |
| } | |
| func identifyMe(question HelloStruct, done chan struct{}, writeChan chan string) { | |
| pass := generatePassword(question.Auth) | |
| identify := IdentifyStruct{MainMessage{Identify}, IdentifyDataStruct{pass, question.RpcVersion}} | |
| output, err := json.Marshal(&identify) | |
| if err != nil { | |
| log.Println("error", err) | |
| close(done) | |
| } | |
| writeChan <- string(output) | |
| } | |
| func tryDecodeJson[t any](message []byte, myVar *t, done chan struct{}) { | |
| err := json.Unmarshal(message, myVar) | |
| if err != nil { | |
| log.Println("Error decoding json:", err) | |
| close(done) | |
| } | |
| } | |
| func decodeMessage(message []byte, done chan struct{}, writeChan chan string, isLogged chan bool) { | |
| var msgObj MainMessage = MainMessage{} | |
| tryDecodeJson(message, &msgObj, done) | |
| switch msgObj.OpCode { | |
| case Hello: | |
| var helloMessage HelloStruct | |
| tryDecodeJson(message, &helloMessage, done) | |
| identifyMe(helloMessage, done, writeChan) | |
| case Identified: | |
| var identifiedMessage IdentifiedStruct | |
| tryDecodeJson(message, &identifiedMessage, done) | |
| if identifiedMessage.IdentifiedDataStruct.RpcVersion > 0 { | |
| isLogged <- true | |
| } | |
| case Event: | |
| var eventMessage EventStruct | |
| tryDecodeJson(message, &eventMessage, done) | |
| log.Println("Evento en obs: ", eventMessage.EventDataStruct) | |
| case RequestResponse: | |
| var response RequestResponseStruct | |
| tryDecodeJson(message, &response, done) | |
| log.Println("Respuesta a solicitud", response.RequestResponseDataStruc.ResponseData) | |
| default: | |
| log.Println(msgObj) | |
| } | |
| } | |
| func closeWebSocket(c *websocket.Conn, myChanel chan struct{}) { | |
| log.Println("interrupt") | |
| err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "chao")) | |
| if err != nil { | |
| log.Println("write close:", err) | |
| return | |
| } | |
| select { | |
| case <-myChanel: | |
| case <-time.After(time.Second * 5): | |
| } | |
| } | |
| func askForVersion(done chan struct{}, writeChan chan string) { | |
| requestID := uuid.New().String() | |
| requestMessage := RequestStruct{MainMessage{Request}, RequestDataStruct{"GetStats", requestID, nil}} | |
| output, err := json.Marshal(&requestMessage) | |
| if err != nil { | |
| log.Println("error", err) | |
| close(done) | |
| } | |
| writeChan <- string(output) | |
| } | |
| func askChangeScene(done chan struct{}, writeChan chan string) { | |
| requestID := uuid.New().String() | |
| type Params struct { | |
| SceneName string `json:"sceneName"` | |
| } | |
| newScene := Params{"brb"} | |
| requestMessage := RequestStruct{MainMessage{Request}, RequestDataStruct{"SetCurrentProgramScene", requestID, newScene}} | |
| output, err := json.Marshal(&requestMessage) | |
| if err != nil { | |
| log.Println("error", err) | |
| close(done) | |
| } | |
| writeChan <- string(output) | |
| } | |
| func main() { | |
| err := godotenv.Load() | |
| if err != nil { | |
| log.Fatal("Error:", err) | |
| } | |
| host := os.Getenv("HOST") + ":" + os.Getenv("PORT") | |
| u := url.URL{Scheme: "ws", Host: host, Path: "/"} | |
| log.Printf("connecting to %s", u.String()) | |
| c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) | |
| if err != nil { | |
| log.Fatal("dial:", err) | |
| } | |
| defer c.Close() | |
| done := make(chan struct{}) | |
| timeout := make(chan struct{}) | |
| dataToSend := make(chan string, 100) | |
| isLogged := make(chan bool, 1) | |
| // We are reading the response for server here | |
| go func() { | |
| defer close(done) | |
| for { | |
| _, message, err := c.ReadMessage() | |
| if err != nil { | |
| log.Println("read:", err) | |
| return | |
| } | |
| log.Printf("recv: %s \n", message) | |
| decodeMessage(message, done, dataToSend, isLogged) | |
| } | |
| }() | |
| go func() { | |
| defer close(timeout) | |
| time.Sleep(10 * time.Second) | |
| }() | |
| for { | |
| select { | |
| case <-done: | |
| return | |
| case <-timeout: | |
| closeWebSocket(c, done) | |
| return | |
| case texto, ok := <-dataToSend: | |
| if !ok { | |
| log.Println("cerrADO") | |
| } | |
| err := c.WriteMessage(websocket.TextMessage, []byte(texto)) | |
| if err != nil { | |
| log.Println("write:", err) | |
| return | |
| } | |
| log.Printf("sent: %s \n", texto) | |
| case loggedIn := <-isLogged: | |
| if loggedIn { | |
| log.Println("estoy autenticado") | |
| askForVersion(done, dataToSend) | |
| askChangeScene(done, dataToSend) | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment