/// <summary> /// Handle a failed outbound message that a plugin expects a reply for. /// </summary> /// <param name="er"></param> public void parseFailedReply(ExpectedReply er) { expectedReplies.Remove(er); Modules.RobotoModuleTemplate pluginToCall = null; foreach (Modules.RobotoModuleTemplate plugin in settings.plugins) { if (er.pluginType == plugin.GetType().ToString()) { //stash these for calling outside of the "foreach" loop. This is so we can be sure it is called ONCE only, and so that we can remove //the expected reply before calling the method, so any post-processing works smoother. pluginToCall = plugin; } } //now send it to the plugin (remove first, so any checks can be done) if (pluginToCall == null) { Roboto.log.log("Expected Reply wasnt on the stack - probably sent in immediate-mode! Couldnt remove it", logging.loglevel.normal); } else { bool pluginProcessed = pluginToCall.replyReceived(er, null, true); if (!pluginProcessed) { Roboto.log.log("Plugin " + pluginToCall.GetType().ToString() + " didnt process the message it expected a reply to!", logging.loglevel.high); throw new InvalidProgramException("Plugin didnt process the message it expected a reply to!"); } } }
/// <summary> /// Add a new expected reply to the stack. Should be called internally only - New messages should be sent via TelegramAPI.GetExpectedReply /// </summary> /// <param name="e"></param> /// <param name="trySendImmediately">Try and send the message immediately, assuming nothing is outstanding. Will jump the queue, but not override any existing messages</param> /// <returns>An integer specifying the message id. -1 indicates it is queueed, long.MinValue indicates a failure</returns> public long newExpectedReply(ExpectedReply e, bool trySendImmediately) { //flag the user as present in the chat if (e.isPrivateMessage) { markPresence(e.userID, e.chatID, e.userName); } //check if we can send it? Get the messageID back long messageID = -1; //is this a message to a group? if (!e.isPrivateMessage) { //send, dont queue. //TODO - doesnt handle group PMs messageID = e.sendMessage(); } //this is a PM. Does the user have anything in the queue? else if (!userHasOutstandingMessages(e.userID)) { //send the message. messageID = e.sendMessage(); //queue if it was a question if (e.expectsReply) { expectedReplies.Add(e); } } //If they have messages in the queue, and we want to jump ahead, has one already been asked, or are we open? else if (trySendImmediately && !userHasOutstandingQuestions(e.userID)) { Roboto.log.log("Message jumping queue due to immediatemode", logging.loglevel.verbose); //send the message, grab the ID. messageID = e.sendMessage(); //did it work/ if (messageID == long.MinValue) { Roboto.log.log("Tried to send message using immediateMode, but it failed.", logging.loglevel.warn); return(messageID); } //queue if it was a question else if (e.expectsReply) { expectedReplies.Add(e); } } else { //chuck it on the stack if its going to be queued expectedReplies.Add(e); } //make sure we are in a safe state. This will make sure if we sent a message-only, that the next message(s) are processed. expectedReplyHousekeeping(); return(messageID); }
/// <summary> /// Send a message, which we are expecting a reply to. Message can be sent publically or privately. Replies will be detected and sent via the plugin replyReceived method. /// </summary> /// <param name="chatID"></param> /// <param name="text"></param> /// <param name="replyToMessageID"></param> /// <param name="selective"></param> /// <param name="answerKeyboard"></param> /// <returns>An integer specifying the message id. -1 indicates it is queueed, long.MinValue indicates a failure</returns> public static long GetExpectedReply(long chatID, long userID, string text, bool isPrivateMessage, Type pluginType, string messageData, string userName = null, long replyToMessageID = -1, bool selective = false, string answerKeyboard = "", bool useMarkdown = false, bool clearKeyboard = false, bool trySendImmediately = false) { ExpectedReply e = new ExpectedReply(chatID, userID, userName, text, isPrivateMessage, pluginType, messageData, replyToMessageID, selective, answerKeyboard, useMarkdown, clearKeyboard, true); //add the message to the stack. If it is sent, get the messageID back. long messageID = Roboto.Settings.newExpectedReply(e, trySendImmediately); return(messageID); }
/// <summary> /// Send a message. Returns the ID of the send message /// </summary> /// <param name="chatID">User or Chat ID</param> /// <param name="text"></param> /// <param name="markDown"></param> /// <param name="replyToMessageID"></param> /// <returns>An integer specifying the message id. -1 indicates it is queued, int.MinValue indicates a failure</returns> public static long SendMessage(long chatID, string text, string userName = null, bool markDown = false, long replyToMessageID = -1, bool clearKeyboard = false, bool trySendImmediately = false) { bool isPM = (chatID < 0 ? false : true); ExpectedReply e = new ExpectedReply(chatID, chatID, userName, text, isPM, null, null, replyToMessageID, false, "", markDown, clearKeyboard, false); //add the message to the stack. If it is sent, get the messageID back. long messageID = Roboto.Settings.newExpectedReply(e, trySendImmediately); return(messageID); }
/// <summary> /// Send a message. Returns the ID of the send message /// </summary> /// <param name="chatID">User or Chat ID</param> /// <param name="text"></param> /// <param name="markDown"></param> /// <param name="replyToMessageID"></param> /// <returns>An integer specifying the message id. -1 indicates it is queued, int.MinValue indicates a failure</returns> public static long SendMessage(long chatID, string text, bool markDown = false, long replyToMessageID = -1, bool clearKeyboard = false, bool trySendImmediately = false) { bool isPM = (chatID < 0 ? false : true); ExpectedReply e = new ExpectedReply(chatID, chatID, text, isPM , null, null, replyToMessageID, false, "", markDown, clearKeyboard, false); //add the message to the stack. If it is sent, get the messageID back. long messageID = Roboto.Settings.newExpectedReply(e, trySendImmediately); return messageID; }
public void removeReply(ExpectedReply r) { expectedReplies.Remove(r); }
public bool parseExpectedReplies(message m) { //are we expecteing this? bool processed = false; Modules.RobotoModuleTemplate pluginToCall = null; ExpectedReply er = null; try { foreach (ExpectedReply e in expectedReplies) { //we are looking for direct messages from the user where c_id = m_id, OR reply messages where m_id = reply_id //could trigger twice if we f****d something up - dont think this is an issue but checking processed flag for safety if (!processed && e.isSent() && m.userID == e.userID) { if (m.chatID == e.userID || m.replyMessageID == e.outboundMessageID) { //find the plugin, send the expectedreply to it foreach (Modules.RobotoModuleTemplate plugin in settings.plugins) { if (e.isOfType(plugin.GetType())) { //stash these for calling outside of the "foreach" loop. This is so we can be sure it is called ONCE only, and so that we can remove //the expected reply before calling the method, so any post-processing works smoother. pluginToCall = plugin; er = e; } } processed = true; } } } } catch (Exception e) { Roboto.log.log("Error matching incoming message to plugin - " + e.ToString(), logging.loglevel.critical); } if (processed) { expectedReplies.Remove(er); //now send it to the plugin (remove first, so any checks can be done) try { bool pluginProcessed = pluginToCall.replyReceived(er, m); if (pluginProcessed) { //reset our chat timer chat c = getChat(er.chatID); c.resetLastUpdateTime(); } else { throw new InvalidProgramException("Plugin didnt process the message it expected a reply to!"); } } catch (Exception e) { Roboto.log.log("Error calling plugin " + pluginToCall.GetType().ToString() + " with expected reply. " + e.ToString(), logging.loglevel.critical); } /*/are there any more messages for the user? If so, find & send * ExpectedReply messageToSend = null; * foreach (ExpectedReply e in expectedReplies) * { * if (e.userID == m.userID) * { * if (messageToSend == null || e.timeLogged < messageToSend.timeLogged) * { * messageToSend = e; * } * * } * } * * //send it * //note that the plugin might send an urgent message during this processing that may have jumped the queue (using trySendImmediate param) * if ( !userHasOutstandingMessages(m.userID) && messageToSend != null) * { * messageToSend.sendMessage(); * //make sure we are in a safe state. This will make sure if we sent a message-only, that the next message(s) are processed. * expectedReplyHousekeeping(); * } */ expectedReplyHousekeeping(); } return(processed); }
/// <summary> /// Make sure any reply processing is being done /// </summary> public void expectedReplyHousekeeping() { //Build up a list of user IDs //List<int> userIDs = new List<int>(); //foreach (ExpectedReply e in expectedReplies) { userIDs.Add(e.userID); } //userIDs = (List<int>)userIDs.Distinct<int>(); try { List <long> userIDs = expectedReplies.Select(e => e.userID).Distinct().ToList <long>(); //remove any invalid messages List <ExpectedReply> messagesToRemove = expectedReplies.Where(e => e.outboundMessageID > 0 && e.expectsReply == false).ToList(); if (messagesToRemove.Count > 0) { Roboto.log.log("Removing " + messagesToRemove.Count() + " messages from queue as they are sent and dont require a reply", logging.loglevel.warn); } foreach (ExpectedReply e in messagesToRemove) { expectedReplies.Remove(e); } foreach (long userID in userIDs) { bool retry = true; while (retry) { //for each user, check if a message has been sent, and track the oldest message ExpectedReply oldest = null; List <ExpectedReply> userReplies = expectedReplies.Where(e => e.userID == userID).ToList(); //try find a message to send. Drop out if we already have a sent message on the stack (waiting for a reply) bool sent = false; foreach (ExpectedReply e in userReplies) { if (e.isSent()) { sent = true; } //message is waiting else { if (oldest == null || e.timeLogged < oldest.timeLogged) { oldest = e; } } } //send the message if neccessary if (!sent && oldest != null) { oldest.sendMessage(); if (!oldest.expectsReply) { expectedReplies.Remove(oldest); } //make sure we are in a safe state. This will make sure if we sent a message-only, that the next message(s) are processed. } //what do we do next? if (sent == true) { retry = false; } // drop out if we have a message awaiting an answer else if (oldest == null) { retry = false; } // drop out if we have no messages to send else if (oldest.expectsReply) { retry = false; } //drop out if we sent a message that expects a reply } } } catch (Exception e) { Roboto.log.log("Error during expected reply housekeeping " + e.ToString(), logging.loglevel.critical); } }
/// <summary> /// Send a message, which we are expecting a reply to. Message can be sent publically or privately. Replies will be detected and sent via the plugin replyRecieved method. /// </summary> /// <param name="chatID"></param> /// <param name="text"></param> /// <param name="replyToMessageID"></param> /// <param name="selective"></param> /// <param name="answerKeyboard"></param> /// <returns>An integer specifying the message id. -1 indicates it is queueed, long.MinValue indicates a failure</returns> public static long GetExpectedReply(long chatID, long userID, string text, bool isPrivateMessage, Type pluginType, string messageData, long replyToMessageID = -1, bool selective = false, string answerKeyboard = "", bool useMarkdown = false, bool clearKeyboard = false, bool trySendImmediately = false) { ExpectedReply e = new ExpectedReply(chatID, userID, text, isPrivateMessage, pluginType, messageData, replyToMessageID, selective, answerKeyboard, useMarkdown, clearKeyboard, true ); //add the message to the stack. If it is sent, get the messageID back. long messageID = Roboto.Settings.newExpectedReply(e, trySendImmediately); return messageID; }
/// <summary> /// Send the message in the expected reply. Should only be called from the expectedReply Class. May or may not expect a reply. /// </summary> /// <param name="e"></param> /// <returns>A long specifying the message id. long.MinValue indicates a failure. Negative values are error codes</returns> public static long postExpectedReplyToPlayer(ExpectedReply e) { Roboto.Settings.stats.logStat(new statItem("Outgoing Msgs", typeof(TelegramAPI))); string postURL = Roboto.Settings.telegramAPIURL + Roboto.Settings.telegramAPIKey + "/sendMessage"; //assemble collection of name/value data var pairs = new NameValueCollection(); string chatID = e.isPrivateMessage ? e.userID.ToString() : e.chatID.ToString(); //send to chat or privately try { pairs.Add("chat_id", chatID); if (e.text.Length > 1950) { e.text = e.text.Substring(0, 1950); } //check if the user has participated in multiple chats recently, so we can stamp the message with the current chat title. //only do this where the message relates to a chat. The chat ID shouldnt = the user id if this is the case. if (e.isPrivateMessage && e.chatID != e.userID && e.chatID < 0) { int nrChats = Roboto.Settings.getChatPresence(e.userID).Count(); if (nrChats > 1) { //get the current chat; chat c = Roboto.Settings.getChat(e.chatID); if (c == null) { Roboto.log.log("Couldnt find chat for " + e.chatID + " - did you use the userID accidentally?", logging.loglevel.high); } else { if (e.markDown && c.chatTitle != null) { e.text = "*" + c.chatTitle + "* :" + "\r\n" + e.text; } else { e.text = "=>" + c.chatTitle + "\r\n" + e.text; } } } } pairs.Add("text", e.text); if (e.markDown) { pairs["parse_mode"] = "Markdown"; } } catch (Exception ex) { Roboto.log.log("Error assembling message!. " + ex.ToString(), logging.loglevel.critical); } try //TODO - cant see how this is erroring here. Added try/catch to try debug it. { //force a reply if we expect one, and the keyboard is empty if (e.expectsReply && (e.keyboard == null || e.keyboard == "")) { bool forceReply = (!e.isPrivateMessage); //pairs.Add("reply_markup", "{\"force_reply\":true,\"selective\":" + e.selective.ToString().ToLower() + "}"); pairs.Add("reply_markup", "{\"force_reply\":" //force reply if we are NOT in a PM + forceReply.ToString().ToLower() //mark selective if passed in + ",\"selective\":" + e.selective.ToString().ToLower() + "}"); } else if (e.clearKeyboard) { pairs["reply_markup"] = "{\"hide_keyboard\":true}"; } else if (e.keyboard != null && e.keyboard != "") { pairs.Add("reply_markup", "{" + e.keyboard + "}"); } } catch (Exception ex) { //if we failed to attach, it probably wasnt important! Roboto.log.log("Error assembling message pairs. " + ex.ToString(), logging.loglevel.high); } try //TODO - cant see how this is erroring here. Added try/catch to try debug it. { if (e.replyToMessageID != -1) { pairs.Add("reply_to_message_id", e.replyToMessageID.ToString()); } } catch (Exception ex) { //if we failed to attach, it probably wasnt important! Roboto.log.log("Error attaching Reply Message ID to message. " + ex.ToString() , logging.loglevel.high); } try { JObject response = sendPOST(postURL, pairs).Result; if (response != null) { bool success = response.SelectToken("ok").Value<Boolean>(); if (success) { JToken response_token = response.SelectToken("result"); if (response_token != null) { JToken messageID_token = response.SelectToken("result.message_id"); if (messageID_token != null) { int messageID = messageID_token.Value<int>(); return messageID; } else { Roboto.log.log("MessageID Token was null.", logging.loglevel.high); } } else { Roboto.log.log("Response Token was null.", logging.loglevel.high); } } else { int errorCode = response.SelectToken("error_code").Value<int>(); string errorDesc = response.SelectToken("description").Value<string>(); if (errorCode == 400 && errorDesc == "PEER_ID_INVALID") { //return a -403 for this - we want to signal that the call failed Roboto.Settings.parseFailedReply(e); return -403; } else if (errorCode == 403 && (errorDesc == "Bot was blocked by the user" || errorDesc == "Forbidden: Bot can't initiate conversation with a user")) { //return a -403 for this - we want to signal that the call failed Roboto.Settings.parseFailedReply(e); return -403; } else if (errorCode == 403 && errorDesc == "Forbidden: bot was kicked from the group chat") { //return a -403 for this - we want to signal that the call failed Roboto.Settings.parseFailedReply(e); return -403; } else { Roboto.log.log("Unmapped error recieved - " + errorCode + " " + errorDesc, logging.loglevel.high); Roboto.Settings.parseFailedReply(e); return -1; } } } else { Roboto.log.log("Response was null.", logging.loglevel.high); } Roboto.Settings.parseFailedReply(e); return long.MinValue; } catch (WebException ex) { Roboto.log.log("Couldnt send message to " + chatID.ToString() + " because " + ex.ToString(), logging.loglevel.high); //Mark as failed and return the failure to the calling method if (e.expectsReply) { Roboto.log.log("Returning message " + e.messageData + " to plugin " + e.pluginType.ToString() + " as failed.", logging.loglevel.high); Roboto.Settings.parseFailedReply(e); } return long.MinValue; } catch (Exception ex) { Roboto.log.log("Exception sending message to " + chatID.ToString() + " because " + ex.ToString(), logging.loglevel.high); //Mark as failed and return the failure to the calling method if (e.expectsReply) { Roboto.log.log("Returning message " + e.messageData + " to plugin " + e.pluginType.ToString() + " as failed.", logging.loglevel.high); Roboto.Settings.parseFailedReply(e); } return long.MinValue; } }
/// <summary> /// Send the message in the expected reply. Should only be called from the expectedReply Class. May or may not expect a reply. /// </summary> /// <param name="e"></param> /// <returns>A long specifying the message id. long.MinValue indicates a failure. Negative values are error codes</returns> public static long postExpectedReplyToPlayer(ExpectedReply e) { Roboto.Settings.stats.logStat(new statItem("Outgoing Msgs", typeof(TelegramAPI))); string postURL = Roboto.Settings.telegramAPIURL + Roboto.Settings.telegramAPIKey + "/sendMessage"; //assemble collection of name/value data var pairs = new NameValueCollection(); string chatID = e.isPrivateMessage ? e.userID.ToString() : e.chatID.ToString(); //send to chat or privately try { pairs.Add("chat_id", chatID); if (e.text.Length > 1950) { e.text = e.text.Substring(0, 1950); } //check if the user has participated in multiple chats recently, so we can stamp the message with the current chat title. //only do this where the message relates to a chat. The chat ID shouldnt = the user id if this is the case. if (e.isPrivateMessage && e.chatID != e.userID && e.chatID < 0) { int nrChats = Roboto.Settings.getChatPresence(e.userID).Count(); if (nrChats > 1) { //get the current chat; chat c = Roboto.Settings.getChat(e.chatID); if (c == null) { Roboto.log.log("Couldnt find chat for " + e.chatID + " - did you use the userID accidentally?", logging.loglevel.high); } else { if (e.markDown && c.chatTitle != null) { e.text = "*" + c.chatTitle + "* :" + "\r\n" + e.text; } else { e.text = "=>" + c.chatTitle + "\r\n" + e.text; } } } } pairs.Add("text", e.text); if (e.markDown) { pairs["parse_mode"] = "Markdown"; } } catch (Exception ex) { Roboto.log.log("Error assembling message!. " + ex.ToString(), logging.loglevel.critical); } try //TODO - cant see how this is erroring here. Added try/catch to try debug it. { //force a reply if we expect one, and the keyboard is empty if (e.expectsReply && (e.keyboard == null || e.keyboard == "")) { bool forceReply = (!e.isPrivateMessage); //pairs.Add("reply_markup", "{\"force_reply\":true,\"selective\":" + e.selective.ToString().ToLower() + "}"); pairs.Add("reply_markup", "{\"force_reply\":" //force reply if we are NOT in a PM + forceReply.ToString().ToLower() //mark selective if passed in + ",\"selective\":" + e.selective.ToString().ToLower() + "}"); } else if (e.clearKeyboard) { pairs["reply_markup"] = "{\"hide_keyboard\":true}"; } else if (e.keyboard != null && e.keyboard != "") { pairs.Add("reply_markup", "{" + e.keyboard + "}"); } } catch (Exception ex) { //if we failed to attach, it probably wasnt important! Roboto.log.log("Error assembling message pairs. " + ex.ToString(), logging.loglevel.high); } try //TODO - cant see how this is erroring here. Added try/catch to try debug it. { if (e.replyToMessageID != -1) { pairs.Add("reply_to_message_id", e.replyToMessageID.ToString()); } } catch (Exception ex) { //if we failed to attach, it probably wasnt important! Roboto.log.log("Error attaching Reply Message ID to message. " + ex.ToString(), logging.loglevel.high); } try { JObject response = sendPOST(postURL, pairs).Result; if (response != null) { bool success = response.SelectToken("ok").Value <Boolean>(); if (success) { JToken response_token = response.SelectToken("result"); if (response_token != null) { JToken messageID_token = response.SelectToken("result.message_id"); if (messageID_token != null) { int messageID = messageID_token.Value <int>(); return(messageID); } else { Roboto.log.log("MessageID Token was null.", logging.loglevel.high); } } else { Roboto.log.log("Response Token was null.", logging.loglevel.high); } } else { int errorCode = response.SelectToken("error_code").Value <int>(); string errorDesc = response.SelectToken("description").Value <string>(); int result = parseErrorCode(errorCode, errorDesc); Roboto.log.log("Message failed with code " + result, logging.loglevel.high); Roboto.Settings.parseFailedReply(e); return(result); } } else { Roboto.log.log("Response was null.", logging.loglevel.high); } Roboto.Settings.parseFailedReply(e); return(long.MinValue); } catch (WebException ex) { Roboto.log.log("Couldnt send message to " + chatID.ToString() + " because " + ex.ToString(), logging.loglevel.high); //Mark as failed and return the failure to the calling method if (e.expectsReply) { Roboto.log.log("Returning message " + e.messageData + " to plugin " + e.pluginType.ToString() + " as failed.", logging.loglevel.high); Roboto.Settings.parseFailedReply(e); } return(long.MinValue); } catch (Exception ex) { Roboto.log.log("Exception sending message to " + chatID.ToString() + " because " + ex.ToString(), logging.loglevel.high); //Mark as failed and return the failure to the calling method if (e.expectsReply) { Roboto.log.log("Returning message " + e.messageData + " to plugin " + e.pluginType.ToString() + " as failed.", logging.loglevel.high); Roboto.Settings.parseFailedReply(e); } return(long.MinValue); } }