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  }