/// <summary> /// Add data about a chat to the store. /// </summary> /// <param name="chat_id"></param> public chat addChat(long chat_id, string chatTitle) { if (getChat(chat_id) == null) { Console.WriteLine("Creating data for chat " + chat_id.ToString()); chat chatObj = new chat(chat_id, chatTitle); chatData.Add(chatObj); return(chatObj); } else { throw new InvalidDataException("Chat already exists!"); } }
/// <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); } }
/// <summary> /// Do a healthcheck, and archive any old presence data /// Called from mod_standard's backgorund loop. /// </summary> public void expectedReplyBackgroundProcessing() { RecentChatMembers.RemoveAll(x => x.chatID == x.userID); //TODO <- this should be a startup housekeeping check only. //TODO - are we calling this whole thing every loop at the moment? Move to mod_standard.background? //Remove any stale presence info RecentChatMembers.RemoveAll(x => x.lastSeen < DateTime.Now.Subtract(new TimeSpan(chatPresenceExpiresAfterHours, 0, 0))); Roboto.log.log("There are " + expectedReplies.Count() + " expected replies on the stack", logging.loglevel.verbose); //main processing try { //Remove any ERs that are for dead chats List <ExpectedReply> deadERs = new List <ExpectedReply>(); foreach (ExpectedReply er in expectedReplies) { chat c = getChat(er.chatID); if (c == null) { deadERs.Add(er); } } foreach (ExpectedReply er in deadERs) { expectedReplies.Remove(er); } Roboto.log.log("Removed " + deadERs.Count() + " dead expected replies, now " + expectedReplies.Count() + " remain", deadERs.Count() == 0 ? logging.loglevel.verbose : logging.loglevel.warn); //remove any expired ones int i = expectedReplies.RemoveAll(x => x.timeLogged < DateTime.Now.Subtract(TimeSpan.FromDays(Roboto.Settings.killInactiveChatsAfterXDays))); Roboto.log.log("Removed " + i + " expected replies, now " + expectedReplies.Count() + " remain", i == 0 ? logging.loglevel.verbose : logging.loglevel.warn); //Build up a list of user IDs 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); } }
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) { if (er == null) { Roboto.log.log("Expected reply found, but er not available.", logging.loglevel.critical); return(true); } if (pluginToCall == null) { Roboto.log.log("Expected reply plugin found, but not available.", logging.loglevel.critical); return(true); } //now send it to the plugin (remove first, so any checks can be done) expectedReplies.Remove(er); try { bool pluginProcessed = pluginToCall.replyReceived(er, m); if (pluginProcessed) { //reset our chat timer chat c = getChat(er.chatID); if (c != null) { c.resetLastUpdateTime(); } else { Roboto.log.log("Chat not found for update.", logging.loglevel.critical); } } 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); } //Do any follow up er actions. expectedReplyBackgroundProcessing(); } return(processed); }
private static void startBackground() { logging.longOp lo_s = new logging.longOp("Core Startup", 5); settings.loadPlugins(); lo_s.addone(); log.log("Loading Settings", logging.loglevel.high); Settings = settings.load(); lo_s.addone(); Settings.validate(); lo_s.addone(); log.initialise(); lo_s.complete(); log.log("I am " + Settings.botUserName, logging.loglevel.critical, Colors.White, false, true); Settings.startupChecks(); //AT THIS POINT THE GAME WILL START PROCESSING INSTRUCTIONS!!! //DONT GO PAST IN STARTUP TEST MODE //---------------------------- int ABANDONALLHOPE = 1; ABANDONALLHOPE++; //---------------------------- Settings.save(); if (Settings.isFirstTimeInitialised) { log.log(@"New XML file created in %appdata%\Roboto\ . Enter your API key in there and restart.", logging.loglevel.critical, Colors.White, false, true); } else { log.log("Starting main thread", logging.loglevel.high); DateTime lastUpdate = DateTime.MinValue; while (!endLoop) { //store the time to prevent hammering the service when its down. Pause for a couple of seconds if things are getting toasty if (lastUpdate > DateTime.Now.Subtract(TimeSpan.FromSeconds(10))) { Roboto.Settings.stats.logStat(new statItem("Hammering Prevention", typeof(Roboto))); log.log("Too quick, sleeping", logging.loglevel.warn); Thread.Sleep(2000); } lastUpdate = DateTime.Now; //TODO - move this code to the webAPI class string updateURL = Settings.telegramAPIURL + Settings.telegramAPIKey + "/getUpdates" + "?offset=" + Settings.getUpdateID() + "&timeout=" + Settings.waitDuration + "&limit=10"; HttpWebRequest request = (HttpWebRequest)WebRequest.Create(updateURL); log.log(".", logging.loglevel.low, Colors.White, true); request.Method = "GET"; request.ContentType = "application/json"; try { WebResponse webResponse = request.GetResponse(); using (Stream webStream = webResponse.GetResponseStream()) { if (webStream != null) { using (StreamReader responseReader = new StreamReader(webStream)) { string response = responseReader.ReadToEnd(); JObject jo = JObject.Parse(response); //success? string path = jo.First.Path; string result = jo.First.First.Value <string>(); if (path != "ok" || result != "True") { endLoop = true; log.log("Failure code from web service", logging.loglevel.high); throw new WebException("Failure code from web service"); } else { int resultID = 0; //open the response and parse it using JSON. Probably only one result, but should be more? foreach (JToken token in jo.SelectTokens("result.[*]"))//jo.Children()) //) records[*].data.importedPath" { string logText = Regex.Replace(token.ToString(), @"(\s|\n|\r|\r\n)+", " "); log.log(logText, logging.loglevel.verbose); //Find out what kind of message this is. //TOP LEVEL TOKENS JToken updateID_TK = token.First; JToken update_TK = updateID_TK.Next.First; //Flag the update ID as processed. int updateID = updateID_TK.First.Value <int>(); Settings.lastUpdate = updateID; //is this for a group chat? long chatID = update_TK.SelectToken("chat.id").Value <long>(); chat chatData = null; if (chatID < 0) { //find the chat chatData = Settings.getChat(chatID); string chatTitle = update_TK.SelectToken("chat.title").Value <string>(); //new chat, add if (chatData == null) { chatData = Settings.addChat(chatID, chatTitle); } if (chatData == null) { throw new DataMisalignedException("Something went wrong creating the new chat data"); } chatData.setTitle(chatTitle); } //Do we have an incoming message? if (update_TK.Path == "result[" + resultID.ToString() + "].message" && update_TK.SelectToken(".text") != null) { //prevent delays - its sent something valid back to us so we are probably OK. lastUpdate = DateTime.MinValue; if (chatData != null) { chatData.resetLastUpdateTime(); } message m = new message(update_TK); //now decide what to do with this stuff. bool processed = false; //check if this is an expected reply, and if so route it to the Settings.parseExpectedReplies(m); //TODO - call plugins in some kind of priority order foreach (Modules.RobotoModuleTemplate plugin in settings.plugins) { //Skip this message if the chat is muted. if (plugin.chatHook && (chatData == null || (chatData.muted == false || plugin.chatIfMuted))) { if ((!processed || plugin.chatEvenIfAlreadyMatched)) { processed = plugin.chatEvent(m, chatData); } } } } else { log.log("No text in update", logging.loglevel.verbose); } //dont know what other update types we want to monitor? //TODO - leave / kicked / chat deleted resultID++; } //NB: ER Housekepeping moved from here as called too frequently } } } } } catch (System.Net.WebException e) { log.log("Web Service Timeout during getUpdates: " + e.ToString(), logging.loglevel.high); Settings.stats.logStat(new statItem("BotAPI Timeouts", typeof(Roboto))); } catch (Exception e) { try { log.log("Exception caught at main loop. " + e.ToString(), logging.loglevel.critical, Colors.White, false, false, false, false, 2); } catch (Exception ex) { Console.Out.WriteLine("-----------------"); Console.Out.WriteLine("Error During LOGGING! Original Error was"); Console.Out.WriteLine(e.Message); Console.Out.WriteLine("Logging Error was"); Console.Out.WriteLine(ex.Message); } } //Perform all background processing, syncing etc.. Settings.backgroundProcessing(false); } log.log("Main loop finishing, saving", logging.loglevel.high); Roboto.Settings.save(); log.log("Saved data, exiting main loop", logging.loglevel.high); //todo - do something to allow window to close? logWindow.unlockExit(); log.log("All data saved cleanly - close the form again to exit", logging.loglevel.critical); } }