-
Couldn't load subscription status.
- Fork 47
Add a bidirectional chat sample to demonstrate SignalR Trigger #101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
cd4afc9
Add a bi-directional chat sample
zackliu be8280b
stash
zackliu fe2c0af
Merge branch 'dev' of github.com:Azure/azure-functions-signalrservice…
zackliu 2519ca3
Merge branch 'dev' into trigger8
zackliu 18dffdc
Merge branch 'dev' into trigger8
zackliu 432cf6d
Simplify trigger usage in C#
zackliu 2f69300
Hide connectionstring
zackliu 31fe376
Add tests and update according to comments
zackliu 3cad150
Improve class based model
zackliu 4ff7265
Fix typo
zackliu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,239 @@ | ||
| <html> | ||
|
|
||
| <head> | ||
| <title>Serverless Chat</title> | ||
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css"> | ||
| <script> | ||
| window.apiBaseUrl = 'http://localhost:7071'; | ||
| </script> | ||
| <style> | ||
| .slide-fade-enter-active, | ||
| .slide-fade-leave-active { | ||
| transition: all 1s ease; | ||
| } | ||
|
|
||
| .slide-fade-enter, | ||
| .slide-fade-leave-to { | ||
| height: 0px; | ||
| overflow-y: hidden; | ||
| opacity: 0; | ||
| } | ||
| </style> | ||
| </head> | ||
|
|
||
| <body> | ||
| <p> </p> | ||
| <div id="app" class="container"> | ||
| <h3>Serverless chat</h3> | ||
| <div class="row" v-if="ready"> | ||
| <div class="signalr-demo col-sm"> | ||
| <hr /> | ||
| <div id='groupchecked'> | ||
| <input type="checkbox" id="checkbox" v-model="checked"> | ||
| <label for="checkbox">Send To Default Group: {{ this.defaultgroup }}</label> | ||
| </div> | ||
| <form v-on:submit.prevent="sendNewMessage(checked)"> | ||
| <input type="text" v-model="newMessage" id="message-box" class="form-control" placeholder="Type message here..." /> | ||
| </form> | ||
| </div> | ||
| </div> | ||
| <div class="row" v-if="!ready"> | ||
| <div class="col-sm"> | ||
| <div>Loading...</div> | ||
| </div> | ||
| </div> | ||
| <div v-if="ready"> | ||
| <transition-group name="slide-fade" tag="div"> | ||
| <div class="row" v-for="message in messages" v-bind:key="message.id"> | ||
| <div class="col-sm"> | ||
| <hr /> | ||
| <div> | ||
| <div style="display: inline-block; padding-left: 12px;"> | ||
| <div> | ||
| <a href="#" v-on:click.prevent="sendPrivateMessage(message.Sender)"> | ||
| <span class="text-info small"> | ||
| <strong>{{ message.Sender || message.sender }}</strong> | ||
| </span> | ||
| </a> | ||
| <span v-if="message.ConnectionId || message.connectionId"> | ||
| <a href="#" v-on:click.prevent="sendToConnection(message.ConnectionId || message.connectionId)"> | ||
| <span class="badge badge-primary">Connection: {{ message.ConnectionId || message.connectionId }}</span> | ||
| </a> | ||
| </span> | ||
| <a href="#" v-on:click.prevent="addUserToGroup(message.Sender || message.sender)"> | ||
| <span class="badge badge-primary">AddUserToGroup</span> | ||
| </a> | ||
| <a href="#" v-on:click.prevent="removeUserFromGroup(message.Sender || message.sender)"> | ||
| <span class="badge badge-primary">RemoveUserFromGroup</span> | ||
| </a> | ||
| <a href="#" v-on:click.prevent="addConnectionToGroup(message.ConnectionId || message.connectionId)"> | ||
| <span v-if="message.ConnectionId || message.connectionId" class="badge badge-primary">AddConnectionToGroup</span> | ||
| </a> | ||
| <a href="#" v-on:click.prevent="removeConnectionIdFromGroup(message.ConnectionId || message.connectionId)"> | ||
| <span v-if="message.ConnectionId || message.connectionId" class="badge badge-primary">RemoveConnectionFromGroup</span> | ||
| </a> | ||
| <span v-if="message.IsPrivate || message.isPrivate" class="badge badge-secondary">private message | ||
| </span> | ||
| </div> | ||
| <div> | ||
| {{ message.Text || message.text }} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </transition-group> | ||
| </div> | ||
|
|
||
| <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js"></script> | ||
| <script src="https://cdn.jsdelivr.net/npm/@aspnet/signalr@1.0.3/dist/browser/signalr.js"></script> | ||
| <script src="https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js"></script> | ||
| <script src="https://cdn.jsdelivr.net/npm/crypto-js@3.1.9-1/crypto-js.js"></script> | ||
| <script src="https://cdn.jsdelivr.net/npm/crypto-js@3.1.9-1/enc-base64.js"></script> | ||
| <script> | ||
| const data = { | ||
| username: '', | ||
| defaultgroup: 'AzureSignalR', | ||
| checked: false, | ||
| newMessage: '', | ||
| messages: [], | ||
| myConnectionId: '', | ||
| ready: false | ||
| }; | ||
| const app = new Vue({ | ||
| el: '#app', | ||
| data: data, | ||
| methods: { | ||
| sendNewMessage: function (isToGroup) { | ||
| if (isToGroup) { | ||
| connection.invoke("sendToGroup", this.defaultgroup, this.newMessage); | ||
| } | ||
| else { | ||
| connection.invoke("broadcast", this.newMessage); | ||
| } | ||
| this.newMessage = ''; | ||
| }, | ||
| sendPrivateMessage: function (user) { | ||
| const messageText = prompt('Send private message to ' + user); | ||
|
|
||
| if (messageText) { | ||
| connection.invoke("sendToUser", user, messageText); | ||
| } | ||
| }, | ||
| sendToConnection: function (connectionId) { | ||
| const messageText = prompt('Send private message to connection ' + connectionId); | ||
|
|
||
| if (messageText) { | ||
| connection.invoke("sendToConnection", connectionId, messageText); | ||
| } | ||
| }, | ||
| addConnectionToGroup: function(connectionId) { | ||
| confirm('Add connection ' + connectionId + ' to group: ' + this.defaultgroup); | ||
| connection.invoke("joinGroup", connectionId, this.defaultgroup); | ||
| }, | ||
| addUserToGroup: function (user) { | ||
| r = confirm('Add user ' + user + ' to group: ' + this.defaultgroup); | ||
| connection.invoke("joinUserToGroup", user, this.defaultgroup); | ||
| }, | ||
| removeConnectionIdFromGroup: function(connectionId) { | ||
| confirm('Remove connection ' + connectionId + ' from group: ' + this.defaultgroup); | ||
| connection.invoke("leaveGroup", connectionId, this.defaultgroup); | ||
| }, | ||
| removeUserFromGroup: function(user) { | ||
| confirm('Remove user ' + user + ' from group: ' + this.defaultgroup); | ||
| connection.invoke("leaveUserFromGroup", user, this.defaultgroup); | ||
| } | ||
| } | ||
| }); | ||
| const apiBaseUrl = prompt('Enter the Azure Function app base URL', window.apiBaseUrl); | ||
| data.username = prompt("Enter your username"); | ||
| const isAdmin = confirm('Work as administrator? (only an administrator can broadcast messages)'); | ||
| if (!data.username) { | ||
| alert("No username entered. Reload page and try again."); | ||
| throw "No username entered"; | ||
| } | ||
| const connection = new signalR.HubConnectionBuilder() | ||
| .withUrl(apiBaseUrl + '/api', { | ||
| accessTokenFactory: () => { | ||
| return generateAccessToken(data.username) | ||
| } | ||
| }) | ||
| .configureLogging(signalR.LogLevel.Information) | ||
| .build(); | ||
| connection.on('newMessage', onNewMessage); | ||
| connection.on('newConnection', onNewConnection) | ||
| connection.onclose(() => console.log('disconnected')); | ||
| console.log('connecting...'); | ||
| connection.start() | ||
| .then(() => { | ||
| data.ready = true; | ||
| console.log('connected!'); | ||
| }) | ||
| .catch(console.error); | ||
| function getAxiosConfig() { | ||
| const config = { | ||
| headers: { | ||
| 'x-ms-signalr-user-id': data.username, | ||
| 'Authorization': 'Bearer ' + generateAccessToken(data.username) | ||
| } | ||
| }; | ||
| return config; | ||
| } | ||
| let counter = 0; | ||
| function onNewMessage(message) { | ||
| message.id = counter++; // vue transitions need an id | ||
| data.messages.unshift(message); | ||
| }; | ||
| function onNewConnection(message) { | ||
| data.myConnectionId = message.ConnectionId; | ||
| } | ||
|
|
||
| function base64url(source) { | ||
| // Encode in classical base64 | ||
| encodedSource = CryptoJS.enc.Base64.stringify(source); | ||
|
|
||
| // Remove padding equal characters | ||
| encodedSource = encodedSource.replace(/=+$/, ''); | ||
|
|
||
| // Replace characters according to base64url specifications | ||
| encodedSource = encodedSource.replace(/\+/g, '-'); | ||
| encodedSource = encodedSource.replace(/\//g, '_'); | ||
|
|
||
| return encodedSource; | ||
| } | ||
|
|
||
| // this function should be in auth server, do not expose your secret | ||
| function generateAccessToken(userName) { | ||
| var header = { | ||
| "alg": "HS256", | ||
| "typ": "JWT" | ||
| }; | ||
|
|
||
| var stringifiedHeader = CryptoJS.enc.Utf8.parse(JSON.stringify(header)); | ||
| var encodedHeader = base64url(stringifiedHeader); | ||
|
|
||
| // customize your JWT token payload here | ||
| var data = { | ||
| "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": userName, | ||
| "exp": 1699819025, | ||
| 'admin': isAdmin | ||
| }; | ||
|
|
||
| var stringifiedData = CryptoJS.enc.Utf8.parse(JSON.stringify(data)); | ||
| var encodedData = base64url(stringifiedData); | ||
|
|
||
| var token = encodedHeader + "." + encodedData; | ||
|
|
||
| var secret = "myfunctionauthtest"; // do not expose your secret here | ||
|
|
||
| var signature = CryptoJS.HmacSHA256(token, secret); | ||
| signature = base64url(signature); | ||
|
|
||
| var signedToken = token + "." + signature; | ||
|
|
||
| return signedToken; | ||
| } | ||
| </script> | ||
| </body> | ||
|
|
||
| </html> | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to do a camel case conversion for class members? (ConnectionId -> connectionId) I think SignalR by default does this conversion.