I am currently developing a prototype to communicate data between a chatbot and website elements. I am using the Azure Bot Services BotFrameworkAdapter and DirectLine in order to communicate between the two applications.
I am having a problem which I have narrowed down to the 'pollingInterval' property of the DirectLine object. The documentation says:
Clients that poll using HTTP GET should choose a polling interval that matches their intended use.
Service-to-service applications often use a polling interval of 5s or 10s.
Client-facing applications often use a polling interval of 1s, and issue a single additional request shortly after every message that the client sends (to rapidly retrieve a bot's response). This delay can be as short at 300ms but should be tuned based on the bot's speed and transit time. Polling should not be more frequent than once per second for any extended period of time.
To my understanding this is how the DirectLine object receives events from the bot, however I only need the event to trigger once, and not on the polling interval. It seems like there is no way to say "I am finished with this event" and move on. As soon as it is triggered once, it is continuously triggered which is causing issues with the functionality of the application.
I have this BotConnection object that is used to create a DirectLine instance and subscribe event handlers to the received events:
import { DirectLine } from 'botframework-directlinejs';
export default class BotConnection {
constructor() {
//Connect to directline object hosted by the bot
this.connection = new DirectLine({
domain: 'http://localhost:3001/directline',
pollingInterval: 1000,
webSocket: false
})
}
getConnection() {
return this.connection;
}
subscribeEventHandler(name, handle) {
console.log('subscribeEventHandler');
this.connection.activity$
.filter(activity => activity.type === "event" && activity.name === name)
.subscribe(activity => handle(activity));
}
}
I am implementing the botConnection class in my App file like so:
props.botConnection.subscribeEventHandler('changePage', () => console.log('test'));
My Bot file takes a message and sends an event to the page that should be handled once on the client application:
const { ActivityHandler } = require('botbuilder');
class MyBot extends ActivityHandler {
constructor() {
super();
this.onMessage(async (context, next) => {
//Testing directline back-channel functionality
if(context.activity.text === "Change page") {
await context.sendActivity({type: 'event', name: 'changePage', value: '/test'});
}
await next();
}
}
Any help with this issue would be fantastic. I am unsure if there is something supported by Azure or if there is some custom magic that I need to do.
Thanks
Related
Im currently implementing a Microservice CQRS architecture with NestJs.
Microservices currently talk through RabbitMQ with each other.
I now want to introduce a BFF.
Problem example: SignUp
When the user signs up, the bff calls the procedure on the auth microservice which dispatches the SignUpUserCommand.
After checking and validating, the bff gets a 201 response (Commands don't return information). The UserSignedUpEvent is triggered inside auth microservice and now persists the user into the database and generates an access and refresh token.
How can the BFF now receive the event result (tokens)? I know it's a practice to let the bff listen to events. Does this mean that the bff must be able to uniquely identify the exact event which corresponds the the api call?
What is a good way to implement the event listening?
You should decide, are u going to use commands or events. It depends on which type of communication you try to achieve, request/response via message broker or publish/subscribe.
Request/Response Approach with Commands:
From Nestjs docs
To enable the request-response message type, Nest creates two logical channels - one is responsible for transferring the data while the other waits for incoming responses.
I don't have good experience with RMQ and mostly I'm using Kafka, but the approach is the same. if you put SignUpUserCommand in registration topic, you get the reply in registration.replay topic. Nest will automatically take care of it .
From Bff you can use:
this.client.send(topic, payload) // Observable is returned and you can subscribe to it.
Auth microservice:
#MessagePattern('registration')
async signUp(data: ...)
Publish/Subscribe Approach with Events:
Bff
async signUp(){
this.client.emit('registration', payload)
}
#EventPattern('registration.complete')
async signUpComplete(data: ...)
Auth microservice:
#EventPattern('registration')
async signUp(data: ...)
async signUpComplete(){
this.client.emit('registration.complete', payload)
}
Just see the difference between send and emit in Nestjs docs.
Thank you for your response/s!
Yes I did it like that now:
The BFF calls the Microservice (RPC) via the rabbit message queue:
// BFF Application / authentication.controller.ts
constructor(private readonly authenticationService: AuthenticationService) {}
#Post('/local/signup')
public async signupLocal(#Body() signUpDto: SignUpDto): Promise<AuthTokensDto> {
return await this.authenticationService.signUpUser(signUpDto);
}
// BFF Application / authentication.service.ts
constructor(
#Inject(RmqChannels.AUTH_SERVICE) private readonly authClient: ClientProxy,
private readonly apiService: ApiService,
) {}
signUpUser(signUpUserDto: SignUpDto): Promise<AuthTokensDto> {
return this.apiService.requestTo<AuthTokensDto>(
this.authClient, // Which microservice to talk to
signUpUserDto, // Payload for the rpc
RmqRpcPatterns.SIGN_UP_USER, // The rpc signature
RmqApiEventPatterns.SIGNED_UP_USER, // Wait for this event to get response
);
}
Inside the BFF Application there is a api.service.ts and an api.controller.ts.
The api.service.ts just forwards the communication. The api.controller.ts just handles the api events and adds them to the event observable:
// BFF Application / api.controller.ts
constructor(private apiService: ApiService, private rmqService: RmqService) {}
#EventPattern(RmqApiEventPatterns.EVENT)
async handleEvent(
#Payload() payload: RmqEventPayload,
#Ctx() ctx?: RmqContext): Promise<void> {
this.apiService.pushEvent(payload);
if (ctx) this.rmqService.ack(ctx);
return;
}
// Note: Its listening for 'api_event' pattern
// The exact Api Event Type is inside the RmqEventPayload (signed_up, user_updated,...)
Now the delicate part: The api.service.ts makes any type of rpc and listens for a correlating api event (rabbit event).
// BFF Application / api.service.ts
private events: Subject<RmqEventPayload> = new Subject();
private events$ = this.events.asObservable();
public pushEvent(event: RmqEventPayload): void {
this.events.next(event);
}
requestTo<T>(
client: ClientProxy,
dto: any,
rpc: RmqRpcPatterns,
apiEvent: RmqApiEventPatterns,
): Promise<T> {
return new Promise(async (resolve, reject) => {
// Generating requestId to identify upcoming api event
const requestId = uuidv4();
// Creating the rpc payload
const payload: RmqRpcPayload = {
requestId,
dto,
};
// Waiting for the associated api event of the to be scheduled rpc call
this.notifyWhen<T>(apiEvent, requestId).then((result) => {
resolve(result);
});
// Invoking the remote procedure
const result = await lastValueFrom(client.send(rpc, payload));
// It result is not true (error in command execution), throw the error it returned
if (result !== true) {
// If the result is a known exception code, throw it
if (Object.values(ExceptionTypesCode).includes(result)) {
reject(ExceptionUtils.ToHttpException(result));
} else {
// else just throw the unknown result
reject(result);
}
}
});
}
public notifyWhen<T>(
pattern: RmqApiEventPatterns,
requestId: string,
): Promise<T> {
// Returning a promise which waits for the associated api event and parses the result dto
return firstValueFrom(
this.events$.pipe(
filter(
(apiEvent: RmqEventPayload) =>
apiEvent.eventPattern === pattern &&
apiEvent.requestId === requestId,
),
map((apiEvent: RmqEventPayload) => apiEvent.dto),
),
);
}
The Microservice RPC returns true when no (rpc) exception was thrown in the command validation process (auth, integrity,...).
When some other Exception was thrown, it translates it to a http exception and throws it
When the async data access/manipulation on the microservice is done, an Rabbit Event is emitted. In the example of the sign up process:
// Microservice Application / signed_up.handler.ts
constructor(
#Inject(RmqChannels.API_SERVICE) private readonly client: ClientProxy,
) {}
// ... done
const payload: RmqEventPayload = {
requestId: event.requestId,
dto: authTokens,
eventPattern: RmqApiEventPatterns.SIGNED_UP_USER,
};
this.client.emit(RmqApiEventPatterns.EVENT, payload);
Hope it's clear enough how the mechanism work.
Here a diagram:
Inside the Firebase Console, under the Cloud Messaging view, users are able to create test notifications. This functionality also allows you to schedule the time at which the notification will send to a device or set of devices.
Is it possible to create and send scheduled FCM notifications to specific devices by using firebase cloud functions and the Firebase Admin SDK? Is there an alternative way to solving this?
The current way that I send scheduled messages to users is like so:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const schedule = require('node-schedule');
admin.initializeApp();
exports.setScheduledNotification = functions.https.onRequest(async (req, res) => {
const key = req.query.notification_key;
const message = {
notification: {
title: 'Test Notification',
body: 'Test Notification body.'
}
};
var currentDate = new Date();
var laterDate = new Date(currentDate.getTime() + (1 * 60000));
var job = schedule.scheduleJob(key, laterDate, () => {
const snapshot = admin.messaging().sendToDevice(key, message);
});
return res.status(200).send(`Message has been scheduled.`);
});
First of all, I am unsure how node-schedule interacts with firebase cloud functions. The logs appear that the function terminates very quickly which I would think would be correct. The longer the operation runs the more costly it is in our firebase bills. The notification does still run on a scheduled time though. I'm confused on how that all is working behind the scenes.
Secondly, I am having issues canceling these scheduled notifications. The notifications will most likely be on a 2hr timed schedule from the point it gets created. Before the 2hrs is up, I'd like the have the ability to cancel/overwrite the notification with an updated scheduled time.
I tried this to cancel the notification and it failed to find the previously created notification. Here is the code for that:
exports.cancelScheduledNotification = functions.https.onRequest(async (req, res) => {
const key = req.query.notification_key;
var job = schedule.scheduledJobs[key];
job.cancel();
return res.status(200).send(`Message has been canceled.`);
});
Is it possible to tap into the scheduling functionality of firebase cloud messaging outside of the firebase console? Or am I stuck with hacking my way around this issue?
A Cloud Function can run for a maximum of 9 minutes. So unless you're using node-schedule for periods shorter than that, your current approach won't work. Even if it would work, or if you are scheduling for less than 9 minutes in advance, using this approach is very uneconomic as you'll be paying for the Cloud Functions for all this time while it's waiting.
A more common approach is to store information about what message you want to be delivered to whom at what time in a database, and then use regular scheduled functions to periodically check what messages to send. For more on this, see these previous questions:
Firebase scheduled notification in android
How to schedule push notifcations for react native expo?
Schedule jobs in Firebase
Ionic: Is it possible to delay incoming push FCM push notification from showing to my device until a specific time
Cloud Functions for Firebase trigger on time?
How to create cron jobs dynamically in firebase
A recent improvement on this is to use the Cloud Tasks API to programmatically schedule Cloud Functions to be called at a specific time with a specific payload, and then use that to send the message through FCM. Doug Stevenson wrote a great blog post about this here: How to schedule a Cloud Function to run in the future with Cloud Tasks (to build a Firestore document TTL). While the post is about deleting documents at a certain time, you can combine it with the previous approach to schedule FCM messages too.
Scheduling of tasks is now also described in the documentation on enqueueing functions with Cloud Tasks
A final option, and one I'd actually recommend nowadays, is to separate the delivery of the message from the display of the notification.
Display of data messages (unlike notification messages) is never handled by the system, and always left to your application. So you can deliver the FCM data message straight away that then contains the time to display the message, and then wake the device up to display the message (often called a local notification) at that time.
To make Frank's answer more tangible, I am including some sample code below for scheduled cloud functions, that can help you achieve the 'scheduled FCM notifications'.
You should store the information required to send your notification(s) in Firestore (e.g. the when-to-notify parameter and the FCM token(s) of the users you want to send the notification to) and run a cloud function every minute to evaluate if there is any notification that needs to be delivered.
The function checks what Firestore documents have a WhenToNofity parameter that is due, and send the notifications to the receiver tokens immediately. Once sent, the function sets the boolean 'notificationSent' to true, to avoid that the users receive the same notification again on the next iteration.
The code below achieves just that:
const admin = require('firebase-admin');
admin.initializeApp();
const database = admin.firestore();
exports.sendNotification = functions.pubsub.schedule('* * * * *').onRun(async (context) => {
//check whether notification should be sent
//send it if yes
const query = await database.collection("experiences")
.where("whenToNotify", '<=', admin.firestore.Timestamp.now())
.where("notificationSent", "==", false).get();
query.forEach(async snapshot => {
sendNotification(snapshot.data().tokens);
await database.doc('experiences/' + snapshot.id).update({
"notificationSent": true,
});
});
function sendNotification(tokens) {
let title = "INSERT YOUR TITLE";
let body = "INSERT YOUR BODY";
const message = {
notification: { title: title, body: body},
tokens: tokens,
android: {
notification: {
sound: "default"
}
},
apns: {
payload: {
aps: {
sound: "default"
}
}
}
};
admin.messaging().sendMulticast(message).then(response => {
return console.log("Successful Message Sent");
}).catch(error => {
console.log(error);
return console.log("Error Sending Message");
});
}
return console.log('End Of Function');
});
If you're unfamiliar with setting up cloud functions, you can check how to set them up here. Cloud functions require a billing account, but you get 1M cloud invocations per month for free, which is more than enough to cover the costs of this approach.
Once done, you can insert your function in the index.js file.
I have this simple command where I and one other user can only use it to set the "Playing" status of the bot, whilst it's pretty useful for changing it on the fly, I also want to set it when I start up the bot so that I don't have to set it myself every time I restart the bot. Is it possible I can implement this code in the Bot.cs file so that when it starts up, it has the status set and ready
[Command("status")]
public async Task SetBotStatus(CommandContext ctx, string message)
{
if (ctx.User.Id == 572877986223751188 || ctx.User.Id == 327845261692895232)
{
DiscordActivity activity = new DiscordActivity();
DiscordClient discord = ctx.Client;
activity.Name = message;
await discord.UpdateStatusAsync(activity);
return;
}
Basically, the way this current command works is that it's based on an if statement stating only I and another user can use this command. The command has a requirement that you pass over a string which is the text that will be applied to the status and that is simply updated using discord.UpdateStatusAsync
With Dsharp+ this is a pretty easy fix to implement!
In your Main Async method or in a global/public context, you will want to define your activity status which will be the default activity and then set a client-ready handler.
In this example I am using a sharded client, this is not necessary to do so yourself. Your bot must lose its presence in a server before the ready event will fire on starting so it will need to be offline for a couple of minutes for Discord to catch that the client is offline and revoke the presence of the bot from servers. This means in testing you must let the bot lose its presence from being offline, and in production your bot will not lose its set activity from a momentary disconnection or a couple of missed heartbeats.
If 5 consecutive heartbeats are missed the next successful heartbeat will fire the ready event resetting the bot's activity status to the default status. Dsharp+ will tell you this happened by warning you the client has become a zombie.
static async Task MainAsync()
{
DiscordActivity activity = new();
activity.Name = "Testing...";
activity.ActivityType = ActivityType.Streaming;
//The client I have here is sharded but that is not necessary.
var discord = new DiscordShardedClient(new DiscordConfiguration()
{
Token = botToken,
TokenType = botTokenType,
Intents = botIntents,
MinimumLogLevel = botLogLevel
});
// the handler for ready will set with UpdateStatusAsync()
discord.Ready += async (client, readyEventArgs) =>
await discord.UpdateStatusAsync(activity);
// StartAsync() MUST come after this and any other handler.
await discord.StartAsync();
await Task.Delay(-1);
}
I have built a slack bot using the slack/bots apis in node.js: https://slack.dev/bolt-js/tutorial/getting-started
Currently, it is working fine when I type <bot> help in a channel I have set up for using webhooks. I am trying to run those same commands in a DM with the bot using the app.event('app_mention',...) method but it is not working. its like the message doesn't register in a DM with the bot for some reason but it works in a public channel. code snippet below:
app.event('app_mention', async ({ event, client}) => {
console.log(event);
const text = event.text;
const parentMessageId = event.ts;
const eventChannel = event.channel;
if (text.includes("help")) {
console.log(event);
try {
await client.chat.postMessage({
channel: eventChannel,
text: helpMessage,
thread_ts: parentMessageId
});
} catch (err) {
console.error(err);
}
I should have permissions set up correctly as well. I basically have all the permissions that I can add for the bot
The documentation of the app_mention api specifically mentions that this event does not work with DMs.
Messages sent to your app in direct message conversations are not
dispatched via app_mention, whether the app is explicitly mentioned or
otherwise. Subscribe to message.im events to receive messages directed
to your bot user in direct message conversations.
Check here : https://api.slack.com/events/app_mention
I'm implementing Azure SignalR service in my ASP.NET Core 2.2 app with React front-end. When I send a message, I'm NOT getting any errors but my messages are not reaching the Azure SignalR service.
To be specific, this is a private chat application so when a message reaches the hub, I only need to send it to participants in that particular chat and NOT to all connections.
When I send a message, it hits my hub but I see no indication that the message is making it to the Azure Service.
For security, I use Auth0 JWT Token authentication. In my hub, I correctly see the authorized user claims so I don't think there's any issues with security. As I mentioned, the fact that I'm able to hit the hub tells me that the frontend and security are working fine.
In the Azure portal however, I see no indication of any messages but if I'm reading the data correctly, I do see 2 client connections which is correct in my tests i.e. two open browsers I'm using for testing. Here's a screen shot:
Here's my Startup.cs code:
public void ConfigureServices(IServiceCollection services)
{
// Omitted for brevity
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwtOptions => {
jwtOptions.Authority = authority;
jwtOptions.Audience = audience;
jwtOptions.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// Check to see if the message is coming into chat
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/im")))
{
context.Token = accessToken;
}
return System.Threading.Tasks.Task.CompletedTask;
}
};
});
// Add SignalR
services.AddSignalR(hubOptions => {
hubOptions.KeepAliveInterval = TimeSpan.FromSeconds(10);
}).AddAzureSignalR(Configuration["AzureSignalR:ConnectionString"]);
}
And here's the Configure() method:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Omitted for brevity
app.UseSignalRQueryStringAuth();
app.UseAzureSignalR(routes =>
{
routes.MapHub<Hubs.IngridMessaging>("/im");
});
}
Here's the method I use to map a user's connectionId to the userName:
public override async Task OnConnectedAsync()
{
// Get connectionId
var connectionId = Context.ConnectionId;
// Get current userId
var userId = Utils.GetUserId(Context.User);
// Add connection
var connections = await _myServices.AddHubConnection(userId, connectionId);
await Groups.AddToGroupAsync(connectionId, "Online Users");
await base.OnConnectedAsync();
}
Here's one of my hub methods. Please note that I'm aware a user may have multiple connections simultaneously. I just simplified the code here to make it easier to digest. My actual code accounts for users having multiple connections:
[Authorize]
public async Task CreateConversation(Conversation conversation)
{
// Get sender
var user = Context.User;
var connectionId = Context.ConnectionId;
// Send message to all participants of this chat
foreach(var person in conversation.Participants)
{
var userConnectionId = Utils.GetUserConnectionId(user.Id);
await Clients.User(userConnectionId.ToString()).SendAsync("new_conversation", conversation.Message);
}
}
Any idea what I'm doing wrong that prevents messages from reaching the Azure SignalR service?
It might be caused by misspelled method, incorrect method signature, incorrect hub name, duplicate method name on the client, or missing JSON parser on the client, as it might fail silently on the server.
Taken from Calling methods between the client and server silently fails
:
Misspelled method, incorrect method signature, or incorrect hub name
If the name or signature of a called method does not exactly match an appropriate method on the client, the call will fail. Verify that the method name called by the server matches the name of the method on the client. Also, SignalR creates the hub proxy using camel-cased methods, as is appropriate in JavaScript, so a method called SendMessage on the server would be called sendMessage in the client proxy. If you use the HubName attribute in your server-side code, verify that the name used matches the name used to create the hub on the client. If you do not use the HubName attribute, verify that the name of the hub in a JavaScript client is camel-cased, such as chatHub instead of ChatHub.
Duplicate method name on client
Verify that you do not have a duplicate method on the client that differs only by case. If your client application has a method called sendMessage, verify that there isn't also a method called SendMessage as well.
Missing JSON parser on the client
SignalR requires a JSON parser to be present to serialize calls between the server and the client. If your client doesn't have a built-in JSON parser (such as Internet Explorer 7), you'll need to include one in your application.
Update
In response to your comments, I would suggest you try one of the Azure SignalR samples, such as
Get Started with SignalR: a Chat Room Example to see if you get the same behavior.
Hope it helps!