private static void CheckForUnknownUserOrRoom(CampfireState campfireInfo, Message msg, ICampfireAPI api) { // If this userId isn't already known... if (!campfireInfo.IsUserKnown(msg.UserId)) { // fetch all the user Info and then add User newUser = api.GetUser(msg.UserId); if ((newUser != null) && (newUser.Type == User.UserType.Member) && !string.IsNullOrEmpty(newUser.Name) && !string.IsNullOrEmpty(newUser.Email)) { Utils.TraceVerboseMessage(string.Format("Found a new User: {0}, Id: {1}", newUser.Name, newUser.Id)); campfireInfo.AddUser(newUser.Id, newUser.Name, newUser.Email, string.Empty); } } // If this roomId isn't already known... if (!campfireInfo.IsRoomKnown(msg.RoomId)) { // fetch all the user Info and then add Room newRoom = api.GetRoom(msg.RoomId); if (newRoom != null) { Utils.TraceVerboseMessage(string.Format("Found a new Room: {0}, Id: {1}", newRoom.Name, newRoom.Id)); campfireInfo.AddRoom(newRoom.Id, newRoom.Name, 0); } } }
private static void ProcessMessages_ProcessQueuedMessages(CampfireState campfireInfo, ICampfireAPI api) { /* * For each message that comes from any room: * Remove any entry in “PendingNotification” collection where PendingNotification.user is message.author (and room's match) * (e.g he’s already participating after being called out so no need to notify) * If there’s a User Reference match * Enter (user, time, room) into a PendingNotification collection * (unless user/room already exists in collection) * * For each entry in PendingNotification * If entry.timestamp is < Now – entry.user.ConfiguredDelay then * Send that user a notification that he’s been called out in entry.room */ Message msg; while ((msg = campfireInfo.PopMessage()) != null) { CheckForUnknownUserOrRoom(campfireInfo, msg, api); if (msg.Type == Message.MType.EnterMessage) { ProcessEnterMessage(campfireInfo, msg, api); } else if (msg.Type == Message.MType.TextMessage) { ProcessTextMessage(campfireInfo, msg, api); } } TriggerNotifications(campfireInfo); // Now look to fire any pending notifications }
private static void ProcessMessages_FetchNewMessages(CampfireState campfireInfo, ICampfireAPI api) { IList <CampfireState.RoomInfo> rooms = campfireInfo.Rooms; foreach (CampfireState.RoomInfo room in rooms) { List <Message> msgs; try { msgs = api.RecentMessages(room.Id, room.LastMessageId, Message.MType.TextMessage | Message.MType.EnterMessage); } catch (System.Net.WebException ex) { // oops. Let's break out and then try again later Utils.TraceException(TraceLevel.Warning, ex, "Exception calling API.RecentMessages"); break; } int lastMsgId = 0; foreach (Message msg in msgs) { lastMsgId = msg.Id; QueueMessage(campfireInfo, msg); } if (lastMsgId != 0) { // remember that we've now processed through this message Id. So next time we're only fetch newer messages campfireInfo.UpdateLastMessageId(room.Id, lastMsgId); } } }
public static bool Initialize(string storePath, INotifier notifier) { bool success = true; if (Instance == null) { BackingStorePath = string.Format(@"{0}\{1}", storePath, "CampfireInfo.xml"); Instance = Restore(BackingStorePath); if (Instance == null) { // Data in the xml file was corrupt/unreadable success = false; Instance = new CampfireState(); } if (notifier == null) { notifier = new Notifier(); } Instance.Notifier = notifier; } return(success); }
/// <summary> /// (Thread Safe) Save state to persistant store /// </summary> public void Save() { lock (this.saveLock) { CampfireState.SaveToStore(this); } }
/// <summary> /// Retrieve all 'new' messages from each room. And then "process" them. /// </summary> /// <param name="campfireInfo"></param> /// <param name="api"></param> private static void Work_ProcessNewMessagesForAllRooms(CampfireState campfireInfo, ICampfireAPI api) { /* * 2 phases: * - fetch all new message for all the rooms. Plase messages into appropriate queue * - Process all those queue up messages, in order * * Structuring this as 2 phases in case they are even executes by different threads. E.g. a thread for each room "waiting" on messages. */ ProcessMessages_FetchNewMessages(campfireInfo, api); ProcessMessages_ProcessQueuedMessages(campfireInfo, api); }
private static void TriggerNotifications(CampfireState campfireInfo) { IList <CampfireState.PendingNotify> pending = campfireInfo.PendingNotifications; foreach (CampfireState.PendingNotify pn in pending) { if (pn.TriggerTime < DateTime.Now) { TriggerNotification(campfireInfo, pn.Reference); // send notification campfireInfo.RemovePendingNotification(pn.Reference.TargetUser.Id, pn.Reference.Room.Id); // remove from pending list } } }
private static void TriggerNotification(CampfireState campfireInfo, CampfireState.UserReference userRef) { if ((userRef.TargetUser != null) && (userRef.Room != null)) { string email = userRef.TargetUser.SmokeEmail; Utils.TraceVerboseMessage(string.Format("Sending notification: User: {0}, Room: {1}, SmokeEmail: {2}", userRef.TargetUser.Name, userRef.Room.Name, userRef.TargetUser.SmokeEmail)); string userName = (userRef.SourceUser != null) ? userRef.SourceUser.Name : "guest"; campfireInfo.Notifier.Doit(userRef.TargetUser.Name, userRef.Room.Name, userRef.Room.Id, email, SmokeSignalConfig.Instance.CampfireName, userName, userRef.NearByText); } }
public static void TraceMessage(TraceLevel level, Message msg, string extraText) { CampfireState campState = CampfireState.Instance; IList <CampfireState.UserInfo> allUsers = campState.Users; CampfireState.UserInfo user = allUsers.FirstOrDefault(u => u.Id == msg.UserId); string userName = (user != null) ? user.NickName : msg.UserId.ToString(); CampfireState.RoomInfo room = campState.Rooms.FirstOrDefault(r => r.Id == msg.RoomId); string roomName = (room != null) ? room.Name : msg.RoomId.ToString(); TraceMessage(level, string.Format("At: {0}, User: {1}, Room: {2}, {3}{4}{5}", msg.PostedAt.ToString(), userName, roomName, (extraText ?? ""), (!string.IsNullOrEmpty(extraText)) ? ": " : "", msg.TrimmedBody)); }
public void SendSettings(CampfireState.UserInfo user, bool haveChanged) { if (user == null) return; try { MailMessage msg = new MailMessage(); msg.To.Add(new MailAddress(user.EmailAddress)); msg.BodyEncoding = Encoding.ASCII; msg.Subject = string.Format(haveChanged ? "Your Smoke Signal settings have changed..." : "Your Smoke Signal settings"); StringBuilder sb = new StringBuilder(); AppendWithNewLine(sb, string.Format("Dear {0}", user.NickName)); AppendWithNewLine(sb); AppendWithNewLine(sb, string.Format(haveChanged ? "Your SmokeSignal settings have been changed per your request:" : "Here are the Smoke Signal settings as requested:")); int delay = user.DelayInMinutes > 0 ? user.DelayInMinutes : SmokeSignalConfig.Instance.DelayBeforeSmokeSignalInMinutes; bool defaultDelay = user.DelayInMinutes < 1; AppendWithNewLine(sb, string.Format(" Campfire Email : {0}", user.EmailAddress)); AppendWithNewLine(sb, string.Format(" Smoke Signal Email : {0}", user.SmokeEmail)); AppendWithNewLine(sb, string.Format(" Delay In Minutes : {0}{1}", delay, defaultDelay ? " (default)" : "")); AppendWithNewLine(sb); AppendWithNewLine(sb, "Sincerely,"); AppendWithNewLine(sb, "Smoke Signal"); msg.Body = sb.ToString(); SmtpClient smtp = ConfigSmtpClient(); Utils.TraceInfoMessage(string.Format("Sending Settings email. {0}", user.EmailAddress)); smtp.Send(msg); } catch (InvalidOperationException ex) { Utils.TraceException(TraceLevel.Error, ex, "Exception trying to send a help email"); } catch (SmtpException ex) { Utils.TraceException(TraceLevel.Error, ex, "Exception trying to send a help email"); } }
private static void SaveToStore(CampfireState state, string toPath) { if (!System.IO.File.Exists(toPath)) { string dir = System.IO.Path.GetDirectoryName(toPath); if (!System.IO.Directory.Exists(dir)) { System.IO.Directory.CreateDirectory(dir); } } XmlSerializer ser = new XmlSerializer(typeof(CampfireState)); System.IO.StreamWriter file = new System.IO.StreamWriter(toPath); ser.Serialize(file, state); file.Close(); }
/// <summary> /// This is called every so often to check if users Names or Email address has changed, or User has been Deleted /// </summary> /// <param name="campfireInfo"></param> private static void Work_ScanForUserChanges(CampfireState campfireInfo, ICampfireAPI api) { // iterate of all known users... and see if name or email address has changed // Must do this in a thread safe fashion by first getting all the ids. List <int> userIds = campfireInfo.UserIds(); foreach (int uid in userIds) { User user = api.GetUser(uid); if (user != null && !string.IsNullOrEmpty(user.Name) && !string.IsNullOrEmpty(user.Email)) { campfireInfo.UpdateUser(user.Id, user.Name, user.Email); } else { campfireInfo.DeleteUser(uid); } } }
private static void QueueMessage(CampfireState campfireInfo, Message msg) { if (msg.Type == Message.MType.TextMessage) { // only process messages posted within the last 1 hour if (msg.PostedAt >= DateTime.Now.AddHours(-1)) { // put msg into the Text Queue Utils.TraceVerboseMessage(msg, "Msg"); campfireInfo.QueueMessage(msg); } } else if (msg.Type == Message.MType.EnterMessage) { // put msg into the Enter Queue Utils.TraceVerboseMessage(msg, "Enter"); campfireInfo.QueueMessage(msg); } }
private static void ProcessTextMessage(CampfireState campfireInfo, Message msg, ICampfireAPI api) { // The person that posted this message... If they have a pending notification in the room... then cancel it... they've spoken campfireInfo.RemovePendingNotification(msg.UserId, msg.RoomId, msg.PostedAt, true); IList <CampfireState.UserInfo> allUsers = campfireInfo.Users; IList <CampfireState.UserReference> lazyNotificationUsers; IList <CampfireState.UserReference> immediateNotificationUsers; IList <CampfireState.UserReference> smokeSignalReferences; Utils.FindUserReferences(msg.Body, allUsers, out lazyNotificationUsers, out immediateNotificationUsers, out smokeSignalReferences); CampfireState.UserInfo source = allUsers.FirstOrDefault(u => u.Id == msg.UserId); CampfireState.RoomInfo room = CampfireState.Instance.Rooms.FirstOrDefault(r => r.Id == msg.RoomId); // special smoke signal commands only make sense if a legitimate user issued them if (source != null) { foreach (CampfireState.UserReference ri in smokeSignalReferences) { ri.SourceUser = source; ri.Room = room; ProcessSmokeSignalCommands(source.Id, ri); } } foreach (CampfireState.UserReference ri in immediateNotificationUsers) { ri.SourceUser = source; ri.Room = room; campfireInfo.AddPendingNotification(ri, msg.PostedAt.AddSeconds(0)); } foreach (CampfireState.UserReference ri in lazyNotificationUsers) { ri.SourceUser = source; ri.Room = room; int delay = ri.TargetUser.DelayInMinutes > 0 ? ri.TargetUser.DelayInMinutes : SmokeSignalConfig.Instance.DelayBeforeSmokeSignalInMinutes; campfireInfo.AddPendingNotification(ri, msg.PostedAt.AddSeconds(delay * 60)); } }
/// <summary> /// This is called every so often to update the known list of rooms /// </summary> /// <param name="campfireInfo"></param> private static void Work_ScanForAddOrRemoveRooms(CampfireState campfireInfo, ICampfireAPI api) { List <int> roomIds = campfireInfo.RoomIds(); List <Room> rooms = api.Rooms(); foreach (Room r in rooms) { // since r.id exists, remove it from 'roomIds' roomIds.Remove(r.Id); } List <int> roomsToDelete = roomIds; foreach (int roomToDeleteId in roomsToDelete) { campfireInfo.DeleteRoom(roomToDeleteId); } campfireInfo.AddRooms(rooms); }
private static CampfireState Restore(string fromPath) { CampfireState campInfo = null; bool error = false; if (!System.IO.File.Exists(fromPath)) { campInfo = new CampfireState(); campInfo.Save(); return(campInfo); } Exception caughtException = null; try { XmlDocument xmlDoc = new XmlDocument(); xmlDoc.Load(fromPath); XmlSerializer ser = new XmlSerializer(typeof(CampfireState)); using (StringReader reader = new StringReader(xmlDoc.OuterXml)) { campInfo = (CampfireState)ser.Deserialize(reader); reader.Close(); } } catch (XmlException ex) { caughtException = ex; } catch (IOException ex) { caughtException = ex; } catch (UnauthorizedAccessException ex) { caughtException = ex; } catch (System.Security.SecurityException ex) { caughtException = ex; } catch (InvalidOperationException ex) { caughtException = ex; } if (caughtException != null) { // xml file was corrupt. Make a backup and delete original Utils.TraceException(TraceLevel.Error, caughtException, string.Format("Campfile.xml was corrupt. Making a backup and starting with a new file.")); error = Utils.SafeBackupAndDelete(fromPath); } return(campInfo); }
private static void SaveToStore(CampfireState state) { CampfireState.SaveToStore(state, BackingStorePath); }
/// <summary> /// Several duties. Starts a thread which looks for new rooms every X minutes. /// Starts a thread which read messages for rooms. /// </summary> public MessageProcessor(CampfireState campfireInfo) { this.CampfireInfo = campfireInfo; this.WorkerThreads = new List <Thread>(); }
/// <summary> /// This is called every so often to check if users Names or Email address has changed, or User has been Deleted /// </summary> /// <param name="campfireInfo"></param> private static void Work_ScanForUserChanges(CampfireState campfireInfo, ICampfireAPI api) { // iterate of all known users... and see if name or email address has changed // Must do this in a thread safe fashion by first getting all the ids. List<int> userIds = campfireInfo.UserIds(); foreach (int uid in userIds) { User user = api.GetUser(uid); if (user != null && !string.IsNullOrEmpty(user.Name) && !string.IsNullOrEmpty(user.Email)) { campfireInfo.UpdateUser(user.Id, user.Name, user.Email); } else { campfireInfo.DeleteUser(uid); } } }
/// <summary> /// This is called every so often to update the known list of rooms /// </summary> /// <param name="campfireInfo"></param> private static void Work_ScanForAddOrRemoveRooms(CampfireState campfireInfo, ICampfireAPI api) { List<int> roomIds = campfireInfo.RoomIds(); List<Room> rooms = api.Rooms(); foreach (Room r in rooms) { // since r.id exists, remove it from 'roomIds' roomIds.Remove(r.Id); } List<int> roomsToDelete = roomIds; foreach (int roomToDeleteId in roomsToDelete) { campfireInfo.DeleteRoom(roomToDeleteId); } campfireInfo.AddRooms(rooms); }
private static void ProcessTextMessage(CampfireState campfireInfo, Message msg, ICampfireAPI api) { // The person that posted this message... If they have a pending notification in the room... then cancel it... they've spoken campfireInfo.RemovePendingNotification(msg.UserId, msg.RoomId, msg.PostedAt, true); IList<CampfireState.UserInfo> allUsers = campfireInfo.Users; IList<CampfireState.UserReference> lazyNotificationUsers; IList<CampfireState.UserReference> immediateNotificationUsers; IList<CampfireState.UserReference> smokeSignalReferences; Utils.FindUserReferences(msg.Body, allUsers, out lazyNotificationUsers, out immediateNotificationUsers, out smokeSignalReferences); CampfireState.UserInfo source = allUsers.FirstOrDefault(u => u.Id == msg.UserId); CampfireState.RoomInfo room = CampfireState.Instance.Rooms.FirstOrDefault(r => r.Id == msg.RoomId); // special smoke signal commands only make sense if a legitimate user issued them if (source != null) { foreach (CampfireState.UserReference ri in smokeSignalReferences) { ri.SourceUser = source; ri.Room = room; ProcessSmokeSignalCommands(source.Id, ri); } } foreach (CampfireState.UserReference ri in immediateNotificationUsers) { ri.SourceUser = source; ri.Room = room; campfireInfo.AddPendingNotification(ri, msg.PostedAt.AddSeconds(0)); } foreach (CampfireState.UserReference ri in lazyNotificationUsers) { ri.SourceUser = source; ri.Room = room; int delay = ri.TargetUser.DelayInMinutes > 0 ? ri.TargetUser.DelayInMinutes : SmokeSignalConfig.Instance.DelayBeforeSmokeSignalInMinutes; campfireInfo.AddPendingNotification(ri, msg.PostedAt.AddSeconds(delay * 60)); } }
private static void ProcessSmokeSignalCommands(int userId, CampfireState.UserReference ri) { // ??? Should a syntax error generate an email to the user explaining the problem? int pos = ri.NearByText.IndexOf(':'); if (pos < 0) return; string cmd = ri.NearByText.Remove(0, pos + 1); string[] args = cmd.Split('='); switch (args[0].ToLowerInvariant()) { case "help": CampfireState.Instance.Notifier.SendHelp(ri.SourceUser.Name, ri.SourceUser.EmailAddress); break; case "settings": CampfireState.Instance.Notifier.SendSettings(ri.SourceUser, false); break; case "delay": int newDelay; string newDelayStr = ""; if (args.Length > 2) return; if (args.Length == 2) newDelayStr = args[1]; if (string.IsNullOrEmpty(newDelayStr)) newDelayStr = "-1"; if (Int32.TryParse(newDelayStr, out newDelay)) { ri.SourceUser = CampfireState.Instance.UpdateUser(userId, newDelay); CampfireState.Instance.Notifier.SendSettings(ri.SourceUser, true); } break; case "altemail": string newEmail = null; if (args.Length > 2) return; if (args.Length == 2) newEmail = args[1]; if (!string.IsNullOrEmpty(newEmail) && !IsValidEmail(newEmail)) { return; } ri.SourceUser = CampfireState.Instance.UpdateAltEmail(ri.SourceUser.Id, newEmail); CampfireState.Instance.Notifier.SendSettings(ri.SourceUser, true); break; default: break; } }
private static void ProcessEnterMessage(CampfireState campfireInfo, Message msg, ICampfireAPI api) { // remove any pending notifications for this user in the room in which this Enter message appeared campfireInfo.RemovePendingNotification(msg.UserId, msg.RoomId, msg.PostedAt, true); }
private static CampfireState.UserReference InitReferenceInfo(string text, string target, CampfireState.UserInfo user, bool smokeSignalCommands) { string refText; if (smokeSignalCommands) { refText = target; } else if (text.Length < 80) { refText = text; } else { try { refText = FindSentenceContaining(text, target); } catch (ArgumentException ex) { refText = string.Empty; } } return new CampfireState.UserReference(refText, user); }
private static CampfireState Restore(string fromPath) { CampfireState campInfo = null; bool error = false; if (!System.IO.File.Exists(fromPath)) { campInfo = new CampfireState(); campInfo.Save(); return campInfo; } Exception caughtException = null; try { XmlDocument xmlDoc = new XmlDocument(); xmlDoc.Load(fromPath); XmlSerializer ser = new XmlSerializer(typeof(CampfireState)); using (StringReader reader = new StringReader(xmlDoc.OuterXml)) { campInfo = (CampfireState)ser.Deserialize(reader); reader.Close(); } } catch (XmlException ex) { caughtException = ex; } catch (IOException ex) { caughtException = ex; } catch (UnauthorizedAccessException ex) { caughtException = ex; } catch (System.Security.SecurityException ex) { caughtException = ex; } catch (InvalidOperationException ex) { caughtException = ex; } if (caughtException != null) { // xml file was corrupt. Make a backup and delete original Utils.TraceException(TraceLevel.Error, caughtException, string.Format("Campfile.xml was corrupt. Making a backup and starting with a new file.")); error = Utils.SafeBackupAndDelete(fromPath); } return campInfo; }
public UserReference(string text, CampfireState.UserInfo target) { this.TargetUser = target; this.NearByText = text; }
private static void ProcessMessages_FetchNewMessages(CampfireState campfireInfo, ICampfireAPI api) { IList<CampfireState.RoomInfo> rooms = campfireInfo.Rooms; foreach (CampfireState.RoomInfo room in rooms) { List<Message> msgs; try { msgs = api.RecentMessages(room.Id, room.LastMessageId, Message.MType.TextMessage | Message.MType.EnterMessage); } catch (System.Net.WebException ex) { // oops. Let's break out and then try again later Utils.TraceException(TraceLevel.Warning, ex, "Exception calling API.RecentMessages"); break; } int lastMsgId = 0; foreach (Message msg in msgs) { lastMsgId = msg.Id; QueueMessage(campfireInfo, msg); } if (lastMsgId != 0) { // remember that we've now processed through this message Id. So next time we're only fetch newer messages campfireInfo.UpdateLastMessageId(room.Id, lastMsgId); } } }
public static bool Initialize(string storePath, INotifier notifier) { bool success = true; if (Instance == null) { BackingStorePath = string.Format(@"{0}\{1}", storePath, "CampfireInfo.xml"); Instance = Restore(BackingStorePath); if (Instance == null) { // Data in the xml file was corrupt/unreadable success = false; Instance = new CampfireState(); } if (notifier == null) { notifier = new Notifier(); } Instance.Notifier = notifier; } return success; }
private static void ProcessMessages_ProcessQueuedMessages(CampfireState campfireInfo, ICampfireAPI api) { /* For each message that comes from any room: Remove any entry in “PendingNotification” collection where PendingNotification.user is message.author (and room's match) (e.g he’s already participating after being called out so no need to notify) If there’s a User Reference match Enter (user, time, room) into a PendingNotification collection (unless user/room already exists in collection) For each entry in PendingNotification If entry.timestamp is < Now – entry.user.ConfiguredDelay then Send that user a notification that he’s been called out in entry.room */ Message msg; while ((msg = campfireInfo.PopMessage()) != null) { CheckForUnknownUserOrRoom(campfireInfo, msg, api); if (msg.Type == Message.MType.EnterMessage) { ProcessEnterMessage(campfireInfo, msg, api); } else if (msg.Type == Message.MType.TextMessage) { ProcessTextMessage(campfireInfo, msg, api); } } TriggerNotifications(campfireInfo); // Now look to fire any pending notifications }
public void SendSettings(CampfireState.UserInfo user, bool haveChanged) { LogSent(user.Name, null, "Settings-Email"); }
private static void TriggerNotifications(CampfireState campfireInfo) { IList<CampfireState.PendingNotify> pending = campfireInfo.PendingNotifications; foreach (CampfireState.PendingNotify pn in pending) { if (pn.TriggerTime < DateTime.Now) { TriggerNotification(campfireInfo, pn.Reference); // send notification campfireInfo.RemovePendingNotification(pn.Reference.TargetUser.Id, pn.Reference.Room.Id); // remove from pending list } } }
// //Use TestCleanup to run code after each test has run //[TestCleanup()] //public void MyTestCleanup() //{ //} // #endregion private void Match(CampfireState expected, CampfireState actual) { Assert.AreEqual(expected.Users.Count, actual.Users.Count); Assert.AreEqual(expected.Rooms.Count, actual.Rooms.Count); foreach (CampfireState.RoomInfo room in expected.Rooms) { Assert.IsTrue(actual.Rooms.Count(r => r.Id == room.Id && r.LastMessageId == room.LastMessageId && r.Name == room.Name) == 1); } foreach (CampfireState.UserInfo user in expected.Users) { Assert.IsTrue(actual.Users.Count(u => u.Id == user.Id && u.Name == user.Name && u.EmailAddress == user.EmailAddress && u.AltEmailAddress == user.AltEmailAddress) == 1); } }
/// <summary> /// Several duties. Starts a thread which looks for new rooms every X minutes. /// Starts a thread which read messages for rooms. /// </summary> public MessageProcessor(CampfireState campfireInfo) { this.CampfireInfo = campfireInfo; this.WorkerThreads = new List<Thread>(); }