I created a bot with a command, that allows the user to configure some sort of 'feed' to their channel.
This feed is supposed to send a message, save guild, channel and message id. And in a stand-alone update cycle, try to update the message with new information.
This all works fairly well, as long as it is within the same session.
Say the bot losses it's connection due to a discord outage, and re-connects x amount of time later, the bot no longer seems to be able to find, and thus update the message anymore.
In particular, it seems to be unable to retrieve the message by id
var message = await channel.GetMessageAsync(playtimeFeed.MessageId) as SocketUserMessage;
It's worth to note that I make use of _settings which is persisted in json format, and is loaded again upon bot reboot.
I also confirmed that the message still exists in the server at the channel, with the same message id. And that the bot has permissions to view the message history of the channel.
Thus my question, how come the GetMessageAsync is unable to retrieve a previously posted message after reconnecting?
Initialy invoked command
public async Task BindPlaytimeFeedAsync(ICommandContext context)
{
var builder = await _scumService.GetTop25PlaytimeByDate(new DateTime(), DateTime.Now);
var message = await context.Channel.SendMessageAsync(null, false, builder.Build());
_settings.PlaytimeFeed = new MessageInfo()
{
GuildId = context.Guild.Id,
ChannelId = context.Channel.Id,
MessageId = message.Id,
};
var ptFeedMessage = await context.Channel.SendMessageAsync("Playtime feed is now bound to this channel (this message self-destructs in 5 seconds)");
await Task.Delay(5000);
await ptFeedMessage.DeleteAsync();
}
The refresh interval of the feed is defined alongside the bot itself using a timer as seen below.
...
_client = new DiscordSocketClient(
new DiscordSocketConfig
{
LogLevel = LogSeverity.Verbose,
AlwaysDownloadUsers = true, // Start the cache off with updated information.
MessageCacheSize = 1000
}
);
_service = ConfigureServices();
_feedInterval = new Timer(async (e) =>
{
Console.WriteLine("doing feed stuff");
await HandleFeedsAsync();
}, null, 15000, 300000);
CmdHandler = new CommandHandler(_service, state);
...
private async Task HandleFeedsAsync()
{
var botSettings = _service.GetService<ISettings>() as BotSettings;
await HandleKdFeedAsync(botSettings.KdFeed);
await HandlePlaytimeFeedAsync(botSettings.PlaytimeFeed);
await HandleWeeklyPlaytimeFeed(botSettings.WeeklyPlaytimeFeed);
await HandleAdminFeed(botSettings);
}
And ultimately the message is overwritten using the below snippet.
private async Task HandlePlaytimeFeedAsync(MessageInfo playtimeFeed)
{
if (playtimeFeed == null)
return;
var scumService = _service.GetService<ScumService>();
var guild = _client.GetGuild(playtimeFeed.GuildId);
var channel = guild.GetTextChannel(playtimeFeed.ChannelId);
var message = await channel.GetMessageAsync(playtimeFeed.MessageId) as SocketUserMessage;
if (message == null)
return;
var builder = await scumService.GetTop25PlaytimeByDate(new DateTime(), DateTime.Now);
await message.ModifyAsync(prop =>
{
prop.Embed = builder.Build();
});
}
var message = await channel.GetMessageAsync(playtimeFeed.MessageId) as SocketUserMessage;
The GetMessageAsync method attempts to retrieve a message from cache as a SocketUserMessage, if however the message is not found in cache, a rest request is performed which would return a RestUserMessge. By performing a soft cast on the result of GetMessageAsync, you can get null if/when a RestUserMessage is returned.
When the possibility exists that the message you are dealing with can be either a Socket entity or Rest entity, simply use the interface to interact with it -- IUserMessage.
Related
I have developed chat bot with C#
Need to show different greeting message, on User using the chatbot for second time with in a day.
If user is using the chatbot for first time in a day, need to show "Hi, how can I help You" greeting message. second time on using chatbot need to show "Welcome back 'Username', how can I help you" message.
How to achieve this?
By default Bots are stateless. The state and storage features of the Bot Framework SDK allow you to manage the state of your bot. There is documentation available in this context - Save user and conversation data
Here is some code that meets your requirement.
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
// Get the state properties from the turn context.
var conversationStateAccessors = _conversationState.CreateProperty<ConversationData>(nameof(ConversationData));
var conversationData = await conversationStateAccessors.GetAsync(turnContext, () => new ConversationData());
var userStateAccessors = _userState.CreateProperty<UserProfile>(nameof(UserProfile));
var userProfile = await userStateAccessors.GetAsync(turnContext, () => new UserProfile());
if (string.IsNullOrEmpty(userProfile.Name))
{
// First time around this is set to false, so we will prompt user for name.
if (conversationData.PromptedUserForName)
{
// Set the name to what the user provided.
userProfile.Name = turnContext.Activity.Text?.Trim();
// Acknowledge that we got their name.
await turnContext.SendActivityAsync($"Thanks {userProfile.Name}. To see conversation data, type anything.");
// Reset the flag to allow the bot to go through the cycle again.
conversationData.PromptedUserForName = false;
}
else
{
// Prompt the user for their name.
await turnContext.SendActivityAsync($"What is your name?");
// Set the flag to true, so we don't prompt in the next turn.
conversationData.PromptedUserForName = true;
}
}
else
{
// Add message details to the conversation data.
// Convert saved Timestamp to local DateTimeOffset, then to string for display.
var messageTimeOffset = (DateTimeOffset) turnContext.Activity.Timestamp;
var localMessageTime = messageTimeOffset.ToLocalTime();
conversationData.Timestamp = localMessageTime.ToString();
conversationData.ChannelId = turnContext.Activity.ChannelId.ToString();
// Display state data.
await turnContext.SendActivityAsync($"{userProfile.Name} sent: {turnContext.Activity.Text}");
await turnContext.SendActivityAsync($"Message received at: {conversationData.Timestamp}");
await turnContext.SendActivityAsync($"Message received from: {conversationData.ChannelId}");
}
}
Below is error that I could not update refreshSchedule of the datasets:
{
"error": {
"code": "InvalidRequest",
"message": "Invalid NotifyOption value 'MailOnFailure' for app only owner requests"
}
}
Below is code to call it:
var datasets = await client.Datasets.GetDatasetsAsync(new Guid(_workspaceId));
var days = new List<Days?> { Days.Monday, Days.Tuesday, Days.Wednesday, Days.Thursday, Days.Friday, Days.Saturday, Days.Sunday };
var times = new List<string> { "00:00" };
var refreshSchedule = new RefreshSchedule(days, times, true, "UTC");
var id = "XXX";
await client.Datasets.TakeOverAsync(new Guid(_workspaceId), id);
var refreshRequest = new RefreshRequest(NotifyOption.NoNotification);
// refresh datasets
await client.Datasets.RefreshDatasetAsync(new Guid(_workspaceId), id, refreshRequest);
// Target: Update RefreshSchedule (Exception for calling this)
await client.Datasets.UpdateRefreshScheduleInGroupAsync(new Guid(_workspaceId), id, refreshSchedule);
Can you pls let me know how the app is consuming the endpoint - User Interventaion or Completeley done through AppOnly or using the master user Credential?
Alternatively, if you don't want the MailOnfailure, can you set up explicitly No Notification for the Refresh Schedule and Try ?
just modified your piece of code and presented below :
var refreshSchedule = new RefreshSchedule(days, times, true, "UTC", ScheduleNotifyOption.NoNotification);
The snippet :
var days = new List<Days?> { Days.Monday, Days.Tuesday, Days.Wednesday, Days.Thursday, Days.Friday, Days.Saturday, Days.Sunday };
var times = new List<string> { "00:00" };
//Fixed code
var refreshSchedule = new RefreshSchedule(days, times, true, "UTC", ScheduleNotifyOption.NoNotification);
await client.Datasets.UpdateRefreshScheduleInGroupAsync(WorkspaceId, datasets.Id, refreshSchedule);
UPDATE
When i used ServicePrincipal (without any UserIntervention/Master User Credential)
I was able to repro your issue
Error
Status: BadRequest (400) Response: {"error":{"code":"InvalidRequest","message":"Invalid NotifyOption
value 'MailOnFailure' for app only owner requests"}}
I was able to get past the error by making use of the ScheduleNotifyOption.NoNotification in the refreshSchedule mentioned above.
Or if i use the app through cred of a mailenabled account.
I am building a bot using the Microsoft C# bot framework v4. When the bot is first added to a team, I want it to proactively 1:1 message each member of the team, introducing itself. I know it should be possible to start a chat with a user, even if the user has not previously interacted with the bot. This process is described in the documentation:
When using proactive messaging to send a welcome message to a user you must keep in mind that for most people receiving the message they will have no context for why they are receiving it. This is also the first time they will have interacted with your app; it is your opportunity to create a good first impression. The best welcome messages will include:
Why are they receiving this message. It should be very clear to the user why they are receiving the message. If your bot was installed in a channel and you sent a welcome message to all users, let them know what channel it was installed in and potentially who installed it.
To me this indicates that I should be able to initiate a chat message with each member of the channel, but I cannot get the bot to message anyone in the channel other than me. In the TeamsConversationBot sample provided by Microsoft, there is a MessageAllMembers() method that seems to be what I am looking for, however when I call it in my code, and add the bot to a team in Teams, the bot only messages me.
Here is the MessageAllMembers() code I am using:
private async Task MessageAllMembersAsync(ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
var Id = turnContext.Activity.TeamsGetChannelId();
var serviceUrl = turnContext.Activity.ServiceUrl;
var credentials = new MicrosoftAppCredentials(_configuration["MicrosoftAppId"], _configuration["MicrosoftAppPassword"]);
ConversationReference conversationReference = null;
var members = await TeamsInfo.GetMembersAsync(turnContext, cancellationToken);
foreach (var teamMember in members)
{
if (teamMember.Id != turnContext.Activity.Recipient.Id) {
var proactiveMessage = MessageFactory.Text($"Hello {teamMember.GivenName} {teamMember.Surname}. I'm a Teams conversation bot.");
var conversationParameters = new ConversationParameters
{
IsGroup = false,
Bot = turnContext.Activity.Recipient,
Members = new ChannelAccount[] { teamMember },
TenantId = turnContext.Activity.Conversation.TenantId,
};
await ((BotFrameworkAdapter)turnContext.Adapter).CreateConversationAsync(
Id,
serviceUrl,
credentials,
conversationParameters,
async (t1, c1) =>
{
conversationReference = t1.Activity.GetConversationReference();
await ((BotFrameworkAdapter)turnContext.Adapter).ContinueConversationAsync(
_configuration["MicrosoftAppId"],
conversationReference,
async (t2, c2) =>
{
await t2.SendActivityAsync(proactiveMessage, c2);
},
cancellationToken);
},
cancellationToken);
}
}
await turnContext.SendActivityAsync(MessageFactory.Text("All messages have been sent."), cancellationToken);
}
It is not throwing any exceptions, it's just not doing what I expect.
In C# I have a web service with an operation result defined as below:
OperationResult<createAccountResponse> CreateAccount(string token, createAccountServiceModel model);
Inside that method I call another method with a signature indicating it is async, like so:
var sendEmailInvite = this.SendExhibitorInviteEmailAsync(new ExhibitorInviteEmailPartialRequest()
{
CompanyId = company.CompanyID,
EventId = event.EventID
});
And inside SendExhibitorInviteEmailAsync I await a method which is also marked as async. Here is that method (snipped for brevity)
public async Task<ExhibitorInviteEmailResponse> SendExhibitorInviteEmailAsync(ExhibitorInviteEmailResolvedRequest request)
{
ExhibitorInviteEmailResponse response = null;
try
{
response = new ExhibitorInviteEmailResponse();
var apiKey = "snip";
var client = new SendGridClient(apiKey);
var msg = new SendGridMessage();
msg.SetFrom(new EmailAddress("noreply#domain.com", "Display name"));
msg.AddTo(new EmailAddress(request.EmailAddress, request.AccountOwnerFirstName));
msg.SetTemplateId("snipped");
\
msg.SetTemplateData(dynamicTemplateData);
await client.SendEmailAsync(msg);
}
catch (Exception ex)
{
response = new ExhibitorInviteEmailResponse
{
Success = false,
Error = true,
ErrorMessage = ex.Message
};
}
return response;
}
If the email is meant to be sent (flag field in the json) then I start working on the email.If no email is meant to be sent, the whole method takes about a second which was what it was before.
The issue I am having is when I run this method from Postman or from C# generated by Postman, it seems the async code for sending the email causes the duration of the request to be 30+ seconds - so it seems like something is not waiting for the email to send? When I run this via a browser it works in 1-2 seconds with no delay.
What is the recommended flow when using Postman and async? Do all internal method's parents have to await as well?
In a dialog within my bot, I store a flag value in the ConversationData like so:
context.ConversationData.SetValue("SomeFlag", true);
Later, I need to check that flag in my MessagesController, before the message is dispatched to a dialog. As per this previous question I tried retrieving the ConversationData in via the StateClient like this:
public async Task<HttpResponseMessage> Post([FromBody] Activity incomingMessage)
{
StateClient stateClient = incomingMessage.GetStateClient();
BotData userData = await stateClient.BotState.GetConversationDataAsync(message.ChannelId, message.Conversation.Id);
bool finishedQuote = userData.GetProperty<bool>("SomeFlag");
//...
// do conditional logic, then dispatch to a dialog as normal
}
However, at runtime, the userData variable holds a BotData object where userData.Data is null, and I'm unable to retrieve any stored flags via GetProperty. I don't see anything in the relevant documentation that helps shed light on this issue - what might I be doing wrong here? Is there something I'm misunderstanding?
The following should work for what you need:
if (activity.Type == ActivityTypes.Message)
{
var message = activity as IMessageActivity;
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message))
{
var botDataStore = scope.Resolve<IBotDataStore<BotData>>();
var key = Address.FromActivity(message);
ConversationReference r = new ConversationReference();
var userData = await botDataStore.LoadAsync(key, BotStoreType.BotUserData, CancellationToken.None);
//you can get/set UserData, ConversationData, or PrivateConversationData like below
//set state data
userData.SetProperty("key 1", "value1");
userData.SetProperty("key 2", "value2");
//get state data
userData.GetProperty<string>("key 1");
userData.GetProperty<string>("key 2");
await botDataStore.SaveAsync(key, BotStoreType.BotUserData, userData, CancellationToken.None);
await botDataStore.FlushAsync(key, CancellationToken.None);
}
await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
}
Initialize BotState object with StateClient as below. Try the below code
public static T GetStateData<T>(Activity activity, string key)
{
BotState botState = new BotState(activity.GetStateClient());
BotData botData = botState.GetConversationData(activity.ChannelId, activity.Conversation.Id);
return botData.GetProperty<T>(key);
}