github.com/angenalZZZ/gofunc@v0.0.0-20210507121333-48ff1be3917b/http/longpoll/examples/advanced/advanced.go (about) 1 // This is a more advanced example that shows a few more possibilities when 2 // using longpoll. 3 // 4 // In this example, we'll demonstrate publishing events via http handlers and 5 // also adding additional logic/validation on top of the LongpollManager's 6 // SubscriptionHandler function. 7 // 8 // To run this example: 9 // go build examples/advanced/advanced.go 10 // Then run the binary and visit http://127.0.0.1:8081/advanced 11 // Try clicking the action button with a variety of different actions and 12 // toggle whether or not they are public or private. Then switch to other 13 // users and do the same. Observe what you see. Better yet, have multiple 14 // browser windows open and click from the different users and observe. 15 // 16 // Noteworthy things going on in this example: 17 // - Event payloads are an actual json object, not a plain string. 18 // 19 // - we use closures to capture the LongpollManager to support calling 20 // Publish() from an http handler, and to wrap the SubscriptionHandler 21 // with our own logic. This is safe to have random http handlers 22 // calling functions on LongpollManager because the manager's data members 23 // are all channels which are made for sharing. 24 // 25 // - The html and javascript is not the prettiest :-P 26 // 27 package main 28 29 import ( 30 "fmt" 31 "github.com/angenalZZZ/gofunc/http/longpoll" 32 "log" 33 "net/http" 34 ) 35 36 func main() { 37 manager, err := longpoll.StartLongpoll(longpoll.Options{ 38 LoggingEnabled: true, 39 MaxLongpollTimeoutSeconds: 120, 40 MaxEventBufferSize: 100, 41 EventTimeToLiveSeconds: 60 * 2, // Event's stick around for 2 minutes 42 DeleteEventAfterFirstRetrieval: false, 43 }) 44 if err != nil { 45 log.Fatalf("Failed to create manager: %q", err) 46 } 47 // Serve our example driver webpage 48 http.HandleFunc("/advanced", AdvancedExampleHomepage) 49 // Serve handler that generates events 50 http.HandleFunc("/advanced/user/action", getUserActionHandler(manager)) 51 // Serve handler that subscribes to events. 52 http.HandleFunc("/advanced/events", getEventSubscriptionHandler(manager)) 53 // Start webserver 54 fmt.Println("Serving webpage at http://127.0.0.1:8081/advanced") 55 http.ListenAndServe("127.0.0.1:8081", nil) 56 57 // We'll never get here as long as http.ListenAndServe starts successfully 58 // because it runs until you kill the program (like pressing Control-C) 59 // Buf if you make a stoppable http server, or want to shut down the 60 // internal longpoll manager for other reasons, you can do so via 61 // Shutdown: 62 manager.Shutdown() // Stops the internal goroutine that provides subscription behavior 63 // Again, calling shutdown is a bit silly here since the goroutines will 64 // exit on main() exit. But I wanted to show you that it is possible. 65 } 66 67 // A fairly trivial json-convertable structure that demonstrates how events 68 // don't have to be a plain string. Anything JSON will work. 69 type UserAction struct { 70 User string `json:"user"` 71 Action string `json:"action"` 72 IsPublic bool `json:"is_public"` // Whether or not others can see this 73 } 74 75 // Creates a closure function that is used as an http handler that allows 76 // users to publish events (what this example is calling a user action event) 77 func getUserActionHandler(manager *longpoll.LongpollManager) func(w http.ResponseWriter, r *http.Request) { 78 // Creates closure that captures the LongpollManager 79 return func(w http.ResponseWriter, r *http.Request) { 80 user := r.URL.Query().Get("user") 81 action := r.URL.Query().Get("action") 82 public := r.URL.Query().Get("public") 83 // Perform validation on url query params: 84 if len(user) == 0 || len(action) == 0 { 85 w.WriteHeader(http.StatusBadRequest) 86 w.Write([]byte("Missing required URL param.")) 87 return 88 } 89 if user != "larry" && user != "moe" && user != "curly" { 90 w.WriteHeader(http.StatusBadRequest) 91 w.Write([]byte("Not a user.")) 92 return 93 } 94 if len(public) > 0 && public != "true" { 95 w.WriteHeader(http.StatusBadRequest) 96 w.Write([]byte("Optional param 'public' must be 'true' if present.")) 97 return 98 } 99 // convert string arg to bool 100 isPublic := false 101 if public == "true" { 102 isPublic = true 103 } 104 actionEvent := UserAction{User: user, Action: action, IsPublic: isPublic} 105 // Publish on public subscription channel if the action is public. 106 // Everyone can see this event. 107 if isPublic { 108 manager.Publish("public_actions", actionEvent) 109 } 110 // Publish on user's private channel regardless 111 // Only the user that called this will see the event. 112 manager.Publish(user+"_actions", actionEvent) 113 } 114 } 115 116 // Creates a closure function that is used as an http handler for browsers to 117 // subscribe to events via longpolling. 118 // Notice how we're wrapping LongpollManager.SubscriptionHandler in order to 119 // add our own logic and validation. 120 func getEventSubscriptionHandler(manager *longpoll.LongpollManager) func(w http.ResponseWriter, r *http.Request) { 121 // Creates closure that captures the LongpollManager 122 // Wraps the manager.SubscriptionHandler with a layer of dummy access control validation 123 return func(w http.ResponseWriter, r *http.Request) { 124 category := r.URL.Query().Get("category") 125 user := r.URL.Query().Get("user") 126 // NOTE: real user authentication should be used in the real world! 127 128 // Dummy user access control in the event the client is requesting 129 // a user's private activity stream: 130 if category == "larry_actions" && user != "larry" { 131 w.WriteHeader(http.StatusForbidden) 132 w.Write([]byte("You're not Larry.")) 133 return 134 } 135 if category == "moe_actions" && user != "moe" { 136 w.WriteHeader(http.StatusForbidden) 137 w.Write([]byte("You're not Moe.")) 138 return 139 } 140 if category == "curly_actions" && user != "curly" { 141 w.WriteHeader(http.StatusForbidden) 142 w.Write([]byte("You're not Curly.")) 143 return 144 } 145 146 // Only allow supported subscription categories: 147 if category != "public_actions" && category != "larry_actions" && 148 category != "moe_actions" && category != "curly_actions" { 149 w.WriteHeader(http.StatusBadRequest) 150 w.Write([]byte("Subscription channel does not exist.")) 151 return 152 } 153 154 // Client is either requesting the public stream, or a private 155 // stream that they're allowed to see. 156 // Go ahead and let the subscription happen: 157 manager.SubscriptionHandler(w, r) 158 } 159 } 160 161 // Here we're providing a webpage that lets you pick a user, perform an action 162 // and see the recent history (last 2 min) of all your actions and any public 163 // action by the other users. 164 // 165 // In this code you'll see a sample of how to implement longpolling on the 166 // client side in javascript. I used jquery here. There are TWO longpolls 167 // going on in this webpage: for your actions, and for everyone's public actions 168 // 169 // I was too lazy to serve this file statically. 170 // This is me setting a bad example :) 171 func AdvancedExampleHomepage(w http.ResponseWriter, r *http.Request) { 172 // Hacky way to inject the current user into the webpage: 173 username := r.URL.Query().Get("user") 174 if username == "" { 175 username = "curly" 176 } 177 fmt.Fprintf(w, ` 178 <html> 179 <head> 180 <title>golongpoll advanced example</title> 181 </head> 182 <body> 183 <h1>Hello, <script> document.write("%s"); </script></h1> 184 Switch to user: 185 <ul> 186 <li><a href="/advanced?user=curly">Curly</a></li> 187 <li><a href="/advanced?user=moe">Moe</a></li> 188 <li><a href="/advanced?user=larry">Larry</a></li> 189 </ul> 190 191 <div> 192 <h3>Try doing something:</h3> 193 <input type="radio" name="actionGroup" value="punch"> Punch<br> 194 <input type="radio" name="actionGroup" value="slap" checked> Slap<br> 195 <input type="radio" name="actionGroup" value="poke"> Poke<br> 196 <input type="radio" name="actionGroup" value="nuk nuk nuk"> Say: Nuk Nuk Nuk!<br><br> 197 <input type="checkbox" id="isPublic" value="true"> Let others see that I did this.<br> 198 <input type="button" id="action-button" value="Do it!"> 199 </div> 200 <hr> 201 <h3>Activity Stream</h3> 202 <table border="1"> 203 <tr> 204 <th>Your actions</th> 205 <th>Everyone's public actions</th> 206 </tr> 207 <tr> 208 <td style="vertical-align:top;"> 209 <table border="1" id="your-actions"> 210 </table> 211 </td> 212 <td style="vertical-align:top;"> 213 <table border="1" id="public-actions"> 214 </table> 215 </td> 216 </tr> 217 </table> 218 219 <script src="http://code.jquery.com/jquery-1.11.3.min.js"></script> 220 <script> 221 // This is a bunch of copy-n-paste hackathon javascript that is not good form 222 // The point of this example is to demonstrate the longpoll usage, not how 223 // to write good js/html 224 225 // for browsers that don't have console 226 if(typeof window.console == 'undefined') { window.console = {log: function (msg) {} }; } 227 228 // Start checking for any events that occurred within 2 minutes prior to page load 229 // so you can switch pages to other users, and then come back and see 230 // recent events: 231 var yourActionsSinceTime = (new Date(Date.now() - 120000)).getTime();; 232 233 // Let's subscribe to your events. 234 var yourActionsCategory = "%s_actions"; 235 236 // Longpoll subscription for your actions. this will show both public and 237 // private events because that's what the server is publishing on this 238 // category, and only you are allowed to access it. 239 (function pollYourActions() { 240 var timeout = 45; // in seconds 241 var optionalSince = ""; 242 if (yourActionsSinceTime) { 243 optionalSince = "&since_time=" + yourActionsSinceTime; 244 } 245 var pollUrl = "/advanced/events?user=%s&timeout=" + timeout + "&category=" + yourActionsCategory + optionalSince; 246 // how long to wait before starting next longpoll request in each case: 247 var successDelay = 10; // 10 ms 248 var errorDelay = 3000; // 3 sec 249 $.ajax({ url: pollUrl, 250 success: function(data) { 251 if (data && data.events && data.events.length > 0) { 252 // got events, process them 253 // NOTE: these events are in chronological order (oldest first) 254 for (var i = 0; i < data.events.length; i++) { 255 // Display event 256 var event = data.events[i]; 257 var publicString = "(public)"; 258 if (event.data.is_public === false) { 259 var publicString = "(private)"; 260 } 261 // prepend instead of append so newest is up top--easier to see with no scrolling 262 $("#your-actions").prepend("<tr><td>" + event.data.user + ": " + event.data.action + " " + publicString + " at " + (new Date(event.timestamp).toLocaleTimeString()) + "</td></tr>") 263 // Update sinceTime to only request events that occurred after this one. 264 yourActionsSinceTime = event.timestamp; 265 } 266 // success! start next longpoll 267 setTimeout(pollYourActions, successDelay); 268 return; 269 } 270 if (data && data.timeout) { 271 console.log("No events, checking again."); 272 // no events within timeout window, start another longpoll: 273 setTimeout(pollYourActions, successDelay); 274 return; 275 } 276 if (data && data.error) { 277 console.log("Error response: " + data.error); 278 console.log("Trying again shortly...") 279 setTimeout(pollYourActions, errorDelay); 280 return; 281 } 282 // We should have gotten one of the above 3 cases: 283 // either nonempty event data, a timeout, or an error. 284 console.log("Didn't get expected event data, try again shortly..."); 285 setTimeout(pollYourActions, errorDelay); 286 }, dataType: "json", 287 error: function (data) { 288 console.log("Error in ajax request--trying again shortly..."); 289 setTimeout(pollYourActions, errorDelay); // 3s 290 } 291 }); 292 })(); 293 294 // Add another longpoller for all user's public events: 295 var publicActionsSinceTime = (new Date(Date.now() - 120000)).getTime();; 296 var publicActionsCategory = "public_actions"; 297 298 // Longpoll subscription for everyone's (public) actions. 299 // You wont see other people's private actions 300 (function pollPublicActions() { 301 var timeout = 45; // in seconds 302 var optionalSince = ""; 303 if (publicActionsSinceTime) { 304 optionalSince = "&since_time=" + publicActionsSinceTime; 305 } 306 var pollUrl = "/advanced/events?user=%s&timeout=" + timeout + "&category=" + publicActionsCategory + optionalSince; 307 // how long to wait before starting next longpoll request in each case: 308 var successDelay = 10; // 10 ms 309 var errorDelay = 3000; // 3 sec 310 $.ajax({ url: pollUrl, 311 success: function(data) { 312 if (data && data.events && data.events.length > 0) { 313 // got events, process them 314 // NOTE: these events are in chronological order (oldest first) 315 for (var i = 0; i < data.events.length; i++) { 316 // Display event 317 var event = data.events[i]; 318 var publicString = "(public)"; 319 if (event.data.is_public === false) { 320 var publicString = "(private)"; 321 } 322 // prepend instead of append so newest is up top--easier to see with no scrolling 323 $("#public-actions").prepend("<tr><td>" + event.data.user + ": " + event.data.action + " " + publicString + " at " + (new Date(event.timestamp).toLocaleTimeString()) + "</td></tr>") 324 // Update sinceTime to only request events that occurred after this one. 325 publicActionsSinceTime = event.timestamp; 326 } 327 // success! start next longpoll 328 setTimeout(pollPublicActions, successDelay); 329 return; 330 } 331 if (data && data.timeout) { 332 console.log("No events, checking again."); 333 // no events within timeout window, start another longpoll: 334 setTimeout(pollPublicActions, successDelay); 335 return; 336 } 337 if (data && data.error) { 338 console.log("Error response: " + data.error); 339 console.log("Trying again shortly...") 340 setTimeout(pollPublicActions, errorDelay); 341 return; 342 } 343 // We should have gotten one of the above 3 cases: 344 // either nonempty event data, a timeout, or an error. 345 console.log("Didn't get expected event data, try again shortly..."); 346 setTimeout(pollPublicActions, errorDelay); 347 }, dataType: "json", 348 error: function (data) { 349 console.log("Error in ajax request--trying again shortly..."); 350 setTimeout(pollPublicActions, errorDelay); // 3s 351 } 352 }); 353 })(); 354 355 356 // Click handler for action button. This hits the http handler that publishes 357 // events. 358 $( "#action-button" ).click(function() { 359 var actionString = $('input:radio[name=actionGroup]:checked').val(); 360 var optionalPublic = ""; 361 if ($("#isPublic").is(':checked')) { 362 optionalPublic = "&public=true"; 363 } 364 var actionSubmitUrl = "/advanced/user/action?user=%s&action=" + actionString + optionalPublic; 365 366 $.ajax({ url: actionSubmitUrl, 367 success: function(data) { 368 console.log("action submitted"); 369 }, dataType: "html", 370 error: function (data) { 371 alert("Action failed due to error."); 372 } 373 }); 374 }); 375 376 </script> 377 </body> 378 </html>`, username, username, username, username, username) 379 // Those ugly, repeated username params are all populating some %s placeholder 380 // throughout our html/javascript. 381 }