├── README.md └── owncast-demobot ├── Dockerfile ├── botmemory.go ├── build.sh ├── config.go ├── config.yaml ├── globals.go ├── go.mod ├── go.sum ├── httpcontroller.go ├── httpmiddleware.go ├── main.go ├── messages.go ├── owncastapiclient.go ├── readme.md └── webhookstypes.go /README.md: -------------------------------------------------------------------------------- 1 | # Owncast Examples 2 | 3 | a list of projects that show how one can work with Owncast. 4 | 5 | 6 | ## Projects 7 | 8 | - [Owncast Demo Bot](owncast-demobot/) 9 | -------------------------------------------------------------------------------- /owncast-demobot/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | 3 | # Set the Current Working Directory inside the container 4 | WORKDIR /app/owncast-demobot 5 | 6 | COPY go.mod . 7 | COPY go.sum . 8 | RUN go mod download 9 | 10 | COPY . . 11 | RUN go build -o ./out/owncast-demobot . 12 | 13 | EXPOSE 8100 14 | CMD ["./out/owncast-demobot"] -------------------------------------------------------------------------------- /owncast-demobot/botmemory.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/muesli/cache2go" 7 | ) 8 | 9 | var knownUserIds = cache2go.Cache("visitors") 10 | 11 | func AddKnownUser(userId string) { 12 | if IsNewUser(userId) { 13 | knownUserIds.Add(userId, time.Hour*24*15, userId) 14 | } 15 | } 16 | 17 | func IsNewUser(userId string) bool { 18 | exists, _ := knownUserIds.Value(userId) 19 | return exists == nil 20 | } 21 | -------------------------------------------------------------------------------- /owncast-demobot/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker build . -t owncast-demobot 4 | docker tag owncast-demobot:latest ghcr.io/owncast/owncast-demobot:latest 5 | docker push ghcr.io/owncast/owncast-demobot:latest 6 | 7 | -------------------------------------------------------------------------------- /owncast-demobot/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "reflect" 9 | "io/ioutil" 10 | "gopkg.in/yaml.v2" 11 | ) 12 | 13 | type Config struct { 14 | ListenAddress string `yaml:"ListenAddress"` 15 | AccessToken string `yaml:"AccessToken"` 16 | OwncastAddress string `yaml:"OwncastAddress"` 17 | } 18 | 19 | func DefaultConfiguration() Config { 20 | var defaultConfig = Config { ":8100", "SECRETACCESSTOKEN", "http://localhost:8000" } 21 | return defaultConfig 22 | } 23 | 24 | func CollectDemoBotConfiguration(fileName string) Config { 25 | _, err := os.Stat(fileName); 26 | if err != nil { 27 | log.Print(fmt.Sprintf("Could not find file: %s. Reason: '%s'. Using defaults instead.", fileName, err)) 28 | return DefaultConfiguration() 29 | } 30 | 31 | file, err := ioutil.ReadFile(fileName) 32 | if err != nil { 33 | log.Print(fmt.Sprintf("Could not read file: %s. Reason: '%s'. Using defaults instead.", fileName, err)) 34 | return DefaultConfiguration() 35 | } 36 | 37 | var cfg = DefaultConfiguration() 38 | err = yaml.Unmarshal(file, &cfg) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | for i := 0; i < reflect.ValueOf(cfg).NumField(); i++ { 44 | structFieldName := reflect.ValueOf(&cfg).Elem().Type().Field(i).Name 45 | environmentVariableName := fmt.Sprintf("%s_%s", "OWNCAST_DEMOBOT", strings.ToUpper(structFieldName)) 46 | environmentVariableValue := os.Getenv(environmentVariableName) 47 | 48 | if environmentVariableValue != "" { 49 | structField := reflect.ValueOf(&cfg).Elem().FieldByName(structFieldName) 50 | structField.SetString(environmentVariableValue) 51 | } 52 | } 53 | 54 | return cfg 55 | } 56 | -------------------------------------------------------------------------------- /owncast-demobot/config.yaml: -------------------------------------------------------------------------------- 1 | ListenAddress: ":8100" 2 | AccessToken: "" 3 | OwncastAddress: "http://localhost:8080" 4 | -------------------------------------------------------------------------------- /owncast-demobot/globals.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* globally accessible configuration, once read at application start */ 4 | var DemoBotConfiguration Config -------------------------------------------------------------------------------- /owncast-demobot/go.mod: -------------------------------------------------------------------------------- 1 | module bot.watch.owncast.online 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/muesli/cache2go v0.0.0-20211005105910-8e46465cca4a 7 | gopkg.in/yaml.v2 v2.4.0 8 | ) 9 | -------------------------------------------------------------------------------- /owncast-demobot/go.sum: -------------------------------------------------------------------------------- 1 | github.com/muesli/cache2go v0.0.0-20211005105910-8e46465cca4a h1:IZxQOY9gAiiGGuEdlOBnqaC3yumj8UvyQluBNqGP2Ek= 2 | github.com/muesli/cache2go v0.0.0-20211005105910-8e46465cca4a/go.mod h1:WERUkUryfUWlrHnFSO/BEUZ+7Ns8aZy7iVOGewxKzcc= 3 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 4 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 5 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 6 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 7 | -------------------------------------------------------------------------------- /owncast-demobot/httpcontroller.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | func AutoRoute(w http.ResponseWriter, r *http.Request) { 13 | var event WebhookEvent 14 | body, err := ioutil.ReadAll(r.Body) 15 | r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) 16 | err = json.Unmarshal(body, &event) 17 | if err != nil { 18 | log.Print(err) 19 | } 20 | 21 | switch event.Type { 22 | case MessageSent: 23 | UserMessage(w, r) 24 | return 25 | case StreamStarted, StreamStopped: 26 | StreamStartStop(w, r) 27 | return 28 | case UserJoined: 29 | UserJoin(w, r) 30 | return 31 | case UserNameChanged: 32 | UserNameChange(w, r) 33 | return 34 | default: 35 | log.Printf("Warning Unknown EventType") 36 | return 37 | } 38 | } 39 | 40 | func UserJoin(w http.ResponseWriter, r *http.Request) { 41 | var event NameChangeWebhookEvent 42 | body, err := ioutil.ReadAll(r.Body) 43 | err = json.Unmarshal(body, &event) 44 | if err != nil { 45 | log.Print(err) 46 | } 47 | 48 | go SendSystemMessageToClient(event.EventData.ClientId, GetUserJoinMessage(event.EventData.User.DisplayName), 1) 49 | if IsNewUser(event.EventData.User.Id) { 50 | go SendSystemMessageToClient(event.EventData.ClientId, GetBotIntroductionMessage(), 3) 51 | go SendSystemMessageToClient(event.EventData.ClientId, GetNameChangeHint(event.EventData.User.DisplayName), 5) 52 | } 53 | 54 | AddKnownUser(event.EventData.User.Id) 55 | } 56 | 57 | func UserNameChange(w http.ResponseWriter, r *http.Request) { 58 | var event NameChangeWebhookEvent 59 | body, err := ioutil.ReadAll(r.Body) 60 | err = json.Unmarshal(body, &event) 61 | if err != nil { 62 | log.Print(err) 63 | } 64 | 65 | if len(event.EventData.User.PreviousNames) == 1 { 66 | go SendSystemMessageToClient(event.EventData.ClientId, GetNamechangeMessage(event.EventData.User.DisplayName), 1) 67 | } 68 | } 69 | 70 | func UserMessage(w http.ResponseWriter, r *http.Request) { 71 | var event WebhookChatEvent 72 | body, _ := ioutil.ReadAll(r.Body) 73 | err := json.Unmarshal(body, &event) 74 | if err != nil { 75 | log.Print(err) 76 | } 77 | 78 | switch event.EventData.Body { /* bot commands*/ 79 | case "!bot": 80 | go SendSystemMessageToClient(event.EventData.ClientId, GetBotHelpText(), 0) 81 | case "!links": 82 | go SendSystemMessageToClient(event.EventData.ClientId, GetFurtherResourcesMessage(), 0) 83 | } 84 | 85 | // Handle questions from chat users. 86 | questionStrings := []string{ 87 | "?", 88 | "what is", 89 | "how do i", 90 | "can i", 91 | } 92 | 93 | for _, questionString := range questionStrings { 94 | compareString := strings.ToLower(questionString) 95 | if strings.Contains(strings.ToLower(event.EventData.Body), compareString) { 96 | go SendSystemMessageToClient(event.EventData.ClientId, "Good question. I'm just a bot, but here's the best I came up with:", 1) 97 | go SendSystemMessageToClient(event.EventData.ClientId, GetFurtherResourcesMessage(), 2) 98 | 99 | break 100 | } 101 | } 102 | } 103 | 104 | func StreamStartStop(w http.ResponseWriter, r *http.Request) { 105 | var event WebhookStreamStartStopEvent 106 | body, _ := ioutil.ReadAll(r.Body) 107 | err := json.Unmarshal(body, &event) 108 | if err != nil { 109 | log.Print(err) 110 | } 111 | 112 | if event.Type == StreamStarted { 113 | go SendSystemMessage(GetStreamStartedMessage(), 5) 114 | } else { 115 | go SendSystemMessage(GetStreamStoppedMessage(), 5) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /owncast-demobot/httpmiddleware.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | func RequireHttpPost(next http.HandlerFunc) http.HandlerFunc { 11 | return func(w http.ResponseWriter, r *http.Request) { 12 | if r.Method != "POST" { 13 | http.Error(w, "Unsupported Method", http.StatusMethodNotAllowed) 14 | return 15 | } 16 | 17 | next.ServeHTTP(w, r) 18 | } 19 | } 20 | 21 | func RequireJsonContentType(next http.HandlerFunc) http.HandlerFunc { 22 | return func(w http.ResponseWriter, r *http.Request) { 23 | contentType := r.Header.Get("Content-Type") 24 | if contentType != "application/json" { 25 | http.Error(w, "The specified ContentType is not supported", http.StatusUnsupportedMediaType) 26 | } 27 | next.ServeHTTP(w, r) 28 | } 29 | } 30 | 31 | func LogRequest(next http.HandlerFunc) http.HandlerFunc { 32 | return func(w http.ResponseWriter, r *http.Request) { 33 | log.Printf("Tracing request for '%s' from '%s'", r.RequestURI, r.RemoteAddr) 34 | body, _ := ioutil.ReadAll(r.Body) 35 | log.Printf("Body %s", string(body)) 36 | r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) 37 | next.ServeHTTP(w, r) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /owncast-demobot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | func main() { 9 | DemoBotConfiguration = CollectDemoBotConfiguration("config.yaml") 10 | 11 | httpMultiplexer := http.NewServeMux() 12 | httpMultiplexer.HandleFunc("/auto", RequireHttpPost(RequireJsonContentType(LogRequest(AutoRoute)))) 13 | httpMultiplexer.HandleFunc("/stream/stop", RequireHttpPost(RequireJsonContentType(LogRequest(StreamStartStop)))) 14 | httpMultiplexer.HandleFunc("/stream/start", RequireHttpPost(RequireJsonContentType(LogRequest(StreamStartStop)))) 15 | httpMultiplexer.HandleFunc("/user/join", RequireHttpPost(RequireJsonContentType(LogRequest(UserJoin)))) 16 | httpMultiplexer.HandleFunc("/user/message", RequireHttpPost(RequireJsonContentType(LogRequest(UserMessage)))) 17 | 18 | log.Print("Bot is working for " + DemoBotConfiguration.OwncastAddress) 19 | log.Print("Listening for Webhooks on " + DemoBotConfiguration.ListenAddress) 20 | 21 | err := http.ListenAndServe(DemoBotConfiguration.ListenAddress, httpMultiplexer) 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /owncast-demobot/messages.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | ) 7 | 8 | func GetNamechangeMessage(newUsername string) string { 9 | messages := []string{ 10 | "%s, I like your new name a lot.", 11 | "'%s'... What a cool name.", 12 | "%s. This is how heroes are called", 13 | "Great %s, you found the name-change function", 14 | } 15 | return fmt.Sprintf(messages[rand.Intn(len(messages))], newUsername) 16 | } 17 | 18 | func GetBotIntroductionMessage() string { 19 | return "May I introduce myself? I am a friendly Owncast Bot, programmed to help you on this demo server! But I'm not very smart yet, but type **!bot** to see what I can do to help." 20 | } 21 | 22 | func GetNameChangeHint(username string) string { 23 | return "Owncast has assigned you " + username + " as a username. If you don't like it, or want to change it, you can do so by clicking the '" + username + "' label in the upper right corner." 24 | } 25 | 26 | func GetFurtherResourcesMessage() string { 27 | return `# Here are some links for you: 28 | - Find us on our **[Website](https://owncast.online/)**. 29 | - See how easy it is **[to get your personal Owncast up and running](https://owncast.online/quickstart/)**. 30 | - Chat with us on **[RocketChat](https://owncast.rocket.chat)**. 31 | - Collaborate, contribute or file feature requests and bug reports on **[Github](https://github.com/owncast)**. 32 | - Read the **[documentation](https://owncast.online/docs/)** to learn how you can configure Owncast. 33 | - Visit the **[Owncast Directory](https://directory.owncast.online)** and see what others are streaming and how they're using the software.` 34 | } 35 | 36 | func GetBotHelpText() string { 37 | return `Beep bop, I'm a bot built just for this demo server. [Here](https://github.com/unclearParadigm/owncast-examples/) you can find my source code. 38 | Here are the commands that I understand: 39 | - !bot - the message you are reading right now 40 | - !links - send links to various Owncast resources 41 | ` 42 | } 43 | 44 | func GetStreamStartedMessage() string { 45 | messages := []string{"Let's get this party started", "There we go.", "Are you ready?", "Wow, we're live", "How exciting, we're ON AIR"} 46 | return messages[rand.Intn(len(messages))] 47 | } 48 | 49 | func GetStreamStoppedMessage() string { 50 | messages := []string{"Thank you for joining in", "That's it for today", "Good night everyone", "Cya", "Bye Bye"} 51 | return messages[rand.Intn(len(messages))] 52 | } 53 | 54 | func GetUserJoinMessage(username string) string { 55 | messages := []string{ 56 | "Hello %s. Nice to see you around here.", 57 | "Howdy %s. Glad you reached us.", 58 | "Hey %s. Have an owncast-tastic day.", 59 | "Welcome %s. It's nice to have you here.", 60 | "Greetings %s. Happy to see you.", 61 | "Bonjour %s. What a pleasure to meet you.", 62 | "Hey there %s. How nice that you discovered Owncast.", 63 | "Hi %s. What a coincidence to see you here.", 64 | } 65 | return fmt.Sprintf(messages[rand.Intn(len(messages))], username) 66 | } 67 | -------------------------------------------------------------------------------- /owncast-demobot/owncastapiclient.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "log" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | func getApiUrlFor(endpoint string) string { 14 | return strings.TrimRight(DemoBotConfiguration.OwncastAddress, "/") + endpoint 15 | } 16 | 17 | func SendSystemMessage(message string, sendDelay int) { 18 | time.Sleep(time.Duration(sendDelay) * time.Second) 19 | postBody, _ := json.Marshal(map[string]string{ 20 | "body": message, 21 | }) 22 | responseBody := bytes.NewBuffer(postBody) 23 | req, _ := http.NewRequest("POST", getApiUrlFor("/api/integrations/chat/system"), responseBody) 24 | req.Header.Add("Authorization", "Bearer "+DemoBotConfiguration.AccessToken) 25 | req.Header.Add("ContentType", "application/json") 26 | 27 | client := &http.Client{} 28 | _, err := client.Do(req) 29 | if err != nil { 30 | log.Println("Error on response.\n[ERROR] -", err) 31 | } 32 | } 33 | 34 | func SendSystemMessageToClient(clientId uint, message string, sendDelay int) { 35 | time.Sleep(time.Duration(sendDelay) * time.Second) 36 | var clientIdString string = strconv.FormatUint(uint64(clientId), 10) 37 | 38 | postBody, _ := json.Marshal(map[string]string{ 39 | "body": message, 40 | }) 41 | responseBody := bytes.NewBuffer(postBody) 42 | req, _ := http.NewRequest("POST", getApiUrlFor("/api/integrations/chat/system/client/"+clientIdString), responseBody) 43 | req.Header.Add("Authorization", "Bearer "+DemoBotConfiguration.AccessToken) 44 | req.Header.Add("ContentType", "application/json") 45 | 46 | client := &http.Client{} 47 | _, err := client.Do(req) 48 | if err != nil { 49 | log.Println("Error on response.\n[ERROR] -", err) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /owncast-demobot/readme.md: -------------------------------------------------------------------------------- 1 | # Owncast Demo-Boot 2 | 3 | This is a simple bot supposed to help people visiting the [Owncast Demo Instance](https://demo.owncast.online). It's also here, to show how easy it is to build on top of Owncast. This bot is written in [Go](https://golang.org/). 4 | 5 | ## Features 6 | 7 | - receive Webhooks from Owncast with catch-all webhook endpoint 8 | - send random chat messages on Stream Start / Stream Stop 9 | - greet newly joined users 10 | - give new users hints to change their name 11 | - rudimentary question detection, causing the bot to reply with a handful of Owncast links 12 | - support for !bang commands / custom commands 13 | - running as container 14 | 15 | 16 | ## Webhook Endpoints 17 | 18 | - `/auto` 19 | accepts every webhook, does automatically differentiate between the available event types 20 | 21 | - `/stream/stop` 22 | accepts STREAM_START events 23 | 24 | - `/stream/start` 25 | accepts STREAM_STOP events 26 | 27 | - `/user/join` 28 | accepts USER_JOIN events (and NAME_CHANGED events) 29 | 30 | - `/user/message` 31 | accepts CHAT events (any kind of chat message a user types) 32 | 33 | For Details on the individual Webhooks, please refer to the [Owncast Thirdparty API documentation](https://owncast.online/thirdparty/webhooks/) 34 | 35 | ## Configuration 36 | 37 | This bot can be configured via the `config.yaml` configuration file and/or Environment variables. Environment variables take higher precedence and will override any values configured in the configuration. 38 | 39 | ### Configuration file 40 | 41 | - ListenAddress 42 | Hostname/Port the bot is listening for incoming HTTP Requests from Owncast. The expected format is `hostname:port`. 43 | If you don't care to set the hostname, just set the value to the port like so: `:port` (e.g. `:8080`) 44 | 45 | - AccessToken 46 | required to respond to chat-messages (you can create a new AccessToken in the Admin UI of your owncast instance) 47 | 48 | - OwncastAddress 49 | the address to the Owncast instance that the bot should be running on (e.g. `http://demo.owncast.online`) 50 | make sure to include the scheme (`http` or `https`) 51 | 52 | 53 | ### Environment variables 54 | 55 | | Name | Description | Example Value | 56 | |:------------------------------------|:----------------------------------------------------------------------------------|:--------------| 57 | | OWNCAST_DEMOBOT_LISTENADDRESS | Listen Address the Bot is listening on, for incoming HTTP Webhooks from Owncast | `:8100` | 58 | | OWNCAST_DEMOBOT_ACCESSTOKEN | Access token allowing to post to `/api/integrations/chat/system` | `123-asd-234` | 59 | | OWNCAST_DEMOBOT_OWNCASTADDRESS | To which Owncast instance this bot should reply to | `https://https://watch.owncast.online ` 60 | 61 | Environment variables WILL override values in the `config.yaml`. Environment variables are supposed to be used for configuring the bot, when run as Docker container. 62 | 63 | ## How to run this bot? 64 | 65 | ### Build and run from Source Code as executable 66 | 67 | 1. `git clone https://github.com/owncast/owncast-examples` 68 | 1. `cd owncast-examples/owncast-demobot` 69 | 1. edit the `config.yaml` to match your needs 70 | 1. `go run *.go` 71 | 72 | ### Build and run from Source Code as container 73 | 74 | 1. `git clone https://github.com/owncast/owncast-examples` 75 | 1. `cd owncast-examples/owncast-demobot` 76 | 1. `docker build -t owncastdemobot .` 77 | 1. `docker run --name owncastdemobot --network host -e OWNCAST_DEMOBOT_LISTENADDRESS=":8100" -e OWNCAST_DEMOBOT_ACCESSTOKEN=" -e OWNCAST_DEMOBOT_OWNCASTADDRESS="http://localhost:8080" owncastdemobot` 78 | -------------------------------------------------------------------------------- /owncast-demobot/webhookstypes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type EventType = string 8 | 9 | const ( 10 | MessageSent EventType = "CHAT" 11 | UserJoined EventType = "USER_JOINED" 12 | UserNameChanged EventType = "NAME_CHANGE" 13 | StreamStarted EventType = "STREAM_STARTED" 14 | StreamStopped EventType = "STREAM_STOPPED" 15 | ) 16 | 17 | type User struct { 18 | Id string `json:"id"` 19 | AccessToken string `json:"-"` 20 | DisplayName string `json:"displayName"` 21 | DisplayColor int `json:"displayColor"` 22 | CreatedAt time.Time `json:"createdAt"` 23 | DisabledAt time.Time `json:"disabledAt,omitempty"` 24 | PreviousNames []string `json:"previousNames"` 25 | NameChangedAt time.Time `json:"nameChangedAt,omitempty"` 26 | } 27 | 28 | type ChatMessage struct { 29 | User User `json:"user,omitempty"` 30 | Body string `json:"body,omitempty"` 31 | ClientId uint `json:"clientId"` 32 | RawBody string `json:"rawBody,omitempty"` 33 | ID string `json:"id,omitempty"` 34 | Visible bool `json:"visible"` 35 | Timestamp time.Time `json:"timestamp,omitempty"` 36 | } 37 | 38 | type StreamEvent struct { 39 | Summary string `json:"summary"` 40 | Name string `json:"name"` 41 | StreamTitle string `json:"streamTitle"` 42 | } 43 | 44 | type WebhookEvent struct { 45 | Type EventType `json:"type"` 46 | EventData interface{} `json:"eventData,omitempty"` 47 | } 48 | 49 | type WebhookChatEvent struct { 50 | Type EventType `json:"type"` 51 | EventData ChatMessage `json:"eventData,omitempty"` 52 | } 53 | 54 | type WebhookStreamStartStopEvent struct { 55 | Type EventType `json:"type"` 56 | EventData StreamEvent `json:"eventData,omitempty"` 57 | } 58 | 59 | type NameChangeEvent struct { 60 | Id string `json:"id"` 61 | Timestamp time.Time `json:"timestamp"` 62 | User User `json:"user"` 63 | ClientId uint `json:"clientId"` 64 | } 65 | 66 | type NameChangeWebhookEvent struct { 67 | Type EventType `json:"type"` 68 | EventData NameChangeEvent `json:"eventData,omitempty"` 69 | } 70 | --------------------------------------------------------------------------------