home to your local SPACEGIRL 馃挮
arimelody.space
1package cursor
2
3import (
4 "arimelody-web/model"
5 "fmt"
6 "math/rand"
7 "net/http"
8 "strconv"
9 "strings"
10 "sync"
11 "time"
12
13 "github.com/gorilla/websocket"
14)
15
16type CursorClient struct {
17 ID int32
18 Conn *websocket.Conn
19 Route string
20 X float32
21 Y float32
22 Click bool
23 Disconnected bool
24}
25
26type CursorMessage struct {
27 Data []byte
28 Route string
29 Exclude []*CursorClient
30}
31
32func (client *CursorClient) Send(data []byte) {
33 err := client.Conn.WriteMessage(websocket.TextMessage, data)
34 if err != nil {
35 client.Disconnect()
36 }
37}
38
39func (client *CursorClient) Disconnect() {
40 client.Disconnected = true
41 broadcast <- CursorMessage{
42 []byte(fmt.Sprintf("leave:%d", client.ID)),
43 client.Route,
44 []*CursorClient{},
45 }
46}
47
48var clients = make(map[int32]*CursorClient)
49var broadcast = make(chan CursorMessage)
50var mutex = &sync.Mutex{}
51
52func StartCursor(app *model.AppState) {
53 var includes = func (clients []*CursorClient, client *CursorClient) bool {
54 for _, c := range clients {
55 if c.ID == client.ID { return true }
56 }
57 return false
58 }
59
60 log("Cursor message handler ready!")
61
62 for {
63 message := <-broadcast
64 mutex.Lock()
65 for _, client := range clients {
66 if client.Route != message.Route { continue }
67 if includes(message.Exclude, client) { continue }
68 client.Send(message.Data)
69 }
70 mutex.Unlock()
71 }
72}
73
74func handleClient(client *CursorClient) {
75 msgType, message, err := client.Conn.ReadMessage()
76 if err != nil {
77 client.Disconnect()
78 return
79 }
80 if msgType != websocket.TextMessage { return }
81
82 args := strings.Split(string(message), ":")
83 if len(args) == 0 { return }
84 switch args[0] {
85 case "loc":
86 if len(args) < 2 { return }
87
88 client.Route = args[1]
89
90 mutex.Lock()
91 for otherClientID, otherClient := range clients {
92 if otherClientID == client.ID || otherClient.Route != client.Route { continue }
93 client.Send([]byte(fmt.Sprintf("join:%d", otherClientID)))
94 client.Send([]byte(fmt.Sprintf("pos:%d:%f:%f", otherClientID, otherClient.X, otherClient.Y)))
95 }
96 mutex.Unlock()
97
98 broadcast <- CursorMessage{
99 []byte(fmt.Sprintf("join:%d", client.ID)),
100 client.Route,
101 []*CursorClient{ client },
102 }
103 case "char":
104 if len(args) < 2 { return }
105 // haha, turns out using ':' as a separator means you can't type ':'s
106 // i should really be writing byte packets, not this nonsense
107 msg := byte(':')
108 if len(args[1]) > 0 {
109 msg = args[1][0]
110 }
111 broadcast <- CursorMessage{
112 []byte(fmt.Sprintf("char:%d:%c", client.ID, msg)),
113 client.Route,
114 []*CursorClient{ client },
115 }
116 case "nochar":
117 broadcast <- CursorMessage{
118 []byte(fmt.Sprintf("nochar:%d", client.ID)),
119 client.Route,
120 []*CursorClient{ client },
121 }
122 case "click":
123 if len(args) < 2 { return }
124 click := 0
125 if args[1][0] == '1' {
126 click = 1
127 }
128 broadcast <- CursorMessage{
129 []byte(fmt.Sprintf("click:%d:%d", client.ID, click)),
130 client.Route,
131 []*CursorClient{ client },
132 }
133 case "pos":
134 if len(args) < 3 { return }
135 x, err := strconv.ParseFloat(args[1], 32)
136 y, err := strconv.ParseFloat(args[2], 32)
137 if err != nil { return }
138 client.X = float32(x)
139 client.Y = float32(y)
140 broadcast <- CursorMessage{
141 []byte(fmt.Sprintf("pos:%d:%f:%f", client.ID, client.X, client.Y)),
142 client.Route,
143 []*CursorClient{ client },
144 }
145 }
146}
147
148func Handler(app *model.AppState) http.HandlerFunc {
149 var upgrader = websocket.Upgrader{
150 CheckOrigin: func (r *http.Request) bool {
151 origin := r.Header.Get("Origin")
152 return origin == app.Config.BaseUrl
153 },
154 }
155
156 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
157 conn, err := upgrader.Upgrade(w, r, nil)
158 if err != nil {
159 log("Failed to upgrade to WebSocket connection: %v\n", err)
160 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
161 return
162 }
163 defer conn.Close()
164
165 client := CursorClient{
166 ID: rand.Int31(),
167 Conn: conn,
168 X: 0.0,
169 Y: 0.0,
170 Disconnected: false,
171 }
172
173 err = client.Conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("id:%d", client.ID)))
174 if err != nil {
175 client.Conn.Close()
176 return
177 }
178
179 mutex.Lock()
180 clients[client.ID] = &client
181 mutex.Unlock()
182
183 // log("Client connected: %s (%s)", fmt.Sprintf("0x%08x", client.ID), client.Conn.RemoteAddr().String())
184
185 for {
186 if client.Disconnected {
187 mutex.Lock()
188 delete(clients, client.ID)
189 client.Conn.Close()
190 mutex.Unlock()
191 return
192 }
193 handleClient(&client)
194 }
195 })
196}
197
198func log(format string, args ...any) {
199 logString := fmt.Sprintf(format, args...)
200 fmt.Printf("[%s] [CURSOR] %s\n", time.Now().Format(time.UnixDate), logString)
201}