Skip to content

Commit d90d9f8

Browse files
Merge pull request #271395 from vicancy/patch-14
Update tutorial-build-chat.md
2 parents 6c99363 + 3e87b4b commit d90d9f8

File tree

1 file changed

+278
-13
lines changed

1 file changed

+278
-13
lines changed

articles/azure-web-pubsub/tutorial-build-chat.md

Lines changed: 278 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ ms.author: lianwei
66
ms.service: azure-web-pubsub
77
ms.custom: devx-track-azurecli
88
ms.topic: tutorial
9-
ms.date: 12/21/2023
9+
ms.date: 04/11/2024
1010
---
1111

1212
# Tutorial: Create a chat app with Azure Web PubSub service
@@ -369,7 +369,6 @@ const { WebPubSubServiceClient } = require('@azure/web-pubsub');
369369
370370
const app = express();
371371
const hubName = 'Sample_ChatApp';
372-
const port = 8080;
373372
374373
let serviceClient = new WebPubSubServiceClient(process.env.WebPubSubConnectionString, hubName);
375374
@@ -397,7 +396,7 @@ Rerun the server by running `node server`.
397396
398397
# [Java](#tab/java)
399398
400-
First add Azure Web PubSub SDK dependency into the `dependencies` node of `pom.xml`:
399+
First add Azure Web PubSub SDK dependency and gson into the `dependencies` node of `pom.xml`:
401400
402401
```xml
403402
<!-- https://mvnrepository.com/artifact/com.azure/azure-messaging-webpubsub -->
@@ -406,6 +405,12 @@ First add Azure Web PubSub SDK dependency into the `dependencies` node of `pom.x
406405
<artifactId>azure-messaging-webpubsub</artifactId>
407406
<version>1.2.12</version>
408407
</dependency>
408+
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
409+
<dependency>
410+
<groupId>com.google.code.gson</groupId>
411+
<artifactId>gson</artifactId>
412+
<version>2.10.1</version>
413+
</dependency>
409414
```
410415
411416
Now let's add a `/negotiate` API to the `App.java` file to generate the token:
@@ -418,6 +423,9 @@ import com.azure.messaging.webpubsub.WebPubSubServiceClientBuilder;
418423
import com.azure.messaging.webpubsub.models.GetClientAccessTokenOptions;
419424
import com.azure.messaging.webpubsub.models.WebPubSubClientAccessToken;
420425
import com.azure.messaging.webpubsub.models.WebPubSubContentType;
426+
import com.google.gson.Gson;
427+
import com.google.gson.JsonElement;
428+
import com.google.gson.JsonObject;
421429
import io.javalin.Javalin;
422430
423431
public class App {
@@ -452,7 +460,10 @@ public class App {
452460
option.setUserId(id);
453461
WebPubSubClientAccessToken token = service.getClientAccessToken(option);
454462
ctx.contentType("application/json");
455-
String response = String.format("{\"url\":\"%s\"}", token.getUrl());
463+
Gson gson = new Gson();
464+
JsonObject jsonObject = new JsonObject();
465+
jsonObject.addProperty("url", token.getUrl());
466+
String response = gson.toJson(jsonObject);
456467
ctx.result(response);
457468
return;
458469
});
@@ -647,20 +658,20 @@ For now, you need to implement the event handler by your own in Java. The steps
647658
2. First we'd like to handle the abuse protection OPTIONS requests, we check if the header contains `WebHook-Request-Origin` header, and we return the header `WebHook-Allowed-Origin`. For simplicity for demo purpose, we return `*` to allow all the origins.
648659
```java
649660
650-
// validation: https://azure.github.io/azure-webpubsub/references/protocol-cloudevents#validation
661+
// validation: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#protection
651662
app.options("/eventhandler", ctx -> {
652663
ctx.header("WebHook-Allowed-Origin", "*");
653664
});
654665
```
655666
656667
3. Then we'd like to check if the incoming requests are the events we expect. Let's say we now care about the system `connected` event, which should contain the header `ce-type` as `azure.webpubsub.sys.connected`. We add the logic after abuse protection to broadcast the connected event to all clients so they can see who joined the chat room.
657668
```java
658-
// validation: https://azure.github.io/azure-webpubsub/references/protocol-cloudevents#validation
669+
// validation: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#protection
659670
app.options("/eventhandler", ctx -> {
660671
ctx.header("WebHook-Allowed-Origin", "*");
661672
});
662673
663-
// handle events: https://azure.github.io/azure-webpubsub/references/protocol-cloudevents#events
674+
// handle events: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#events
664675
app.post("/eventhandler", ctx -> {
665676
String event = ctx.header("ce-type");
666677
if ("azure.webpubsub.sys.connected".equals(event)) {
@@ -677,7 +688,7 @@ For now, you need to implement the event handler by your own in Java. The steps
677688
4. The `ce-type` of `message` event is always `azure.webpubsub.user.message`. Details see [Event message](./reference-cloud-events.md#message). We update the logic to handle messages that when a message comes in we broadcast the message in JSON format to all the connected clients.
678689
679690
```java
680-
// handle events: https://azure.github.io/azure-webpubsub/references/protocol-cloudevents#events
691+
// handle events: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#events
681692
app.post("/eventhandler", ctx -> {
682693
String event = ctx.header("ce-type");
683694
if ("azure.webpubsub.sys.connected".equals(event)) {
@@ -686,7 +697,12 @@ For now, you need to implement the event handler by your own in Java. The steps
686697
} else if ("azure.webpubsub.user.message".equals(event)) {
687698
String id = ctx.header("ce-userId");
688699
String message = ctx.body();
689-
service.sendToAll(String.format("{\"from\":\"%s\",\"message\":\"%s\"}", id, message), WebPubSubContentType.APPLICATION_JSON);
700+
Gson gson = new Gson();
701+
JsonObject jsonObject = new JsonObject();
702+
jsonObject.addProperty("from", id);
703+
jsonObject.addProperty("message", message);
704+
String messageToSend = gson.toJson(jsonObject);
705+
service.sendToAll(messageToSend, WebPubSubContentType.APPLICATION_JSON);
690706
}
691707
ctx.status(200);
692708
});
@@ -700,7 +716,7 @@ For now, you need to implement the event handler by your own in Python. The step
700716
701717
2. First we'd like to handle the abuse protection OPTIONS requests, we check if the header contains `WebHook-Request-Origin` header, and we return the header `WebHook-Allowed-Origin`. For simplicity for demo purpose, we return `*` to allow all the origins.
702718
```python
703-
# validation: https://azure.github.io/azure-webpubsub/references/protocol-cloudevents#validation
719+
# validation: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#protection
704720
@app.route('/eventhandler', methods=['OPTIONS'])
705721
def handle_event():
706722
if request.method == 'OPTIONS':
@@ -713,8 +729,8 @@ For now, you need to implement the event handler by your own in Python. The step
713729
714730
3. Then we'd like to check if the incoming requests are the events we expect. Let's say we now care about the system `connected` event, which should contain the header `ce-type` as `azure.webpubsub.sys.connected`. We add the logic after abuse protection:
715731
```python
716-
# validation: https://azure.github.io/azure-webpubsub/references/protocol-cloudevents#validation
717-
# handle events: https://azure.github.io/azure-webpubsub/references/protocol-cloudevents#events
732+
# validation: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#protection
733+
# handle events: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#events
718734
@app.route('/eventhandler', methods=['POST', 'OPTIONS'])
719735
def handle_event():
720736
if request.method == 'OPTIONS':
@@ -823,7 +839,7 @@ In this section, we use Azure CLI to set the event handlers and use [awps-tunnel
823839
824840
We set the URL template to use `tunnel` scheme so that Web PubSub routes messages through the `awps-tunnel`'s tunnel connection. Event handlers can be set from either the portal or the CLI as [described in this article](howto-develop-eventhandler.md#configure-event-handler), here we set it through CLI. Since we listen events in path `/eventhandler` as the previous step sets, we set the url template to `tunnel:///eventhandler`.
825841
826-
Use the Azure CLI [az webpubsub hub create](/cli/azure/webpubsub/hub#az-webpubsub-hub-update) command to create the event handler settings for the chat hub.
842+
Use the Azure CLI [az webpubsub hub create](/cli/azure/webpubsub/hub#az-webpubsub-hub-create) command to create the event handler settings for the `Sample_ChatApp` hub.
827843
828844
> [!Important]
829845
> Replace &lt;your-unique-resource-name&gt; with the name of your Web PubSub resource created from the previous steps.
@@ -884,6 +900,255 @@ Open `http://localhost:8080/index.html`. You can input your user name and start
884900
885901
<!-- Adding Lazy Auth part with `connect` handling -->
886902
903+
## Lazy Auth with `connect` event handler
904+
905+
In previous sections, we demonstrate how to use [negotiate](#add-negotiate-endpoint) endpoint to return the Web PubSub service URL and the JWT access token for the clients to connect to Web PubSub service. In some cases, for example, edge devices that have limited resources, clients might prefer direct connect to Web PubSub resources. In such cases, you can configure `connect` event handler to lazy auth the clients, assign user ID to the clients, specify the groups the clients join once they connect, configure the permissions the clients have and WebSocket subprotocol as the WebSocket response to the client, etc. Details please refer to [connect event handler spec](./reference-cloud-events.md#connect).
906+
907+
Now let's use `connect` event handler to acheive the similar as what the [negotiate](#add-negotiate-endpoint) section does.
908+
909+
### Update hub settings
910+
911+
First let's update hub settings to also include `connect` event handler, we need to also allow anonymous connect so that clients without JWT access token can connect to the service.
912+
913+
Use the Azure CLI [az webpubsub hub update](/cli/azure/webpubsub/hub#az-webpubsub-hub-update) command to create the event handler settings for the `Sample_ChatApp` hub.
914+
915+
> [!Important]
916+
> Replace &lt;your-unique-resource-name&gt; with the name of your Web PubSub resource created from the previous steps.
917+
918+
```azurecli-interactive
919+
az webpubsub hub update -n "<your-unique-resource-name>" -g "myResourceGroup" --hub-name "Sample_ChatApp" --allow-anonymous true --event-handler url-template="tunnel:///eventhandler" user-event-pattern="*" system-event="connected" system-event="connect"
920+
```
921+
922+
### Update upstream logic to handle connect event
923+
924+
Now let's update upstream logic to handle connect event. We could also remove the negotiate endpoint now.
925+
926+
As similar to what we do in negotiate endpoint as demo purpose, we also read id from the query parameters. In connect event, the original client query is preserved in connect event requet body.
927+
928+
# [C#](#tab/csharp)
929+
930+
Inside the class `Sample_ChatApp`, override `OnConnectAsync()` to handle `connect` event:
931+
932+
```csharp
933+
sealed class Sample_ChatApp : WebPubSubHub
934+
{
935+
private readonly WebPubSubServiceClient<Sample_ChatApp> _serviceClient;
936+
937+
public Sample_ChatApp(WebPubSubServiceClient<Sample_ChatApp> serviceClient)
938+
{
939+
_serviceClient = serviceClient;
940+
}
941+
942+
public override ValueTask<ConnectEventResponse> OnConnectAsync(ConnectEventRequest request, CancellationToken cancellationToken)
943+
{
944+
if (request.Query.TryGetValue("id", out var id))
945+
{
946+
return new ValueTask<ConnectEventResponse>(request.CreateResponse(userId: id.FirstOrDefault(), null, null, null));
947+
}
948+
949+
// The SDK catches this exception and returns 401 to the caller
950+
throw new UnauthorizedAccessException("Request missing id");
951+
}
952+
953+
public override async Task OnConnectedAsync(ConnectedEventRequest request)
954+
{
955+
Console.WriteLine($"[SYSTEM] {request.ConnectionContext.UserId} joined.");
956+
}
957+
958+
public override async ValueTask<UserEventResponse> OnMessageReceivedAsync(UserEventRequest request, CancellationToken cancellationToken)
959+
{
960+
await _serviceClient.SendToAllAsync(RequestContent.Create(
961+
new
962+
{
963+
from = request.ConnectionContext.UserId,
964+
message = request.Data.ToString()
965+
}),
966+
ContentType.ApplicationJson);
967+
968+
return new UserEventResponse();
969+
}
970+
}
971+
```
972+
973+
# [JavaScript](#tab/javascript)
974+
975+
Update server.js to handle the client connect event:
976+
977+
```javascript
978+
const express = require("express");
979+
const { WebPubSubServiceClient } = require("@azure/web-pubsub");
980+
const { WebPubSubEventHandler } = require("@azure/web-pubsub-express");
981+
982+
const app = express();
983+
const hubName = "Sample_ChatApp";
984+
985+
let serviceClient = new WebPubSubServiceClient(process.env.WebPubSubConnectionString, hubName);
986+
987+
let handler = new WebPubSubEventHandler(hubName, {
988+
path: "/eventhandler",
989+
handleConnect: async (req, res) => {
990+
if (req.context.query.id){
991+
res.success({ userId: req.context.query.id });
992+
} else {
993+
res.fail(401, "missing user id");
994+
}
995+
},
996+
onConnected: async (req) => {
997+
console.log(`${req.context.userId} connected`);
998+
},
999+
handleUserEvent: async (req, res) => {
1000+
if (req.context.eventName === "message")
1001+
await serviceClient.sendToAll({
1002+
from: req.context.userId,
1003+
message: req.data,
1004+
});
1005+
res.success();
1006+
},
1007+
});
1008+
app.use(express.static("public"));
1009+
app.use(handler.getMiddleware());
1010+
1011+
app.listen(8080, () => console.log("server started"));
1012+
```
1013+
1014+
# [Java](#tab/java)
1015+
Now let's add the logic to handle the connect event `azure.webpubsub.sys.connect`:
1016+
1017+
```java
1018+
1019+
// validation: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#protection
1020+
app.options("/eventhandler", ctx -> {
1021+
ctx.header("WebHook-Allowed-Origin", "*");
1022+
});
1023+
1024+
// handle events: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#connect
1025+
app.post("/eventhandler", ctx -> {
1026+
String event = ctx.header("ce-type");
1027+
if ("azure.webpubsub.sys.connect".equals(event)) {
1028+
String body = ctx.body();
1029+
System.out.println("Reading from request body...");
1030+
Gson gson = new Gson();
1031+
JsonObject requestBody = gson.fromJson(body, JsonObject.class); // Parse JSON request body
1032+
JsonObject query = requestBody.getAsJsonObject("query");
1033+
if (query != null) {
1034+
System.out.println("Reading from request body query:" + query.toString());
1035+
JsonElement idElement = query.get("id");
1036+
if (idElement != null) {
1037+
JsonArray idInQuery = query.get("id").getAsJsonArray();
1038+
if (idInQuery != null && idInQuery.size() > 0) {
1039+
String id = idInQuery.get(0).getAsString();
1040+
ctx.contentType("application/json");
1041+
Gson response = new Gson();
1042+
JsonObject jsonObject = new JsonObject();
1043+
jsonObject.addProperty("userId", id);
1044+
ctx.result(response.toJson(jsonObject));
1045+
return;
1046+
}
1047+
}
1048+
} else {
1049+
System.out.println("No query found from request body.");
1050+
}
1051+
ctx.status(401).result("missing user id");
1052+
} else if ("azure.webpubsub.sys.connected".equals(event)) {
1053+
String id = ctx.header("ce-userId");
1054+
System.out.println(id + " connected.");
1055+
ctx.status(200);
1056+
} else if ("azure.webpubsub.user.message".equals(event)) {
1057+
String id = ctx.header("ce-userId");
1058+
String message = ctx.body();
1059+
service.sendToAll(String.format("{\"from\":\"%s\",\"message\":\"%s\"}", id, message), WebPubSubContentType.APPLICATION_JSON);
1060+
ctx.status(200);
1061+
}
1062+
});
1063+
1064+
```
1065+
1066+
# [Python](#tab/python)
1067+
Now let's handle the system `connect` event, which should contain the header `ce-type` as `azure.webpubsub.sys.connect`. We add the logic after abuse protection:
1068+
1069+
```python
1070+
@app.route('/eventhandler', methods=['POST', 'OPTIONS'])
1071+
def handle_event():
1072+
if request.method == 'OPTIONS' or request.method == 'GET':
1073+
if request.headers.get('WebHook-Request-Origin'):
1074+
res = Response()
1075+
res.headers['WebHook-Allowed-Origin'] = '*'
1076+
res.status_code = 200
1077+
return res
1078+
elif request.method == 'POST':
1079+
user_id = request.headers.get('ce-userid')
1080+
type = request.headers.get('ce-type')
1081+
print("Received event of type:", type)
1082+
# Sample connect logic if connect event handler is configured
1083+
if type == 'azure.webpubsub.sys.connect':
1084+
body = request.data.decode('utf-8')
1085+
print("Reading from connect request body...")
1086+
query = json.loads(body)['query']
1087+
print("Reading from request body query:", query)
1088+
id_element = query.get('id')
1089+
user_id = id_element[0] if id_element else None
1090+
if user_id:
1091+
return {'userId': user_id}, 200
1092+
return 'missing user id', 401
1093+
elif type == 'azure.webpubsub.sys.connected':
1094+
return user_id + ' connected', 200
1095+
elif type == 'azure.webpubsub.user.message':
1096+
service.send_to_all(content_type="application/json", message={
1097+
'from': user_id,
1098+
'message': request.data.decode('UTF-8')
1099+
})
1100+
return Response(status=204, content_type='text/plain')
1101+
else:
1102+
return 'Bad Request', 400
1103+
1104+
```
1105+
1106+
---
1107+
1108+
### Update index.html to direct connect
1109+
1110+
Now let's update the web page to direct connect to Web PubSub service. One thing to mention is that now for demo purpose the Web PubSub service endpoint is hard-coded into the client code, please update the service hostname `<the host name of your service>` in the below html with the value from your own service. It might be still useful to fetch the Web PubSub service endpoint value from your server, it gives you more flexibility and controllability to where the client connects to.
1111+
1112+
```html
1113+
<html>
1114+
<body>
1115+
<h1>Azure Web PubSub Chat</h1>
1116+
<input id="message" placeholder="Type to chat...">
1117+
<div id="messages"></div>
1118+
<script>
1119+
(async function () {
1120+
// sample host: mock.webpubsub.azure.com
1121+
let hostname = "<the host name of your service>";
1122+
let id = prompt('Please input your user name');
1123+
let ws = new WebSocket(`wss://${hostname}/client/hubs/Sample_ChatApp?id=${id}`);
1124+
ws.onopen = () => console.log('connected');
1125+
1126+
let messages = document.querySelector('#messages');
1127+
1128+
ws.onmessage = event => {
1129+
let m = document.createElement('p');
1130+
let data = JSON.parse(event.data);
1131+
m.innerText = `[${data.type || ''}${data.from || ''}] ${data.message}`;
1132+
messages.appendChild(m);
1133+
};
1134+
1135+
let message = document.querySelector('#message');
1136+
message.addEventListener('keypress', e => {
1137+
if (e.charCode !== 13) return;
1138+
ws.send(message.value);
1139+
message.value = '';
1140+
});
1141+
})();
1142+
</script>
1143+
</body>
1144+
1145+
</html>
1146+
```
1147+
1148+
### Rerun the server
1149+
1150+
Now [rerun the server](#run-the-web-server) and visit the web page following the instructions before. If you've stopped `awps-tunnel`, please also [rerun the tunnel tool](#run-awps-tunnel-locally).
1151+
8871152
## Next steps
8881153
8891154
This tutorial provides you with a basic idea of how the event system works in Azure Web PubSub service.

0 commit comments

Comments
 (0)