Botkit 4 + SlackAdapter: Unable to get callbacks triggered for basic conversation - javascript

I’m having trouble getting basic conversation callbacks to be fired. Can anyone point me to a working sample with botkit 4 of getting responses from a conversation in Slack? I set up the SlackAdapter and used the SlackEventMiddleware and SlackMessageTypeMiddleware, but my callbacks aren’t getting called.
I took this basic code from the botkit docs and am calling it after a /slash command. The question is written, but no matter what I write, none of the callbacks are fired. I see the events coming to my server, but not to these call backs.
Here’s the code I’m testing with:
let convo = new BotkitConversation('cheese', controller)
await bot.startPrivateConversation(message.user)
// create a path for when a user says YES
convo.addMessage('You said yes! How wonderful.', 'yes_thread')
// create a path for when a user says NO
convo.addMessage('You said no, that is too bad.', 'no_thread')
// create a path where neither option was matched
// this message has an action field, which directs botkit to go back to the `default` thread after sending this message.
convo.addMessage('Sorry I did not understand.', 'bad_response')
// Create a yes/no question in the default thread...
convo.addQuestion(
'Do you like cheese?',
[
{
pattern: 'yes',
handler: async (response, convo, bot) => {
await convo.gotoThread('yes_thread')
}
},
{
pattern: 'no',
handler: async (response, convo, bot) => {
await convo.gotoThread('no_thread')
}
},
{
default: true,
handler: async (response, convo, bot) => {
await convo.gotoThread('bad_response')
}
}
],
'likes_cheese',
'default'
)
controller.addDialog(convo)
await bot.beginDialog(`cheese`)
Any help much appreciated!

So turns out something in my custom storage provider was interfering. I haven't gotten to the bottom of it yet, but when I commented out my storage provider, I was able to get conversation call backs.
e.g.
const controller: Botkit = new Botkit({
adapter: getSlackAdapter()
// TODO: This is causing conversation call backs to NOT fire, let's revisit this later. We may not actually need this.
//storage: services.storageService
})

Related

Delete Button/Component of a specifc message (discord.js)

I am now trying in vain to remove buttons from a bot message. I have created a command for this purpose. When executing it, it should at best remove a specific, the last or at least all buttons that are on a specific message.
I have tried various things, but in all attempts the buttons are not removed.
module.exports = {
category: 'Utilities',
description: 'Delete Buttons',
permissions: ['ADMINISTRATOR'],
callback: async ({ client, message }) => {
const channel = client.channels.cache.get('the channel id')
channel.messages.fetch('the message id').then(msg => msg.edit({components: []}));
console.log(msg)
}
}
When I try to do this, I get the following console error:
TypeError: Cannot read properties of undefined (reading 'messages')
When I try this, I neither get a console log, nor does the bot do anything ...
const { Client, Message } = require("discord.js");
module.exports = {
category: 'Utilities',
description: 'Delete Buttons',
permissions: ['ADMINISTRATOR'],
callback: async ({ client, message }) => {
client.on('ready', async () => {
const channel = client.channels.cache.get('the channel id')
channel.messages.fetch('the message id').then(msg => {
msg.edit({ components: [] })
});
},
)}
}
Maybe one of you knows a solution or an idea. :)
Thanks a lot!
When I try this, I neither get a console log, nor does the bot do anything
The second example does not do anything because you are creating a ready event handler on running your command. Meaning, it's waiting for the bot to once again be "ready", i.e. the state of having logged in to the API as it does on startup. But your bot is already ready, and will not become ready again until the next time it restarts, so nothing will happen.
As for the first example, the error you are getting suggests channel is undefined, meaning either:
A) You have the incorrect channel ID
- OR -
B) The specified channel is no longer in the channel cache
If you are 100% sure the ID is correct, we can assume the issue you are having is the latter (the channel not being in the cache). There are many ways to solve this, but one simple way is to simply fetch the channel similar to how you are trying to fetch the message. Here's an example:
const channel = await client.channels.fetch('the channel id');
const msg = await channel.messages.fetch('the message id');
msg.edit({components: []});
That should fix the issue. If it doesn't, then the issue is much more complicated and not enough information has been provided. Also note that in the above example, I swapped Promise syntax for async/await since you are using an async function anyways; I did this just to make this answer more readable, you can choose whichever format you prefer.

How to sync my saved data on apple devices with service worker?

I know that Background Sync API is not supported in the apple ecosystem, so how would you get around it and make a solution that would work in the apple ecosystem and other platforms as well, now i have a solution that uses Background Sync API and for some reason it literally does not do anything on IOS, it just saves the failed requests, and then never sync-s, could i just access the sync queue somehow, with a indexedDB wrapper and then sync at an arbitrary time?
I tried it once and it broke everything, do you guys have an idea how?
const bgSyncPlugin = new workbox.backgroundSync.Plugin('uploadQueue', {
maxRetentionTime: 60 * 24 * 60,
onSync: async ({ queue }) => {
return getAccessToken().then((token) => {
replayQueue(queue, token).then(() => {
return showNotification();
});
});
},
});
This is the code i have, they all. have a purpose, since my token has a timeout i have to check if the token is expired or not and proceed after that and replace the token in the headers if it is expired, and i have to change data as well when i sync in the request bodies, but it all works good on anything other than apple devices. Apple devices never trigger the onsync, i tried to do listen to fetch events and trigger onsync with:
self.registration.sync.register('uploadQueue');
But to no awail, i tried to register sync on servvice worker registration, nothing seems to help.
If the sync registration is not viable on ios, then can i access the upload queue table somehow?
P.S.: I`m using dexie.js as a indexedDB wrapper, it is a vue.js app, with laravel api, and the sync process is quite complex, but it is working, just have to figure out how to do it on IOS!
I have found an answer to this after like 2 weeks of it being on my mind and on my to do list.
Now get some popcorn and strap yourself the heck in, because this is quite a chonker.
In my case the sync process was pretty complex as my users could be away from any connection for such a long time that my accessTokens would expire so i had to do a check for the access token expiration as well and reFetch it.
Furthermore my users could add new people to the database of people, which all had their on unique server side id-s, so i had to order my requests in a way that the person registrations are sent first then the tasks and campaigns that were completed for them, so i can receive the respective ids from the API.
Now for the fun part:
Firstly you cant use a bgSyncPlugin, because you cant access the replayQueue, you have to use a normal queue, like this:
var bgSyncQueue = new workbox.backgroundSync.Queue('uploadQueue', {
maxRetentionTime: 60 * 24 * 60,
onSync: () => syncData(),
});
And push the failed requests to the queue inside the fetch listener:
this.onfetch = (event) => {
let requestClone = event.request.clone();
if (requestClone.method === 'POST' && 'condition to match the requests you need to replay') {
event.respondWith(
(() => {
const promiseChain = fetch(requestClone).catch(() => {
return bgSyncQueue.pushRequest(event);
});
event.waitUntil(promiseChain);
return promiseChain;
})()
);
} else {
event.respondWith(fetch(event.request));
}
};
When user has connection we trigger the "syncData()" function, on ios this is a bit complicated(more on this later), on android it happens automatically, as the service worker sees it has connection, now lets just check out what syncData does:
async function syncData() {
if (bgSyncQueue) //is there data to sync?
return getAccessToken() //then get the access token, if expired refresh it
.then((token) => replayQueue(bgSyncQueue, token).then(() => showNotification({ body: 'Succsesful sync', title: 'Data synced to server' })))
.catch(() => showNotification({ title: 'Sync unsuccessful', body: 'Please find and area with better coverage' })); //replay the requests and show a notification
return Promise.resolve('empty');//if no requests to replay return with empty
}
For the android/desktop side of thing we are finished you can be happy with your modified data being synced, now on iOS we cant just have the users data be uploaded only when they restart the PWA, thats bad user experience, but we are playing with javascript everything is possible in a way or another.
There is a message event that can be fired every time that the client code sees that it has internet, which looks like this:
if (this.$online && this.isIOSDevice) {
if (window.MessageChannel) {
var messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event) => {
this.onMessageSuccess(event);
};
} else {
navigator.serviceWorker.onmessage = (event) => {
this.onMessageSuccess(event);
};
}
navigator.serviceWorker.ready.then((reg) => {
try {
reg.active.postMessage(
{
text: 'sync',
port: messageChannel && messageChannel.port2,
},
[messageChannel && messageChannel.port2]
);
} catch (e) {
//firefox support
reg.active.postMessage({
text: 'sync',
});
}
});
}
this is inside a Vue.js watch function, which watches whether we have connection or not, if we have connection it also checks if this is a device from the apple ecosystem, like so:
isIosDevice() {
return !!navigator.platform && /iPad|iPhone|MacIntel|iPod/.test(navigator.platform) && /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
}
And so it tells the service worker that it has internet and it has to sync, in that case this bit of code gets activated:
this.onmessage = (event) => {
if (event.data.text === 'sync') {
event.waitUntil(
syncData().then((res) => {
if (res !== 'empty') {
if (event.source) {
event.source.postMessage('doNotification');//this is telling the client code to show a notification (i have a built in notification system into the app, that does not use push notification, just shows a little pill on the bottom of the app with the message)
} else if (event.data.port) {
event.data.port.postMessage('doNotification'); //same thing
}
return res;
}
})
);
}
};
Now the most useful part in my opinion, the replay queue function, this guy gets the queue and the token from getAccessToken, and then it does its thing like clockwork:
const replayQueue = async (queue, token) => {
let entry;
while ((entry = await queue.shiftRequest())) {//while we have requests to replay
let data = await entry.request.clone().json();
try {
//replay the person registrations first and store them into indexed db
if (isPersonRequest) {
//if new person
await fetchPerson(entry, data, token);
//then replay the campaign and task submissions
} else if (isTaskOrCampaignRequest) {
//if task
await fetchCampaigns(entry, data, token);
}
} catch (error) {
showNotification({ title: 'no success', body: 'go for better internet plox' });
await queue.unshiftRequest(entry); //put failed request back into queue, and try again later
}
}
return Promise.resolve();
};
Now this is the big picture as how to use this guy on iOS devices and make Apple mad as heck :) I am open to any questions that are related, in this time i think i have become pretty good with service worker related stuff as this was not the only difficult part of this project but i digress, thats a story for another day.
(you may see that error handling is not perfect and maybe this thing is not he most secure of them all, but this project has a prettty small amount of users, with a fixed number which know how to use it and what it does, so im not really afraid of security in this case, but you may want to improve on things if you use in in a more serious project)
Hope i could help and all of you have a grea day.

UserState showing null in onMembersAdded function

I have logic in my onMembersAdded function to load the user state and see if userData.accountNumber attribute exists. If it does not, a run an auth dialog to get the user's account number. If the attribute does exist, the welcome message should be displayed without a prompt.
When I test on local, this works fine. But when I test on Azure, I always end up in the !userData.accountNumber block. Through checking the console log, I can see that in the onMembersAdded function is showing {} for the userData object. But in auth dialog, even if I skip the prompt (which we allow the user to do), the accountNumber attribute is there in userState (if it had been entered previously).
The only thing I can figure is that somehow using BlobStorage for state, as I do on Azure, is somehow exhibiting different behavior than MemoryStorage which I am using for local testing. I thought it might be a timing issue, but I am awaiting the get user state call, and besides if I do enter an account number in the auth dialog, the console log immediately following the prompt shows the updated account number, no problem.
EDIT: From the comments below, it's apparent that the issue is the different way channels handle onMembersAdded. It seems in emulator both bot and user are added at the same time, but on webchat/directline, user isn't added until the first message is sent. So that is the issue I need a solution to.
Here is the code in the constructor defining the state variables and onMembersAdded function:
// Snippet from the constructor. UserState is passed in from index.js
// Create the property accessors
this.userDialogStateAccessor = userState.createProperty(USER_DIALOG_STATE_PROPERTY);
this.dialogState = conversationState.createProperty(DIALOG_STATE_PROPERTY);
// Create local objects
this.conversationState = conversationState;
this.userState = userState;
this.onMembersAdded(async (context, next) => {
const membersAdded = context.activity.membersAdded;
for (let member of membersAdded) {
if (member.id === context.activity.recipient.id) {
this.appInsightsClient.trackEvent({name:'userAdded'});
// Get user state. If we don't have the account number, run an authentication dialog
// For initial release this is a simple prompt
const userData = await this.userDialogStateAccessor.get(context, {});
console.log('Members added flow');
console.log(userData);
if (!userData.accountNumber) {
console.log('In !userData.accountNumber block');
const dc = await this.dialogs.createContext(context);
await dc.beginDialog(AUTH_DIALOG);
await this.conversationState.saveChanges(context);
await this.userState.saveChanges(context);
} else {
console.log('In userData.accountNumber block');
var welcomeCard = CardHelper.GetHeroCard('',welcomeMessage,menuOptions);
await context.sendActivity(welcomeCard);
this.appInsightsClient.trackEvent({name:'conversationStart', properties:{accountNumber:userData.accountNumber}});
}
}
}
// By calling next() you ensure that the next BotHandler is run.
await next();
});
If you want your bot to receive a conversation update from Web Chat with the correct user ID before the user sends a message manually, you have two options:
Instead of connecting to Direct Line with a secret, connect with a token (recommended). Note that this will only work if you provide a user property in the body of your Generate Token request.
Have Web Chat send an initial activity to the bot automatically so the user doesn't have to. This would be in response to DIRECT_LINE/CONNECT_FULFILLED, and it could be an invisible event activity so to the user it still looks like the first activity in the conversation came from the bot.
If you go with option 1, your bot will receive one conversation update with both the bot and the user in membersAdded, and the from ID of the activity will be the user ID. This is ideal because it means you will be able to acess user state.
If you go with option 2, your bot will receive two conversation update activities. The first is the one you're receiving now, and the second is the one with the user ID that you need. The funny thing about that first conversation update is that the from ID is the conversation ID rather than the bot ID. I presume this was an attempt on Web Chat's part to get the bot to mistake it for the user being added, since Bot Framework bots typically recognize that conversation update by checking if the from ID is different from the member being added. Unfortunately this can result in two welcome messages being sent because it's harder to tell which conversation update to respond to.
Conversation updates have been historically unreliable in Web Chat, as evidenced by a series of GitHub issues. Since you may end up having to write channel-aware bot code anyway, you might consider having the bot respond to a backchannel event instead of a conversation update when it detects that the channel is Web Chat. This is similar to option 2 but you'd have your bot actually respond to that event rather than the conversation update that got sent because of the event.
Per Kyle's answer, I was able to resolve the issue. However, the documentation on initiating a chat session via tokens wasn't entirely clear, so I wanted to provide some guidance for others trying to solve this same issue.
First, you need to create an endpoint in your bot to generate the token. The reason I initiated the session from SECRET initially was because I didn't see a point to creating a token when the SECRET was exposed anyway to generate it. What wasn't made clear in the documentation was that you should create a separate endpoint so that the SECRET isn't in the browser code. You can/should further obfuscate the SECRET using environmental variables or key vault. Here is the code for the endpoint I set up (I'm passing in userId from browser, which you'll see in a minute).
server.post('/directline/token', async (req, res) => {
try {
var body = {User:{Id:req.body.userId}};
const response = await request({
url: 'https://directline.botframework.com/v3/directline/tokens/generate',
method: 'POST',
headers: { Authorization: `Bearer ${process.env.DIRECTLINE_SECRET}`},
json: body,
rejectUnauthorized: false
});
const token = response.token;
res.setHeader('Content-Type', 'text/plain');
res.writeHead(200);
res.write(token);
res.end();
} catch(err) {
console.log(err);
res.setHeader('Content-Type', 'text/plain');
res.writeHead(500);
res.write('Call to retrieve token from Direct Line failed');
res.end();
}
})
You could return JSON here, but I chose to return token only as text. Now to call the function, you'll need to hit this endpoint from the script wherever you are deploying the bot (this is assuming you are using botframework-webchat CDN). Here is the code I used for that.
const response = await fetch('https://YOURAPPSERVICE.azurewebsites.net/directline/token', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({userId:userID})
});
const token = await response.text();
Body of request must be stringified JSON. Fetch returns the response as a stream, so you need to convert it using .text() or .json() depending on how you are sending the response from your bot endpoint (I used .text()). You need to await both the fetch AND the response.text(). My whole script to deploy the webchat is within an async function. Just a note, if you need this to work in IE11 as I do, async/await won't work. I dealt with this by running the entire code through Babel once I was done and it seems to work fine.

Flow for authentication when MFA required for user in AWS Cognito

I am attempting to add MFA for user authentication to an already existing solution (built in Angular) for device management within AWS Cognito.
I am having trouble figuring out how to handle this particular response well from a user-experience perspective. It actually feels broken, so would love if anyone else has experience pain points here.
See Use Case 23. for example implementation, mine is below:
authenticate(username: string, password: string): Observable<any> {
// init cognitoUser here
return new Observable((observer) => {
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: (result: any) => {},
onFailure: (err: Error) => {},
mfaRequired: (codeDeliveryDetails: any) => {
// SMS has just been sent automatically
// and it needs to be confirmed within this scope
// The example linked requests the code via `confirm()`
// which is awful UX...and since this is a service
// probably non-compliant with best practice
// However, without this `confirm` at this point in
// time, we have no confirmationCode below
cognitoUser.sendMFACode(confirmationCode, {
onSuccess: (result) => {
observer.next(result);
observer.complete();
}, onFailure: (err: Error) => {
observer.error(err);
observer.complete();
}
});
}
});
});
}
Expected:
If the user authenticates successfully but has not added this device through MFA, we can manage the redirect to appropriate confirmation code form page and trigger the sendMFACode function manually (perhaps through some sort of limited session?)
Issue/s:
we don't have a session, so we have no way of asking the user the MFA code sent automatically outside of this login screen...catch 22?
adding another show/hide field in the login form doesn't work as it would hit the sendMfaCode function multiple times, resulting in multiple SMS codes sent.
Has anyone had any luck stepping out of this flow?
Whilst I’m sure very talented people worked on the amazon-cognito-identity-js API, it is just straight up badly designed. Thus why it’s been depricated. My personal advise would be to migrate to Amplify, which makes me much less angry.
With Amplify you can do these ones.

import Amplify from 'aws-amplify'
import Auth from '#aws-amplify/auth'
let mfaRequired = false
Amplify.configure({
Auth: {
userPoolWebClientId: '',
userPoolId: ''
}
})
const logUserIn = (user) => {
// Go forth and be happy
}
// Run me on your login form's submit event
const login = async (username, password) => {
const user = await Auth.signIn(username, password)
if (user.challengeName === 'SMS_MFA') {
// Change UI to show MFA Code input
mfaRequired = true
return
}
return logUserIn(user)
}
// Run me when the user submits theire MFA code
const senfMfaCode = async (mfaCode) => {
const user = await Auth.confirmSignIn(mfaCode)
return logUserIn(user)
}
BUT if for some sad reason you need to keep using amazon-cognito-identity-js don’t worry. I got you.
Just keep the cognitoUser object stored outside the callback. The documentation is a little misleading because it only show’s self contained examples but there’s no reason that you can’t notify your UI when MFA is required and then call cognitoUser.sendMFACode() later.
Just remember that the documentation show’s the passing of this to sendMFACode() for scoping (which is terrible) but you can just declare your callbacks as a variable and share it between your authenticateUser() and sendMFACode() functions (or as many functions as you like).
import { CognitoUserPool, AuthenticationDetails, CognitoUser } from 'amazon-cognito-identity-js'
export let mfaRequired = false
export let cognitoUser = null
export const cognitoCallbacks = {
mfaRequired () {
// Implement you functionality to show UI for MFA form
mfaRequired = true
},
onSuccess (response) {
// Dance for joy the code gods be glorious.
},
onFailure () {
// Cry.
}
}
export const logUserIn = payload => {
cognitoUser = new CognitoUser({
Username: 'Matt Damon',
Pool: new CognitoUserPool({
UserPoolId: '',
ClientId: ''
})
})
return cognitoUser.authenticateUser(new AuthenticationDetails(payload), cognitoCallbacks)
}
export const sendMfaCode = MFACode => {
cognitoUser.sendMFACode(MFACode, cognitoCallbacks)
}
That’s a super basic implementation and on top of that you could,
Just overwrite the mfaRequired function in an external module to do whatever you want.
Wrap the whole thing in a pub/sub plugin and subscribe to events.
Hope that helps!
I know this is an old question, but I thought this answer might be helpful for anyone who is still using the amazon-cognito-identity-js API instead of Amplify. #stwilz's answer works somewhat, but there are a few complications that come when you stray too far away from the documentation's use cases (and might come about when doing TOTP MFA instead of SMS MFA). I've created a workaround to address situations where you might get errors like Invalid Access Token, Missing parameter Session, or Invalid session for the user.
If you need to use something like sendMFACodeoutside of the callbacks, it's not enough to just keep cognitoUser stored outside the callback. You actually have to call the authenticateUser function again, then call the sendMFACode within the callback. It gets more complicated with verifySoftwareToken for TOTP, where you actually have to store the Cognito user object and then reassign it when calling authenticateUser again.
If none of this makes sense, I've created a simple Github Gist that uses React and amazon-cognito-identity-js to show how such a flow would work. It's here: https://gist.github.com/harve27/807597824720d0919476c0262e30f587

RxJs Observables: run retryWhen after some more async requests

My use case is:
User requests asset from our API which fails because of JWT expiring (passed as an httpOnly cookie) - API returns a 401 status code.
We go and authenticate them (without the user doing anything) again using a refresh_token to retrieve a new JWT with a request from our client to auth0.
We send that new JWT to our API to be set as an httpOnly cookie to replace the expired one.
We then want to retry the original request the user made to the API in step 1.
I'm trying to use Observables within my Redux app with redux-observable. If you can think of another way of making the above user flow work I would be happy to hear how.
NB. Im using rxjs V5
export const fetchAssetListEpic = (action$, store) => {
return action$.ofType('FETCH_ASSET_LIST')
.switchMap( action => {
const options = {
crossDomain: true,
withCredentials: true,
url: uriGenerator('assetList', action.payload)
};
return ajax(options);
})
.map(fetchAssetListSuccess)
.retryWhen(handleError)
.catch(redirectToSignIn);
};
function handleError(err) {
return (err.status === 401) ?
/* Authenticate here [Step 2] */
/* Send new JWT to API [Step 3] */
/* If successful make original request again [Step 4] */
:
Observable.throw(err);
}
function redirectToSignIn() {
/*I will redirect here*/
}
So far I able to complete steps 1, 2 and 3 but not too sure of a way to add step 4. I may be completely off the mark but any help would be great!
Well one thing you probably won't want to do is allow the error to make it to the top level stream. Even if you do a catch you have effectively killed the top level stream. So unless your redirect is doing a hard redirect instead of a a soft one via something like react-router, you won't be able to use this epic any more.
Thus I would say that you want most of the logic to be encapsulated within the switchMap:
function withAuthorizedFlow(source) {
return source
.map(fetchAssetListSuccess)
// retryWhen takes a callback which accepts an Observable of errors
// emitting a next causes a retry, while an error or complete will
// stop retrying
.retryWhen(e => e.flatMap(err =>
Observable.if(
// Returns the first stream if true, second if false
() => err.status === 401,
reauthenticate, // A stream that will emit once authenticated
Observable.throw(err) // Rethrow the error
))
)
.catch(redirectToSignIn);
}
/** Within the epic **/
.switchMap(({payload}) => {
const options = {
crossDomain: true,
withCredentials: true,
url: uriGenerator('assetList', payload)
};
// Invoke the ajax request
return ajax(options)
// Attach a custom pipeline here
// Not strictly necessary but it keeps this method clean looking.
.let(withAuthorizedFlow);
})
The use of let above is completely optional, I threw it in to clean up the function. Essentially though you want to contain the error to the inner stream so that it can't halt the outer one. I am not sure which ajax library you are using but you should also confirm that it will in fact return a cold Observable otherwise you will need to wrap it in a defer block to in order for the retryWhen to work.

Categories

Resources