Telegram allows commenting on a channel post or on a generic supergroup message, thanks to message threads.
https://core.tlgr.org/api/threads
I get the last messages in the channel
private async Task<TLMessage> GetLastMessage(TLChannel channelFrom)
{
TLChannelMessages resp = (TLChannelMessages)await _client.GetHistoryAsync(new TLInputPeerChannel()
{
ChannelId = channelFrom.Id,
AccessHash = channelFrom.AccessHash ?? 0,
}, limit: 1000);
TLMessage lastMessage = (TLMessage)resp.Messages?.Where(x => x is TLMessage).First();
return lastMessage;
}
I can forward it to another channel / chat:
public async Task ReplyInDiscussion(TLChannel channelFrom, TLChannel chatTo)
{
TLMessage lastMessage = await GetLastMessage(channelFrom);
TLMessage lastChatMessage = await GetLastForwardMessage(chatTo, channelFrom.Id, lastMessage.Id);
await ReplyTo(chatTo, "Text", lastChatMessage.Id);
}
public Task ReplyTo(TLChannel channelTo, string message, int? replyMsgId = null)
{
TLAbsInputPeer to = new TLInputPeerChannel()
{
ChannelId = channelTo.Id,
AccessHash = channelTo.AccessHash ?? 0,
};
return ReplyTo(to, message, replyMsgId);
}
private async Task ReplyTo(TLAbsInputPeer to, string message, int? replyMsgId = null)
{
var req = new TLRequestSendMessage()
{
Peer = to,
Message = message,
RandomId = Helpers.GenerateRandomLong(),
ReplyToMsgId = replyMsgId,
};
await _client.SendRequestAsync<TLUpdates>(req);
}
I need to leave a comment specifically for the received message
example
I do not understand how to do this, it is very difficult for me
I encountered a “date” issue with Bot in TEAMS, The date value cannot render at LeaveRequest() properly. However, the same bot works fine in Bot Emulator.
Following is my sample code. How to fix (or workaround) this issue?
Select a date from date picker and submit
System does not render the first field value.
public class EchoBot
{
public static Attachment LeaveRequest()
{
AdaptiveSchemaVersion schemaVersion = new AdaptiveSchemaVersion(1, 0);
AdaptiveCard card = new AdaptiveCard(schemaVersion);
card.Body.Add(new AdaptiveTextBlock()
{
Text = "Enter new Date",
Size = AdaptiveTextSize.Large,
Weight = AdaptiveTextWeight.Bolder
});
AdaptiveDateInput fromDate = new AdaptiveDateInput()
{
Id = "FromDate",
Placeholder = "From Date"
};
card.Body.Add(fromDate);
AdaptiveTextInput toDate = new AdaptiveTextInput()
{
Id = "ToDate",
Placeholder = "To Date",
Value = DateTime.Today.ToUniversalTime().ToString("u")
};
card.Body.Add(toDate);
card.Actions.Add(new AdaptiveSubmitAction()
{
Title = "Submit"
});
Attachment cardAttachment = new Attachment()
{
ContentType = AdaptiveCard.ContentType,
Content = card
};
return cardAttachment;
}
}
public class RootDialog : IDialog<object>
{
public async Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
}
public virtual async Task MessageReceivedAsync(IDialogContext context,
IAwaitable<IMessageActivity> result)
{
var activity = await result as Activity;
Boolean first = true;
if (activity.Value != null)
{
var jObjectValue = activity.Value as JObject;
var fromDateString = jObjectValue.Value<string>("FromDate");
var toDateString = jObjectValue.Value<string>("ToDate");
await context.PostAsync($"from Date Entered: {fromDateString}");
await context.PostAsync($"to Date Entered: {toDateString}");
first = false;
}
if (first)
{
var reply = activity.CreateReply();
var card = EchoBot.LeaveRequest();
var msg = context.MakeMessage();
msg.Attachments.Add(card);
await context.PostAsync(msg);
}
}
}
Thanks for reaching out!! This is a known issue. We have a bug on this and we are working on getting this fixed.
I was trying the multi-language chat bot in bot framework. https://github.com/Microsoft/BotBuilder-Samples/tree/master/samples/csharp_dotnetcore/17.multilingual-bot.
I was successful in translating ordinary message with no attachment. But I'm having a problem with attachment like herocard or suggestedcard.
var reply = stepContext.Context.Activity.CreateReply();
var card = new HeroCard();
card.Buttons = new List<CardAction>()
{
new CardAction() { Title = "1. All lights are green.", Type = ActionTypes.ImBack, Value = "All lights are green." },
new CardAction() { Title = "2. DSL light is OFF/Red/Blinking Green.", Type = ActionTypes.ImBack, Value = "DSL light is OFF/Red/Blinking Green." },
new CardAction() { Title = "3. Internet light is OFF/Red/Amber or blinking red and green.", Type = ActionTypes.ImBack, Value = "Internet light is OFF/Red/Amber or blinking red and green." },
new CardAction() { Title = "4. Power light is OFF/Red/Amber or blinking.", Type = ActionTypes.ImBack, Value = "Power light is OFF/Red/Amber or blinking." },
};
reply.Attachments = new List<Attachment>() { card.ToAttachment() };
reply.AttachmentLayout = AttachmentLayoutTypes.List;
var options = new PromptOptions()
{
Prompt = reply,
};
await stepContext.Context.SendActivityAsync(reply, cancellationToken);
Thanks!
You need to check if there are any attachments. I have some middleware that sets the speak on message and use the following code:
if (string.IsNullOrEmpty(message.Text))
{
if (message.Attachments[0].Content is HeroCard attachment)
{
message.Speak = TextToSpeechHelper.ConvertTextToSpeechText(attachment.Text);
}
}
else
{
message.Speak = TextToSpeechHelper.ConvertTextToSpeechText(message.Text);
}
You'd have to adjust it to set the text and translate.
Below is a full example of what my middleware is. Keep in mind this middleware sets the speak to the text and sets the input hint.
public class TextToSpeechMiddleware : IMiddleware
{
private readonly IEnumerable<string> ignoreList;
public TextToSpeechMiddleware(string speakIgnore)
{
ignoreList = GetSpeakIgnore(speakIgnore);
}
public Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default(CancellationToken))
{
turnContext.OnSendActivities(OnSendActivities);
turnContext.OnUpdateActivity(OnUpdateActivity);
return next(cancellationToken);
}
private static IEnumerable<string> GetSpeakIgnore(string value)
{
string[] ignoreList = value.Split(';');
return ignoreList.Select(i => i.Trim())
.Where(i => !string.IsNullOrEmpty(i));
}
private Task<ResourceResponse> OnUpdateActivity(ITurnContext turnContext, Activity activity, Func<Task<ResourceResponse>> next)
{
ConvertTextToSpeech(activity);
return next();
}
private Task<ResourceResponse[]> OnSendActivities(ITurnContext turnContext, List<Activity> activities, Func<Task<ResourceResponse[]>> next)
{
foreach (Activity currentActivity in activities.Where(a => a.Type == ActivityTypes.Message))
{
ConvertTextToSpeech(currentActivity);
}
return next();
}
private void ConvertTextToSpeech(Activity message)
{
Activity initialMessage = message;
try
{
if (message.Type == ActivityTypes.Message)
{
bool ignoredSpeak = false;
if (string.IsNullOrEmpty(message.Speak))
{
if (string.IsNullOrEmpty(message.Text))
{
if (message.Attachments[0].Content is HeroCard attachment)
{
message.Speak = TextToSpeechHelper.ConvertTextToSpeechText(attachment.Text);
}
}
else
{
message.Speak = TextToSpeechHelper.ConvertTextToSpeechText(message.Text);
}
message.Speak = message.Speak.Trim();
if (ignoreList.Where(i => message.Speak.ToLower().StartsWith(i.ToLower())).Count() != 0)
{
message.Speak = null;
ignoredSpeak = true;
}
}
else if (string.IsNullOrWhiteSpace(message.Speak))
{
message.Speak = " ";
}
if ((!string.IsNullOrEmpty(message.Speak) && (message.Speak.EndsWith("?") || message.Speak.StartsWith("Is this correct?")))
|| (!string.IsNullOrEmpty(message.Text) && message.Text.EndsWith("?"))
|| ignoredSpeak)
{
message.InputHint = InputHints.ExpectingInput;
}
// IOs won't work with expecting input
if (message.Recipient.Name.EndsWith(":ios"))
{
message.InputHint = InputHints.AcceptingInput;
}
}
// Logic needed to increase speech speed.
// if (!string.IsNullOrEmpty(message.Speak))
// {
// message.Speak = #"<speak version='1.0' " + "xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='en-GB'><prosody rate=\"1.5\">" + message.Speak + "</prosody></speak>";
// }
}
catch (Exception)
{
message = initialMessage;
}
}
}
I'm getting a "Because this call is not awaited..." on
SendPostAsync(CustomerName, email, Phone, maxImages, MainEventName, MainEventCode, CLemail, package_type, PlayerInfo, template_ID, favoritesArray);
Here's the button click:
private void btnCopyAllInvoices_Click(object sender, EventArgs e)
{
//sets up a list to store the incoming invoice numbers from the DB
List<string> InvoiceNums = new List<string>();
mySqlInterface.Connect();
InvoiceNums = mySqlInterface.GetNewInvoices();
//prep the visuals
lblStatus.Text = "";
InvoicePanel.Visible = true;
progressBarInvoice.Value = 0;
progressBarInvoice.Maximum = InvoiceNums.Count;
//for each invoice collected let's copy it
InvoiceNums.ForEach(delegate(string inv)
{
if (OrderDAL.CheckOrderExist(inv))
{
// the order already exist
Order myorder = new Order();
myorder = OrderDAL.GetOrder(inv);
CopyImages(myorder, true);
OrderDAL.UpdateFulfillment(string.Format("Images Copied"), inv);
}
});
//let the user know how we did
MessageBoxButtons buttons = MessageBoxButtons.OK;
string strError = string.Format("{0} Invoices copied.", InvoiceNums.Count);
MessageBox.Show(this, strError, "Copy New Invoices", buttons, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1);
InvoicePanel.Visible = false;
}
Here, CopyImages is called as part of the foreach loop above.
public void CopyImages(Order order, bool CopyAllInv)
{
string baseTarget = WorkSpace.Text;
string CLhotfolderTarget = string.Empty;
//check to see if the order has been photo released. If it has add "pr" to the end of the invoice number
string prInvoice = "";
if (order.Header.SignatureLine != "null" && order.Header.SignatureChecks != "null")
{
prInvoice = "pr";
}
string PackageName = null;
string CustomerName = null;
string Phone = null;
string email = null;
string PlayerInfo = null;
string PlayerName = null;
string PlayerNumber = null;
string MainEventName = null;
string MainEventCode = null;
string CLemail = null;
//go to the DB and get the info
mySqlInterface.Connect();
bool videoPackage = mySqlInterface.VideoInfo(order.Header.InvoiceNumber, out PackageName, out CustomerName, out Phone, out email, out PlayerName, out PlayerNumber, out MainEventName, out MainEventCode);
mySqlInterface.Close();
if (videoPackage)
{
if (PackageName.Contains("Video") || PackageName.Contains("Ultimate Ripken"))
{
CLemail = MainEventCode + "_" + email.Replace("#", "_").Replace(".", "_").Replace("+", "_");
PlayerInfo = PlayerName + " " + PlayerNumber;
int template_ID = 0;
if (txtCLtemplateID.Text != "")
{
template_ID = Convert.ToInt32(txtCLtemplateID.Text);
}
//we will always need a hotfolder. So let's set and create it now
CLhotfolderTarget = txtCLhotfolder.Text + "\\toUpload\\" + CLemail;
if (!System.IO.Directory.Exists(CLhotfolderTarget))
{
// create the directory
System.IO.Directory.CreateDirectory(CLhotfolderTarget);
}
int maxImages = 7;
int package_type = 2;
string[] favoritesArray = new string[maxImages];
//populate the array of images for the video
int count = 0;
foreach (Order.InvoiceImages image in order.ImageList)
{
favoritesArray[count] = image.ImageName;
count++;
}
//let's call the API and send info to CL
SendPostAsync(CustomerName, email, Phone, maxImages, MainEventName, MainEventCode, CLemail, package_type, PlayerInfo, template_ID, favoritesArray);
}
}
}
public async Task SendPostAsync(string name, string email, string phone, int photo_count, string event_name, string event_id, string dir_name, int package_type, string video_text, int template_id, string[] favoritesArray)
{
string postURL = null;
string token = null;
int delivery_method = 2;
//production
postURL = "https://search.apicall.com/photographer/customer";
token = "token xxxxxxxxxxxxxxxxxxxxx";
HttpClient client = new HttpClient();
client.BaseAddress = new Uri(postURL);
client.DefaultRequestHeaders.Add("Authorization", token);
string POSTcall = JsonConvert.SerializeObject(new { name, email, phone, photo_count, event_id, event_name, dir_name, package_type, video_text, delivery_method, template_id, favorites = favoritesArray });
//Send string to log file for debug
WriteLog(POSTcall);
StringContent stringContent = new StringContent(POSTcall, UnicodeEncoding.UTF8, "application/json");
HttpResponseMessage response = await client.PostAsync(new Uri(postURL), stringContent);
string POSTresponse = await response.Content.ReadAsStringAsync();
WriteLog(POSTresponse);
//simplified output for debug
if (POSTresponse.Contains("error") && POSTresponse.Contains("false"))
{
lblStatus.Text = "Error Sending to CL";
}
else
{
lblStatus.Text = "Successfully added to CL";
}
}
I have an await on the HttpResponseMessage response = await client.PostAsync
If I run this one at a time, it works. But when I run this through a loop and there are a bunch back to back, I think the PostAsyncs are getting stepped on. I'm missing entires in the WriteLog.
It seems I need to do the async/awaits further upstream, right? This way I can run the whole method.
Referencing Async/Await - Best Practices in Asynchronous Programming, event handlers allow async void so refactor the code to be async all the way through.
refactor CopyImages to await the posting of the data
public async Task CopyImages(Order order, bool CopyAllInv) {
//...omitted for brevity
if (videoPackage) {
if (PackageName.Contains("Video") || PackageName.Contains("Ultimate Ripken")) {
//...omitted for brevity
await SendPostAsync(CustomerName, email, Phone, maxImages, MainEventName, MainEventCode, CLemail, package_type, PlayerInfo, template_ID, favoritesArray);
}
}
}
And update the event handler
private async void btnCopyAllInvoices_Click(object sender, EventArgs e) {
//sets up a list to store the incoming invoice numbers from the DB
List<string> InvoiceNums = new List<string>();
mySqlInterface.Connect();
InvoiceNums = mySqlInterface.GetNewInvoices();
//prep the visuals
lblStatus.Text = "";
InvoicePanel.Visible = true;
progressBarInvoice.Value = 0;
progressBarInvoice.Maximum = InvoiceNums.Count;
//for each invoice collected let's copy it
foreach(string inv in InvoiceNums) {
if (OrderDAL.CheckOrderExist(inv)) {
// the order already exist
Order myorder = OrderDAL.GetOrder(inv);
await CopyImages(myorder, true);
OrderDAL.UpdateFulfillment(string.Format("Images Copied"), inv);
}
}
//let the user know how we did
MessageBoxButtons buttons = MessageBoxButtons.OK;
string strError = string.Format("{0} Invoices copied.", InvoiceNums.Count);
MessageBox.Show(this, strError, "Copy New Invoices", buttons, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1);
InvoicePanel.Visible = false;
}
I would also advise against creating a HttpClient for each post request. Extract that out and use a single client.
static Lazy<HttpClient> httpClient = new Lazy<HttpClient>(() => {
var postURL = "https://search.apicall.com/photographer/customer";
var token = "token xxxxxxxxxxxxxxxxxxxxx";
HttpClient client = new HttpClient();
client.BaseAddress = new Uri(postURL);
client.DefaultRequestHeaders.Add("Authorization", token);
return client
});
public async Task SendPostAsync(string name, string email, string phone, int photo_count, string event_name, string event_id, string dir_name, int package_type, string video_text, int template_id, string[] favoritesArray)
{
var postURL = "https://search.apicall.com/photographer/customer";
int delivery_method = 2;
string POSTcall = JsonConvert.SerializeObject(new { name, email, phone, photo_count, event_id, event_name, dir_name, package_type, video_text, delivery_method, template_id, favorites = favoritesArray });
//Send string to log file for debug
WriteLog(POSTcall);
StringContent stringContent = new StringContent(POSTcall, UnicodeEncoding.UTF8, "application/json");
HttpResponseMessage response = await httpClient.Value.PostAsync(new Uri(postURL), stringContent);
string POSTresponse = await response.Content.ReadAsStringAsync();
WriteLog(POSTresponse);
//simplified output for debug
if (POSTresponse.Contains("error") && POSTresponse.Contains("false")) {
lblStatus.Text = "Error Sending to CL";
} else {
lblStatus.Text = "Successfully added to CL";
}
}
Below is my Dialog.
[Serializable]
public class EmailDialog : IDialog<object>
{
async Task IDialog<object>.StartAsync(IDialogContext context)
{
context?.Wait(RequestEmailAddress);
}
private async Task RequestEmailAddress(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
context.Wait(RequestEmailAddress);
var thumbNailCard = new ThumbnailCard
{
Title = "BotFramework Thumbnail Card",
Subtitle = "Your bots — wherever your users are talking",
Text = "Build and connect intelligent bots to interact with your users naturally wherever they are, from text/sms to Skype, Slack, Office 365 mail and other popular services.",
Images = new List<CardImage> { new CardImage("https://sec.ch9.ms/ch9/7ff5/e07cfef0-aa3b-40bb-9baa-7c9ef8ff7ff5/buildreactionbotframework_960.jpg") },
Buttons = new List<CardAction> { new CardAction(ActionTypes.OpenUrl, "Get Started", value: "https://docs.botframework.com/en-us/") }
};
var resultMessage = context.MakeMessage();
resultMessage.AttachmentLayout = AttachmentLayoutTypes.Carousel;
resultMessage.Attachments = new List<Attachment> { thumbNailCard.ToAttachment() };
await context.PostAsync(resultMessage);
}
}
After the card is sent, any user input is hitting the REST API endpoint and the method RequestEmailAddress is not called at all.
NOTE: Initially I forgot to add context.Wait(RequestEmailAddress); and did so only after a couple of run. Could it be the bot has already ran into infinite loop of not knowing the context?
I even tried clearing up the stack and clearing up the bot state. Nothing helped.
EDIT: Adding the message controller part
private async Task<Activity> OnHandleActivityType(Activity activity)
{
switch (activity.Type)
{
case ActivityTypes.ContactRelationUpdate:
OnContactRelationUpdate(activity);
break;
case ActivityTypes.ConversationUpdate:
OnConversationUpdate(activity);
break;
case ActivityTypes.DeleteUserData:
break;
case ActivityTypes.EndOfConversation:
break;
case ActivityTypes.Event:
break;
case ActivityTypes.Invoke:
break;
case ActivityTypes.Message:
OnMessageReceived(activity);
break;
case ActivityTypes.Ping:
break;
case ActivityTypes.Typing:
break;
default:
throw new NotImplementedException();
}
return null;
}
private async void OnMessageReceived(Activity activity)
{
var message = activity.Text;
if (message == AppConstants.RegisterEmail))
{
await Conversation.SendAsync(activity, () => new EmailDialog());
}
}
The context.Wait(RequestEmailAddress); call should be at the end of the RequestEmailAddress method.
private async Task RequestEmailAddress(IDialogContext context, IAwaitable<IMessageActivity> result)
{
var message = await result;
var thumbNailCard = new ThumbnailCard
{
Title = "BotFramework Thumbnail Card",
Subtitle = "Your bots — wherever your users are talking",
Text = "Build and connect intelligent bots to interact with your users naturally wherever they are, from text/sms to Skype, Slack, Office 365 mail and other popular services.",
Images = new List<CardImage> { new CardImage("https://sec.ch9.ms/ch9/7ff5/e07cfef0-aa3b-40bb-9baa-7c9ef8ff7ff5/buildreactionbotframework_960.jpg") },
Buttons = new List<CardAction> { new CardAction(ActionTypes.OpenUrl, "Get Started", value: "https://docs.botframework.com/en-us/") }
};
var resultMessage = context.MakeMessage();
resultMessage.AttachmentLayout = AttachmentLayoutTypes.Carousel;
resultMessage.Attachments = new List<Attachment> { thumbNailCard.ToAttachment() };
await context.PostAsync(resultMessage);
context.Wait(RequestEmailAddress);
}
Also, incoming messages will always hit your endpoint (because it's the entry point). Then they will be delivered to the dialog. That means that you need to update the OnMessageReceived method on your controller and remove the if check for the message text match otherwise the user message won't be delivered anywhere:
private async void OnMessageReceived(Activity activity)
{
await Conversation.SendAsync(activity, () => new EmailDialog());
}