Browse Source

first commit

Stuart 1 year ago
commit
3be8577400
42 changed files with 2807 additions and 0 deletions
  1. 136 0
      .gitignore
  2. 28 0
      Dockerfile
  3. 21 0
      LICENSE
  4. 1 0
      README.md
  5. 28 0
      TFA-Bot.sln
  6. 141 0
      TFA-Bot/Data/dialplan.xml
  7. 75 0
      TFA-Bot/DataClasses/clsNetwork.cs
  8. 324 0
      TFA-Bot/DataClasses/clsNode.cs
  9. 121 0
      TFA-Bot/DataClasses/clsNodeGroup.cs
  10. 34 0
      TFA-Bot/DataClasses/clsNotificationPolicy.cs
  11. 31 0
      TFA-Bot/DataClasses/clsSetting.cs
  12. 70 0
      TFA-Bot/DataClasses/clsUser.cs
  13. 20 0
      TFA-Bot/DiscordBot/Commands/IBotCommand.cs
  14. 64 0
      TFA-Bot/DiscordBot/Commands/clsAlarm.cs
  15. 72 0
      TFA-Bot/DiscordBot/Commands/clsBotControl.cs
  16. 32 0
      TFA-Bot/DiscordBot/Commands/clsCall.cs
  17. 32 0
      TFA-Bot/DiscordBot/Commands/clsFembot.cs
  18. 32 0
      TFA-Bot/DiscordBot/Commands/clsGoodAfternoon.cs
  19. 33 0
      TFA-Bot/DiscordBot/Commands/clsGoodnight.cs
  20. 33 0
      TFA-Bot/DiscordBot/Commands/clsHelp.cs
  21. 32 0
      TFA-Bot/DiscordBot/Commands/clsListNodes.cs
  22. 32 0
      TFA-Bot/DiscordBot/Commands/clsSkyNet.cs
  23. 43 0
      TFA-Bot/DiscordBot/Commands/clsUsers.cs
  24. 15 0
      TFA-Bot/DiscordBot/DiscordBot.csproj
  25. 156 0
      TFA-Bot/DiscordBot/clsBotClient.cs
  26. 115 0
      TFA-Bot/DiscordBot/clsCommands.cs
  27. 178 0
      TFA-Bot/Program.cs
  28. 26 0
      TFA-Bot/Properties/AssemblyInfo.cs
  29. 31 0
      TFA-Bot/Spreadsheet/ASheetColumnHeader.cs
  30. 9 0
      TFA-Bot/Spreadsheet/ISpreadsheet.cs
  31. 126 0
      TFA-Bot/Spreadsheet/clsSpreadsheet.cs
  32. 130 0
      TFA-Bot/Spreadsheet/clsSpreadsheetReader.cs
  33. 111 0
      TFA-Bot/TFA-Bot.csproj
  34. 10 0
      TFA-Bot/Utils.cs
  35. 10 0
      TFA-Bot/Utils/clsExtentions.cs
  36. 91 0
      TFA-Bot/Utils/clsRollingAverage.cs
  37. 118 0
      TFA-Bot/clsAlarm.cs
  38. 66 0
      TFA-Bot/clsAlarmManager.cs
  39. 109 0
      TFA-Bot/clsCaller.cs
  40. 52 0
      TFA-Bot/clsExtenstions.cs
  41. 12 0
      TFA-Bot/packages.config
  42. 7 0
      entrypoint.sh

+ 136 - 0
.gitignore

@@ -0,0 +1,136 @@
+sipp-*
+vs/
+
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+
+[Dd]ebug/
+[Rr]elease/
+x64/
+[Bb]in/
+[Oo]bj/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.log
+*.svclog
+*.scc
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.Publish.xml
+*.pubxml
+*.azurePubxml
+
+# NuGet Packages Directory
+## TODO: If you have NuGet Package Restore enabled, uncomment the next line
+packages/
+## TODO: If the tool you use requires repositories.config, also uncomment the next line
+!packages/repositories.config
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+sql/
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+![Ss]tyle[Cc]op.targets
+~$*
+*~
+*.dbmdl
+*.[Pp]ublish.xml
+
+*.publishsettings
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file to a newer
+# Visual Studio version. Backup files are not needed, because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+App_Data/*.mdf
+App_Data/*.ldf
+
+# =========================
+# Windows detritus
+# =========================
+
+# Windows image file caches
+Thumbs.db
+ehthumbs.db
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Mac desktop service store files
+.DS_Store
+
+_NCrunch*

+ 28 - 0
Dockerfile

@@ -0,0 +1,28 @@
+FROM ubuntu:18.04
+
+RUN apt-get update \
+	&& apt-get -y install joe less gnupg ssh wget curl net-tools iputils-ping libncurses5-dev autoconf libncursesw5-dev git
+
+# Set the timezone.
+RUN ln -fs /usr/share/zoneinfo/UTC /etc/localtime
+
+RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF
+RUN echo "deb https://download.mono-project.com/repo/ubuntu stable-bionic main" | tee /etc/apt/sources.list.d/mono-official-stable.list
+
+RUN apt-get -y install mono-devel
+
+RUN mkdir -p /app
+WORKDIR /app
+RUN git clone https://github.com/SIPp/sipp.git
+RUN cd sipp && ./build.sh
+
+//git clone .....
+
+COPY Factom-Monitor/bin/Release/* /app/
+
+COPY entrypoint.sh /entrypoint.sh
+RUN chmod +x entrypoint.sh
+
+
+#ENTRYPOINT ["/bin/bash"]
+ENTRYPOINT ["/entrypoint.sh"]

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2018 The Factoid Authority (TFA) Inc. http://factoid.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 1 - 0
README.md

@@ -0,0 +1 @@
+# Factom Bot Monitor

+ 28 - 0
TFA-Bot.sln

@@ -0,0 +1,28 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2012
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TFA-Bot", "TFA-Bot\TFA-Bot.csproj", "{25609AB4-A77A-4B47-AAB8-B2DB39464A3F}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EF294E86-FB98-4572-A47B-6EA774197488}"
+	ProjectSection(SolutionItems) = preProject
+		Dockerfile = Dockerfile
+		entrypoint.sh = entrypoint.sh
+		.gitignore = .gitignore
+		LICENSE = LICENSE
+		README.md = README.md
+	EndProjectSection
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|x86 = Debug|x86
+		Release|x86 = Release|x86
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{25609AB4-A77A-4B47-AAB8-B2DB39464A3F}.Debug|x86.ActiveCfg = Debug|x86
+		{25609AB4-A77A-4B47-AAB8-B2DB39464A3F}.Debug|x86.Build.0 = Debug|x86
+		{25609AB4-A77A-4B47-AAB8-B2DB39464A3F}.Release|x86.ActiveCfg = Release|x86
+		{25609AB4-A77A-4B47-AAB8-B2DB39464A3F}.Release|x86.Build.0 = Release|x86
+	EndGlobalSection
+	GlobalSection(NestedProjects) = preSolution
+	EndGlobalSection
+EndGlobal

+ 141 - 0
TFA-Bot/Data/dialplan.xml

@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!DOCTYPE scenario SYSTEM "sipp.dtd">
+<!--                                                                    -->
+<!--                 Sipp authenticated invit scenario.                 -->
+<!--                                                                    -->
+<scenario name="Basic Sipstone UAC">
+  <send retrans="500">
+    <![CDATA[
+
+      INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
+      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+      From: sipp <sip:[service]@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
+      To: sut <sip:[service]@[remote_ip]:[remote_port]>
+      Call-ID: [call_id]
+      CSeq: 1 INVITE
+      Contact: sip:[service]@[local_ip]:[local_port]
+      Max-Forwards: 70
+      Subject: Performance Test
+      Content-Type: application/sdp
+      Content-Length: [len]
+
+      v=0
+      o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
+      s=-
+      t=0 0
+      a=rtpmap:0 PCMU/8000
+
+    ]]>
+  </send>
+
+  <recv response="100"
+        optional="true">
+  </recv>
+    <recv response="401" auth="true">
+  </recv>
+
+  <send>
+    <![CDATA[
+
+      ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
+      Via: SIP/2.0/[transport] [local_ip]:[local_port]
+      From: sipp <sip:[service]@[local_ip]:[local_port]>;tag=[call_number]
+      To: sut <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
+      Call-ID: [call_id]
+      CSeq: 1 ACK
+      Contact: sip:[service]@[local_ip]:[local_port]
+      Max-Forwards: 70
+      Subject: Performance Test
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <send retrans="500">
+    <![CDATA[
+
+      INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
+      Via: SIP/2.0/[transport] [local_ip]:[local_port]
+      From: sipp <sip:[service]@[local_ip]:[local_port]>;tag=[call_number]
+      To: sut <sip:[service]@[remote_ip]:[remote_port]>
+      Call-ID: [call_id]
+      CSeq: 2 INVITE
+      Contact: sip:[service]@[local_ip]:[local_port]
+      [authentication ]
+      Max-Forwards: 70
+      Subject: Performance Test
+      Content-Type: application/sdp
+      Content-Length: [len]
+
+      v=0
+      o=user1 53655765 2353687637 IN IP[local_ip_type] [local_ip]
+      s=-
+      t=0 0
+      a=rtpmap:0 PCMU/8000
+
+    ]]>
+  </send>
+
+  <recv response="100"
+        optional="true">
+  </recv>
+
+  <recv response="180" optional="true">
+  </recv>
+
+  <recv response="183" optional="true">
+  </recv>
+  <recv response="200" rtd="true">
+  </recv>
+
+  <!-- Packet lost can be simulated in any send/recv message by         -->
+  <!-- by adding the 'lost = "10"'. Value can be [1-100] percent.       -->
+  <send>
+    <![CDATA[
+
+      ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
+      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+      From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
+      To: sut <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
+      Call-ID: [call_id]
+      CSeq: 1 ACK
+      Contact: sip:sipp@[local_ip]:[local_port]
+      Max-Forwards: 70
+      Subject: Performance Test
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <!-- This delay can be customized by the -d command-line option       -->
+  <!-- or by adding a 'milliseconds = "value"' option here.             -->
+  <pause/>
+
+  <!-- The 'crlf' option inserts a blank line in the statistics report. -->
+  <send retrans="500">
+    <![CDATA[
+
+      BYE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
+      Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
+      From: sipp <sip:sipp@[local_ip]:[local_port]>;tag=[pid]SIPpTag00[call_number]
+      To: sut <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
+      Call-ID: [call_id]
+      CSeq: 2 BYE
+      Contact: sip:sipp@[local_ip]:[local_port]
+      Max-Forwards: 70
+      Subject: Performance Test
+      Content-Length: 0
+
+    ]]>
+  </send>
+
+  <recv response="200" crlf="true">
+  </recv>
+
+  <!-- definition of the response time repartition table (unit is ms)   -->
+  <ResponseTimeRepartition value="10, 20, 30, 40, 50, 100, 150, 200"/>
+
+  <!-- definition of the call length repartition table (unit is ms)     -->
+  <CallLengthRepartition value="10, 50, 100, 500, 1000, 5000, 10000"/>
+
+</scenario>

+ 75 - 0
TFA-Bot/DataClasses/clsNetwork.cs

@@ -0,0 +1,75 @@
+using System;
+namespace TFABot
+{
+    public class clsNetwork : ISpreadsheet<clsNetwork>
+    {
+        public clsNetwork()
+        {
+        
+        }
+        
+        [ASheetColumnHeader(true,"name")]
+        public string Name {get;set;}
+        [ASheetColumnHeader("time")]
+        public uint BlockTimeSeconds {get;set;}
+        [ASheetColumnHeader("after")]
+        public uint BlockTimeSecondsAllowance {get;set;}
+        [ASheetColumnHeader("stall")]
+        public string StallNotification {get;set;}
+        
+       
+        public uint TopHeight {get; private set;}
+        public DateTime? LastHeight  {get; private set;}
+        public DateTime? NextHeight  {get; private set;}
+        int LateHeightCount;
+        
+        public clsAlarm NetworkAlarm = null;
+        
+        public void Update(clsNetwork network)
+        {
+            if (Name != network.Name) throw new Exception("index name does not match");
+            
+            Name = network.Name;
+            BlockTimeSeconds = network.BlockTimeSeconds;
+            BlockTimeSecondsAllowance = network.BlockTimeSecondsAllowance;
+            StallNotification = network.StallNotification;
+        }
+        
+        public void PostPopulate()
+        {
+        
+        }
+        
+        public void SetTopHeight(uint height)
+        {
+            TopHeight = height;
+            LastHeight = DateTime.UtcNow;
+            NextHeight = LastHeight.Value.AddSeconds(BlockTimeSeconds);
+        }
+        
+        public void CheckStall()
+        {
+            bool ok = true;
+            if (NextHeight.HasValue && DateTime.UtcNow > NextHeight.Value)
+            {
+                var seconds = (DateTime.UtcNow - NextHeight).Value.TotalSeconds;
+                if (seconds>BlockTimeSecondsAllowance)
+                {
+                    ok=false;
+                    if (++LateHeightCount==1)
+                    {
+                        NetworkAlarm = new clsAlarm(clsAlarm.enumAlarmType.Network,$"Warning Height {seconds:0} sec late.  {Name} Stall?",this);
+                        Program.AlarmManager.New(NetworkAlarm);
+                    }
+                }
+            }
+            if (LateHeightCount>0 && ok)
+            {
+                LateHeightCount=0;
+                Program.AlarmManager.Clear(NetworkAlarm, $"Stall Alarm Cleared {Name}");
+                NetworkAlarm = null;
+            }
+        }
+        
+    }
+}

+ 324 - 0
TFA-Bot/DataClasses/clsNode.cs

@@ -0,0 +1,324 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Net.NetworkInformation;
+using System.Threading.Tasks;
+using RestSharp;
+
+namespace TFABot
+{
+    public class clsNode : ISpreadsheet<clsNode>
+    {
+    
+        clsRollingAverage LatencyList = new clsRollingAverage(10);
+        clsRollingAverage PacketLoss = new clsRollingAverage(100);
+        
+        public clsNode()
+        {
+        }
+        
+        
+        public clsNodeGroup NodeGroup;
+
+        
+        public uint LatencyLowest { get; private set;}
+
+        
+        [ASheetColumnHeader(true,"name")]
+        public string Name {get;set;}
+        [ASheetColumnHeader("group")]
+        public string Group {get;set;}
+        [ASheetColumnHeader("host")]
+        public string Host {get;set;}
+        [ASheetColumnHeader("monitor")]
+        public bool Monitor {get;set;}
+
+        
+        
+        Task HeightTask = null;
+        
+
+        public String ErrorMsg { get; private set;}
+        
+        
+        public int Latency
+        {
+          get{ return LatencyList.CurrentAverage; }
+        }
+        
+        public DateTime? LastLeaderHeight { get; private set;}
+        uint _leaderHeight = 0;
+        public uint LeaderHeight
+         { 
+            get
+            {
+                return _leaderHeight;
+            }
+            private set
+            {
+                if (_leaderHeight < value)
+                {
+                    _leaderHeight = value;
+                    LastLeaderHeight = DateTime.UtcNow;
+                }
+            }
+        }
+        
+        
+        clsAlarm AlarmHeightLow = null;
+        uint _heightLowCount;
+        public uint HeightLowCount
+        {
+           get
+           {
+             return _heightLowCount;
+           }
+           set
+           {
+             _heightLowCount = value;
+             if (_heightLowCount==0 && AlarmHeightLow!=null)
+             {
+                Program.AlarmManager.Clear(AlarmHeightLow,$"CLEARED:  {Name} height low cleared.");
+                AlarmHeightLow = null;
+                ErrorMsg="";
+             }
+             else if (_heightLowCount > 3 && _requestFailCount ==0 && AlarmHeightLow==null )
+             {
+                AlarmHeightLow = new clsAlarm(clsAlarm.enumAlarmType.Height,$"WARNING: {Name} height low.",this);
+                ErrorMsg="HEIGHT LOW";
+                Program.AlarmManager.New(AlarmHeightLow);
+             }
+           }
+        }
+        
+        clsAlarm AlarmLatencyLow = null;
+        uint _latencyLowCount;
+        public uint LatencyLowCount
+        {
+           get
+           {
+             return _latencyLowCount;
+           }
+           set
+           {
+             _latencyLowCount = value;
+             if (_latencyLowCount==0 && AlarmLatencyLow!=null)
+             {
+                Program.AlarmManager.Clear(AlarmLatencyLow,$"CLEARED:  {Name} poor latency cleared.");
+                AlarmLatencyLow = null;
+                ErrorMsg="";
+             }
+             else if (_requestFailCount ==0 && AlarmLatencyLow==null && _latencyLowCount > 3)
+             {
+                AlarmLatencyLow = new clsAlarm(clsAlarm.enumAlarmType.Height,$"WARNING: {Name} latency poor.",this);
+                ErrorMsg="LATENCY POOR";
+                Program.AlarmManager.New(AlarmLatencyLow);
+             }
+           }
+        }        
+        
+
+        clsAlarm AlarmRequestFail = null;
+        uint _requestFailCount;
+        public uint RequestFailCount
+        {
+           get
+           {
+             return _requestFailCount;
+           }
+           set
+           {
+             _requestFailCount = value;
+             if (_requestFailCount==0 && AlarmRequestFail!=null)
+             {
+                Program.AlarmManager.Clear(AlarmRequestFail,$"CLEARED: {Name} now responding.");
+                AlarmRequestFail = null;
+                ErrorMsg="";
+             }
+             else if (AlarmRequestFail ==null && _requestFailCount == 3)
+             {
+                AlarmRequestFail = new clsAlarm(clsAlarm.enumAlarmType.NoResponse,$"WARNING: {Name} not responding.",this);
+                ErrorMsg="NOT RESPONDING";
+                Program.AlarmManager.New(AlarmRequestFail);
+             }
+           }
+        }
+        
+
+        
+        public async Task GetHeightAsync()
+        {
+        
+            try{
+                if (HeightTask!=null && HeightTask.Status == TaskStatus.Running) return;
+        
+                HeightTask = Task.Run(() => {GetHeight();});
+            } catch (Exception ex)
+            {
+             //   SetError("GetHeight Task failed");
+            }
+             
+        
+        }
+        
+        private void GetHeight()
+        {
+            try {
+                
+                var client = new RestClient($"http://{Host}:8088");
+                        
+                client.Timeout = 2000;                    
+                        
+                var request = new RestRequest("v2", Method.POST);
+                request.AddHeader("Content-type", "application/json");
+                request.AddHeader("header", "value");
+                request.AddJsonBody(
+                    new { jsonrpc = "2.0", id = 0, method = "heights" }
+                );
+                  
+                // execute the request
+                var sw = Stopwatch.StartNew();
+                IRestResponse response = client.Execute(request);
+                sw.Stop();
+                
+                if(response.ResponseStatus == ResponseStatus.Completed)
+                {
+                    
+                    
+                   var content = response.Content; // raw content as string
+                   
+                   var pos1 = 0;
+                   var pos2= 0;
+                   
+                   
+                    if (!string.IsNullOrEmpty(content))
+                    {
+                    
+                        pos1 = content.IndexOf("leaderheight\":");
+                        pos1+=14;
+                        pos2 = content.IndexOf(",",pos1);
+                    
+                        uint msgheight=0;
+                        if (UInt32.TryParse(content.Substring(pos1,pos2-pos1),out msgheight))
+                        {
+                           LeaderHeight = msgheight;
+                        }
+                        else
+                        {
+                            ErrorMsg="Invalid data";
+                        }
+                        
+                    }
+                    else
+                    {
+                        ErrorMsg="Empty data";
+                    }
+                    LatencyList.Add((int)sw.ElapsedMilliseconds);
+                    PacketLoss.Add(0);
+                    RequestFailCount = 0;
+                    
+               } else if(response.ResponseStatus == ResponseStatus.Error || response.ResponseStatus == ResponseStatus.TimedOut)
+               {
+                PacketLoss.Add(100);
+                ErrorMsg=response.ErrorMessage;
+                
+                RequestFailCount++;                
+               }
+               else if(response.ResponseStatus == ResponseStatus.None)
+               {
+                 ErrorMsg="Empty data";
+               }
+                              
+               
+               Console.WriteLine(ToString());
+            
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine("ex.Message");
+                RequestFailCount++;
+            }
+
+        }
+        
+
+        
+        
+        async public void PingHostAsync()
+        {
+        
+            try
+            {
+                var pingTask = Task.Run(() =>
+                {
+            
+                    Uri myUri = new Uri(Host);
+               
+                
+                    clsRollingAverage pingLatency = new clsRollingAverage(10);
+                    clsRollingAverage pingPacketLoss = new clsRollingAverage(10);
+        
+                    
+                    try
+                    {
+                        using (var pinger = new Ping())
+                        {
+                            for (var f=0;f<10;f++)
+                            {
+                                PingReply reply = pinger.Send(myUri.Host,2000);
+                                
+                                if (reply.Status == IPStatus.Success)
+                                {
+                                    pingPacketLoss.Add((int)reply.RoundtripTime);
+                                    pingPacketLoss.Add(0);
+                                    Program.SendAlert($"Ping {myUri.Host} {reply.RoundtripTime:0} ms  {pingPacketLoss.CurrentAverage:0.0}%");
+                                }
+                                else
+                                {
+                                    pingPacketLoss.Add(100);
+                                    Program.SendAlert($"Ping {myUri.Host} {reply.Status} ms  {pingPacketLoss.CurrentAverage:0.0}%");
+                                }
+                            }
+                        }
+                    }
+                    catch (PingException ex)
+                    {
+                        Program.SendAlert($"Ping error {myUri.Host} {ex.Message}");
+                    }
+                });
+            }
+            catch (Exception ex)
+            {
+                Program.SendAlert($"Ping error {Name} {ex.Message}");
+            }
+        }
+        
+        public void Update(clsNode node)
+        {
+            if (Name != node.Name) throw new Exception("index name does not match");
+        
+            Group = node.Group;
+            Host = node.Host;
+            Monitor = node.Monitor;
+        
+        }
+        
+        public void PostPopulate()
+        {
+            if (!Program.NodeGroupList.TryGetValue(Group,out NodeGroup))
+            {
+                Console.WriteLine("Node Group Not Found!");
+            }
+            ErrorMsg = Monitor ? "" : "MONITOR OFF";
+            
+        }
+        
+        
+        public new String ToString()
+        {
+            return $"{Name}\t{Host}\t{LeaderHeight}\t{LatencyList.CurrentAverage.ToString().PadLeft(3)} ms ({(100-PacketLoss.CurrentAverage):0.#}%) {ErrorMsg}";
+        }
+        
+        
+    }
+}

+ 121 - 0
TFA-Bot/DataClasses/clsNodeGroup.cs

@@ -0,0 +1,121 @@
+using System;
+using System.Linq;
+namespace TFABot
+{
+    public class clsNodeGroup : ISpreadsheet<clsNodeGroup>
+    {
+        public clsNodeGroup()
+        {
+        }
+           
+        public clsNetwork Network {get; private set;}
+
+        [ASheetColumnHeader(true,"group")]
+        public String Name {get;set;}
+
+        [ASheetColumnHeader("network")]
+        public String NetworkString
+        {
+            get
+            {
+                return Network?.Name??"Not Set";
+            }
+            set
+            {
+                clsNetwork network;
+                if (Program.NetworkList.TryGetValue(value,out network))
+                {
+                    Network = network;
+                }
+                else
+                {
+                  new Exception("Network Name not found.");
+                }
+            }
+        }
+
+        [ASheetColumnHeader("ping")]
+        public String Ping {get;set;}
+
+        [ASheetColumnHeader("height")]
+        public String Height {get;set;}
+
+        [ASheetColumnHeader("latency")]
+        public String Latency {get;set;}
+
+        [ASheetColumnHeader("stall")]
+        public String Stall {get;set;}
+
+        public void Monitor()
+        {
+            foreach (var node in Program.NodesList.Values.Where(x=>x.Group == this.Name && x.Monitor))
+            {
+                
+                node.GetHeightAsync(); //get node status. The results we test on the next loop.
+                
+                //Check the height, against heighest known height
+                if (node.LeaderHeight > Network.TopHeight) //New highest LearderHeight.
+                {
+                
+                    if (Network.TopHeight > 0 && (node.LeaderHeight - Network.TopHeight > 10))
+                    {
+                        //Suspect wrong network setting
+                        if (Network.NetworkAlarm==null)
+                        {
+                            Network.NetworkAlarm = new clsAlarm(clsAlarm.enumAlarmType.Error,$"WARNING: {node.Name} height too high!  Wrong Bot setting?",Network);
+                            Program.AlarmManager.New(Network.NetworkAlarm);
+                        }
+                    }
+                    else
+                    {
+                        Network.SetTopHeight(node.LeaderHeight);
+                        node.HeightLowCount=0;
+                    }
+                }
+                else if (node.LeaderHeight < Network.TopHeight && node.RequestFailCount==0)  //Node height too low.
+                {
+                    if (Network.TopHeight - node.LeaderHeight > 10) //If by a large amount, maybe Wrong setting or syncing?
+                    {
+                        if (Network.NetworkAlarm==null)
+                        {
+                            Network.NetworkAlarm = new clsAlarm(clsAlarm.enumAlarmType.Error,$"WARNING: {node.Name} height low for network.  Wrong Bot setting or syncing?",Network);
+                            Program.AlarmManager.New(Network.NetworkAlarm);
+                        }
+                    }
+                    else
+                    {
+                        node.HeightLowCount++;
+                    }
+                }
+                else  //All good
+                {   
+                    node.HeightLowCount=0;
+                    if (Network.NetworkAlarm!=null)
+                    {
+                        Program.AlarmManager.Clear(Network.NetworkAlarm,$"{Network.Name} clear.");
+                        Network.NetworkAlarm = null;
+                    }
+                }
+                                    
+                //Check latency
+                if (node.Latency > node.LatencyLowest * 3 && node.LatencyLowest>50) node.LatencyLowCount ++;
+            }
+        }
+       
+         public void Update(clsNodeGroup group)
+        {
+            if (Name != group.Name) throw new Exception("index name does not match");
+            
+            Ping = group.Ping;
+            Height = group.Height;
+            Latency = group.Latency;
+            Stall = group.Stall;
+            Network = group.Network;
+        }
+
+        public void PostPopulate()
+        {
+        
+        }
+    }
+}

+ 34 - 0
TFA-Bot/DataClasses/clsNotificationPolicy.cs

@@ -0,0 +1,34 @@
+using System;
+namespace TFABot
+{
+    public class clsNotificationPolicy : ISpreadsheet<clsNotificationPolicy>
+    {
+        //Name  Discord Call
+        
+        public clsNotificationPolicy()
+        {
+            Call = -1;  //Default
+            Discord = -1; //Default
+        }
+        
+        [ASheetColumnHeader(true,"name")]
+        public String Name {get;set;}
+        
+        [ASheetColumnHeader("discord")]
+        public int Discord {get;set;}
+
+        [ASheetColumnHeader("call")]
+        public int Call {get;set;}
+
+        public void Update(clsNotificationPolicy node)
+        {
+            if (Name != node.Name) throw new Exception("index name does not match");
+            Discord = node.Discord;
+            Call = node.Call;
+        }
+                public void PostPopulate()
+        {
+        
+        }
+    }
+}

+ 31 - 0
TFA-Bot/DataClasses/clsSetting.cs

@@ -0,0 +1,31 @@
+using System;
+namespace TFABot
+{
+    public class clsSetting : ISpreadsheet<clsSetting>
+    {
+        public clsSetting()
+        {
+        
+        }
+        
+        [ASheetColumnHeader(true,"setting")]
+        public string Key {get;set;}
+        
+        [ASheetColumnHeader("value")]
+        public string Value {get;set;}
+        
+        
+        public void Update(clsSetting setting)
+        {
+            if (Key != setting.Key) throw new Exception("index name does not match");
+            
+            Key = setting.Key;
+            Value = setting.Value;
+        }
+        
+        public void PostPopulate()
+        {
+        
+        }
+    }
+}

+ 70 - 0
TFA-Bot/DataClasses/clsUser.cs

@@ -0,0 +1,70 @@
+using System;
+using System.Collections.ObjectModel;
+
+namespace TFABot
+{
+    public class clsUser : ISpreadsheet<clsUser>
+    {
+        public clsUser()
+        {
+        }
+
+        [ASheetColumnHeader(true,"discord")]
+        public String DiscordName {get;set;}
+        [ASheetColumnHeader("name")]
+        public String Name {get;set;}
+        [ASheetColumnHeader("tel")]
+        public String Tel {get;set;}
+        [ASheetColumnHeader("mail")]
+        public String email {get;set;}
+        [ASheetColumnHeader("zone")]
+        public String TimeZone {get;set;}
+        [ASheetColumnHeader("tel")]
+        public int Weight {get;set;}
+        [ASheetColumnHeader("time from","timefrom")]
+        public TimeSpan TimeFrom {get;set;}
+        [ASheetColumnHeader("time to","timeto")]
+        public TimeSpan TimeTo {get;set;}
+    
+        
+        public void Update(clsUser user)
+        {
+            if (DiscordName != user.DiscordName) throw new Exception("index name does not match");
+            
+            Tel = user.Tel;
+            Name = user.Name;
+            email = user.email;
+            TimeZone = user.TimeZone;
+            Weight = user.Weight;
+            TimeFrom = user.TimeFrom;
+            TimeTo = user.TimeTo;
+        }
+        
+        public void PostPopulate()
+        {
+            try
+            {
+                GetUserTime();  //Test to see if we can get the time.
+            }
+            catch (Exception ex)
+            {
+            
+                Console.WriteLine($"{Name} has invalid Timezone {ex.Message}");
+            }
+         
+        }
+        
+        public DateTime GetUserTime()
+        {
+            return DateTime.UtcNow.ToAbvTimeZone(TimeZone);
+        }
+        
+        public bool OnDuty
+        {
+            get
+            {
+                return GetUserTime().TimeBetween(TimeFrom,TimeTo);
+            }
+        }
+    }
+}

+ 20 - 0
TFA-Bot/DiscordBot/Commands/IBotCommand.cs

@@ -0,0 +1,20 @@
+using System;
+using System.Text.RegularExpressions;
+using DSharpPlus.EventArgs;
+
+namespace TFABot.DiscordBot.Commands
+{
+    
+    public interface IBotCommand
+    {
+        String[] MatchCommand {get;}
+        String[] MatchSubstring {get;}
+        Regex[] MatchRegex {get;}
+
+    
+        void Run(MessageCreateEventArgs e);
+        
+        String HelpString {get;}
+        
+    }
+}

+ 64 - 0
TFA-Bot/DiscordBot/Commands/clsAlarm.cs

@@ -0,0 +1,64 @@
+using System;
+using System.Text.RegularExpressions;
+using DSharpPlus.EventArgs;
+using static TFABot.Program;
+
+namespace TFABot.DiscordBot.Commands
+{
+    public class clsAlarm : IBotCommand
+    {
+    
+        public String[] MatchCommand {get; private set;}
+        public String[] MatchSubstring {get; private set;}
+        public Regex[] MatchRegex {get; private set;}
+        
+        public clsAlarm()
+        {
+            MatchCommand = new []{"alarm"};
+        }
+        
+        public void Run(MessageCreateEventArgs e)
+        {
+        
+            var lower = e.Message.Content.ToLower();
+        
+            if (lower.Contains("off"))
+            {
+                Program.AlarmState = EnumAlarmState.Off;
+            }
+            else if (lower.Contains("on"))
+            {
+                Program.AlarmState = EnumAlarmState.On;
+            }
+            else if (lower.Contains("silent"))
+            {
+                Program.AlarmState = EnumAlarmState.On;
+            }
+            else if (lower.Contains("list"))
+            {
+                e.Channel.SendMessageAsync(Program.AlarmManager.ToString());
+            }
+            else
+            {
+                e.Channel.SendMessageAsync(clsCommands.Instance.GetHelpString(this));
+            }
+                
+            e.Channel.SendMessageAsync($"Alarm State: {AlarmState.ToString()}");
+        }
+        
+        public String HelpString
+        {
+            get
+            {
+                return 
+@"alarm\tGet state.
+alarm on\tActive.
+alarm off\tNo Alarms.
+alarm silent\tDiscord warnings only.
+alarm list\tList active alarms.";
+
+            }
+        }
+        
+    }
+}

+ 72 - 0
TFA-Bot/DiscordBot/Commands/clsBotControl.cs

@@ -0,0 +1,72 @@
+using System;
+using System.Text.RegularExpressions;
+using DSharpPlus.EventArgs;
+
+namespace TFABot.DiscordBot.Commands
+{
+    public class clsBotControl : IBotCommand
+    {
+    
+        public String[] MatchCommand {get; private set;}
+        public String[] MatchSubstring {get; private set;}
+        public Regex[] MatchRegex {get; private set;}
+        
+        public clsBotControl()
+        {
+            MatchCommand = new []{"bot"};
+        }
+        
+        public void Run(MessageCreateEventArgs e)
+        {
+            var tolower = e.Message.Content.ToLower();
+            var commands = tolower.Split(new []{' '},StringSplitOptions.RemoveEmptyEntries);
+            
+            if (commands.Length>1)
+            {
+        
+                switch (commands[1])
+                {
+                    case "reload":
+                        e.Channel.SendMessageAsync(TFABot.Program.Spreadsheet.LoadSettings());
+                        break;
+                    case "restart":
+                        Program.SetRunState(Program.enumRunState.Restart);
+                        break;
+                    case "update":
+                        Program.SetRunState(Program.enumRunState.Update);
+                        break;
+                    case "exit":
+                        Program.SetRunState(Program.enumRunState.Stop);
+                        break;                     
+                   case "previous":
+                        Program.SetRunState(Program.enumRunState.PreviousVersion);
+                        break;
+                   default:
+                        e.Channel.SendMessageAsync("unknown command");
+                        break;
+                }
+            }
+            else
+            {
+                e.Channel.SendMessageAsync(clsCommands.Instance.GetHelpString());
+            }
+
+        }
+        public String HelpString
+        {
+            get
+            {
+                return 
+@"bot         
+bot reload\tReload spreadsheet (app settings require a restart).
+bot update\tUpdate to latest bot version.
+bot restart\tRestart bot.
+bot previous\tSwitch back to previous verion (if available).
+bot exit\tStop bot.";
+
+            }
+        }        
+        
+        
+    }
+}

+ 32 - 0
TFA-Bot/DiscordBot/Commands/clsCall.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Text.RegularExpressions;
+using DSharpPlus.EventArgs;
+
+namespace TFABot.DiscordBot.Commands
+{
+    public class clsCall : IBotCommand
+    {
+    
+        public String[] MatchCommand {get; private set;}
+        public String[] MatchSubstring {get; private set;}
+        public Regex[] MatchRegex {get; private set;}
+        
+        public clsCall()
+        {
+            MatchCommand = new []{"call"};
+        }
+        
+        public void Run(MessageCreateEventArgs e)
+        {
+            clsCaller.call(e);
+        }
+        
+        public String HelpString
+        {
+            get
+            {
+                return @"call <user>";
+            }        
+        }
+    }
+}

+ 32 - 0
TFA-Bot/DiscordBot/Commands/clsFembot.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Text.RegularExpressions;
+using DSharpPlus.EventArgs;
+
+namespace TFABot.DiscordBot.Commands
+{
+    public class clsFembot : IBotCommand
+    {
+    
+        public String[] MatchCommand {get; private set;}
+        public String[] MatchSubstring {get; private set;}
+        public Regex[] MatchRegex {get; private set;}
+        
+        public clsFembot()
+        {
+            MatchSubstring = new []{"fembot"};
+        }
+        
+        public void Run(MessageCreateEventArgs e)
+        {
+            e.Channel.SendMessageAsync("*ANGRY BOT :rage: ");
+        }
+        
+        public String HelpString
+        {
+            get
+            {
+                return null;
+            }
+        }        
+    }
+}

+ 32 - 0
TFA-Bot/DiscordBot/Commands/clsGoodAfternoon.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Text.RegularExpressions;
+using DSharpPlus.EventArgs;
+
+namespace TFABot.DiscordBot.Commands
+{
+    public class clsGoodAfternoon : IBotCommand
+    {
+    
+        public String[] MatchCommand {get; private set;}
+        public String[] MatchSubstring {get; private set;}
+        public Regex[] MatchRegex {get; private set;}
+        
+        public clsGoodAfternoon()
+        {
+            MatchSubstring = new []{"afternoon"};
+        }
+        
+        public void Run(MessageCreateEventArgs e)
+        {
+            e.Channel.SendMessageAsync("Good Afternoon! :sun_with_face:");
+        }
+        
+        public String HelpString
+        {
+            get
+            {
+                return null;
+            }
+        }
+    }
+}

+ 33 - 0
TFA-Bot/DiscordBot/Commands/clsGoodnight.cs

@@ -0,0 +1,33 @@
+using System;
+using System.Text.RegularExpressions;
+using DSharpPlus.EventArgs;
+
+namespace TFABot.DiscordBot.Commands
+{
+    public class clsGoodnight : IBotCommand
+    {
+    
+        public String[] MatchCommand {get; private set;}
+        public String[] MatchSubstring {get; private set;}
+        public Regex[] MatchRegex {get; private set;}
+        
+        public clsGoodnight()
+        {
+            MatchCommand = new [] {"night"};
+            MatchSubstring = new []{"Night!","goodnight","good night"};
+        }
+        
+        public void Run(MessageCreateEventArgs e)
+        {
+            e.Channel.SendMessageAsync("Goodnight! :sleeping:");
+        }
+
+        public String HelpString
+        {
+            get
+            {
+                return null;
+            }
+        }
+    }
+}

+ 33 - 0
TFA-Bot/DiscordBot/Commands/clsHelp.cs

@@ -0,0 +1,33 @@
+using System;
+using System.Text;
+using System.Text.RegularExpressions;
+using DSharpPlus.EventArgs;
+
+namespace TFABot.DiscordBot.Commands
+{
+    public class clsHelp : IBotCommand
+    {
+    
+        public String[] MatchCommand {get; private set;}
+        public String[] MatchSubstring {get; private set;}
+        public Regex[] MatchRegex {get; private set;}
+        
+        public clsHelp()
+        {
+            MatchCommand = new []{"help"};
+        }
+        
+        public void Run(MessageCreateEventArgs e)
+        {
+              e.Channel.SendMessageAsync(clsCommands.Instance.GetHelpString());
+        }
+        public String HelpString
+        {
+            get
+            {
+                return @"help";
+            }
+        }        
+        
+    }
+}

+ 32 - 0
TFA-Bot/DiscordBot/Commands/clsListNodes.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Text.RegularExpressions;
+using DSharpPlus.EventArgs;
+
+namespace TFABot.DiscordBot.Commands
+{
+    public class clsListNodes : IBotCommand
+    {
+    
+        public String[] MatchCommand {get; private set;}
+        public String[] MatchSubstring {get; private set;}
+        public Regex[] MatchRegex {get; private set;}
+        
+        public clsListNodes()
+        {
+            MatchCommand = new []{"nodes"};
+        }
+        
+        public void Run(MessageCreateEventArgs e)
+        {
+            e.Channel.SendMessageAsync(TFABot.Program.GetNodes());
+        }
+        
+        public String HelpString
+        {
+            get
+            {
+                return @"nodes\tList nodes.";
+            }        
+        }
+    }
+}

+ 32 - 0
TFA-Bot/DiscordBot/Commands/clsSkyNet.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Text.RegularExpressions;
+using DSharpPlus.EventArgs;
+
+namespace TFABot.DiscordBot.Commands
+{
+    public class clsSkynet : IBotCommand
+    {
+    
+        public String[] MatchCommand {get; private set;}
+        public String[] MatchSubstring {get; private set;}
+        public Regex[] MatchRegex {get; private set;}
+        
+        public clsSkynet()
+        {
+            MatchCommand = new []{"skynet"};
+        }
+        
+        public void Run(MessageCreateEventArgs e)
+        {
+            e.Channel.SendMessageAsync("Skynet Activated");
+        }
+        
+        public String HelpString
+        {
+            get
+            {
+                return null;
+            }        
+        }
+    }
+}

+ 43 - 0
TFA-Bot/DiscordBot/Commands/clsUsers.cs

@@ -0,0 +1,43 @@
+using System;
+using System.Text;
+using System.Text.RegularExpressions;
+using DSharpPlus.EventArgs;
+using TFABot.DiscordBot.Commands;
+
+namespace TFABot
+{
+    public class clsUsers : IBotCommand
+    {
+    
+        public String[] MatchCommand {get; private set;}
+        public String[] MatchSubstring {get; private set;}
+        public Regex[] MatchRegex {get; private set;}
+        
+        public clsUsers()
+        {
+            MatchCommand = new []{"users"};
+        }
+        
+        public void Run(MessageCreateEventArgs e)
+        {
+            var sb = new StringBuilder();
+            sb.Append("```");
+            foreach (var user in Program.UserList.Values)
+            {
+                sb.Append($"{user.Name.PadRight(15)} {user.GetUserTime():yy-MM-dd HH:mm}");
+                if (user.OnDuty) sb.Append($"   ON DUTY");
+                sb.AppendLine();
+            }
+            sb.Append("```");
+            e.Channel.SendMessageAsync(sb.ToString());
+        }
+        public String HelpString
+        {
+            get
+            {
+                return @"users\tList users.";
+            }
+        }        
+        
+    }
+}

+ 15 - 0
TFA-Bot/DiscordBot/DiscordBot.csproj

@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>netcoreapp2.0</TargetFramework>
+    <StartupObject></StartupObject>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="DSharpPlus" Version="3.2.3" />
+    <PackageReference Include="DSharpPlus.CommandsNext" Version="3.2.3" />
+    <PackageReference Include="DSharpPlus.Interactivity" Version="3.2.3" />
+  </ItemGroup>
+
+</Project>

+ 156 - 0
TFA-Bot/DiscordBot/clsBotClient.cs

@@ -0,0 +1,156 @@
+using DSharpPlus;
+using DSharpPlus.Interactivity;
+using DSharpPlus.CommandsNext;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.IO;
+using System.Threading.Tasks;
+using DSharpPlus.EventArgs;
+using System.Threading;
+using DSharpPlus.Net.WebSocket;
+using System.Linq;
+using System.Diagnostics;
+using TFABot;
+using static TFABot.Program;
+using TFABot.DiscordBot;
+
+namespace DiscordBot
+{
+    public class clsBotClient : IDisposable
+    {
+        private DiscordClient _client;
+        private CancellationTokenSource _cts;
+        private clsCommands Commands = new clsCommands();
+
+        public DateTime TimeConnected {get; private set;}
+        
+        public DSharpPlus.Entities.DiscordChannel Our_BotAlert = null;
+        public DSharpPlus.Entities.DiscordChannel Factom_BotAlert = null;
+        
+        
+        public static clsBotClient Instance = null;
+        
+
+        public clsBotClient(String Token)
+        {
+            Instance = this;
+                    
+            _client = new DiscordClient(new DiscordConfiguration()
+            {
+                AutoReconnect = true,
+                EnableCompression = true,
+                LogLevel = LogLevel.Debug,
+                Token = Token,
+                TokenType = TokenType.Bot,
+                UseInternalLogHandler = true
+            });
+
+
+            _client.SetWebSocketClient<WebSocketSharpClient>();
+            _client.Ready += OnReadyAsync;
+            _client.GuildAvailable += this.Client_GuildAvailable;
+            _client.ClientErrored += this.Client_ClientError;
+            _client.MessageCreated += MessageCreateEvent;
+          //  _client.mess
+            
+           
+           Commands.LoadCommandClasses();
+           
+        }
+
+        //Incoming Discord Message
+        private async Task MessageCreateEvent(MessageCreateEventArgs e)
+        {
+        
+            if (e.Author.IsBot) return;  //Reject our own messages
+            
+            if ( e.Channel == Factom_BotAlert)
+            {
+               Our_BotAlert.SendMessageAsync(e.Message.Content);
+               return;
+            }
+            
+            if (e.Channel.GuildId == 419201548372017163) return;  //Ignore Factom's Discord server
+            
+            Commands.DiscordMessage(e); //Forward message to commands lookup.
+            Console.Write(e.Message);
+        }
+
+
+        public async Task RunAsync()
+        {
+            await _client.ConnectAsync();
+            await WaitForCancellationAsync();
+        }
+
+        private async Task WaitForCancellationAsync()
+        {
+            while(!_cts.IsCancellationRequested)
+                await Task.Delay(500);
+        }
+
+        private async Task OnReadyAsync(ReadyEventArgs e)
+        {
+            await Task.Yield();
+            TimeConnected = DateTime.Now;
+        }
+
+
+        //Discord Server connected
+        private Task Client_GuildAvailable(GuildCreateEventArgs e)
+        {
+
+            if (e.Guild.Id == 419201548372017163)  //Factom's Discord server
+            {
+                Factom_BotAlert = e.Guild.Channels.FirstOrDefault(x=>x.Id == 443025488655417364);
+                Console.WriteLine($"Factom Alert channel: {Factom_BotAlert.Name}");
+            }
+            else
+            {
+                String alertChannelString;
+                if (Program.SettingsList.TryGetValue("Discord-AlertsChannel",out alertChannelString))
+                {
+                    alertChannelString = alertChannelString.ToLower().Replace("#","");
+                    var alertChannel = e.Guild.Channels.FirstOrDefault(x=>x.Name == alertChannelString);
+                    if (alertChannel!=null)
+                    {
+                       Our_BotAlert = alertChannel;
+                       Console.WriteLine($"Our Alert channel: {Our_BotAlert.Name}");
+                    }
+                }
+                else
+                {
+                    Console.WriteLine("Warning: Discord-AlertsChannel not set");
+                }
+            }
+
+            
+            // since this method is not async, let's return
+            // a completed task, so that no additional work
+            // is done
+            return Task.CompletedTask;
+        }
+
+        private Task Client_ClientError(ClientErrorEventArgs e)
+        {
+            // let's log the details of the error that just 
+            // occured in our client
+            e.Client.DebugLogger.LogMessage(LogLevel.Error, "ExampleBot", $"Exception occured: {e.Exception.GetType()}: {e.Exception.Message}", DateTime.Now);
+          //  Console.WriteLine();
+            
+            // since this method is not async, let's return
+            // a completed task, so that no additional work
+            // is done
+            return Task.CompletedTask;
+        }   
+
+        public void Dispose()
+        {
+            this._client.Dispose();
+       
+        }
+
+        
+    }
+}

+ 115 - 0
TFA-Bot/DiscordBot/clsCommands.cs

@@ -0,0 +1,115 @@
+using System;
+using System.Collections.Generic;
+using TFABot.DiscordBot.Commands;
+using System.Linq;
+using DSharpPlus.EventArgs;
+using System.Text.RegularExpressions;
+using System.Text;
+
+namespace TFABot.DiscordBot
+{
+    public class clsCommands
+    {
+        List <(string,IBotCommand)> MatchCommand = new List<(string, IBotCommand)>();
+        List <(string,IBotCommand)> MatchSubstring = new List<(string, IBotCommand)>();
+        List <(Regex,IBotCommand)> MatchRegex = new List<(Regex, IBotCommand)>();
+    
+        public static clsCommands Instance;
+    
+        public clsCommands()
+        {
+            Instance = this;
+        }
+        
+        
+        public void DiscordMessage(MessageCreateEventArgs e)
+        {
+            try
+            {
+                if (String.IsNullOrEmpty(e.Message.Content)) return;
+                
+                var lowMessage = e.Message.Content.ToLower();
+                var firstword = lowMessage.Split(new []{' '},2,StringSplitOptions.RemoveEmptyEntries);            
+                
+                foreach (var command in MatchCommand.Where(x =>firstword[0].StartsWith(x.Item1)))
+                {
+                    command.Item2.Run(e);
+                }
+                
+                foreach (var command in MatchSubstring.Where(x => lowMessage.Contains(x.Item1)))
+                {
+                    command.Item2.Run(e);
+                }
+                
+                foreach (var command in MatchRegex.Where(x =>x.Item1.IsMatch(e.Message.Content)))
+                {
+                    command.Item2.Run(e);
+                }
+            } catch (Exception ex)
+            {
+                Console.WriteLine($"DiscordMessage Error: {ex.Message}");
+            }
+        }
+        
+        
+        public void LoadCommandClasses()
+        {
+            var type = typeof(IBotCommand);
+            var types = AppDomain.CurrentDomain.GetAssemblies()
+                .SelectMany(s => s.GetTypes())
+                .Where(p => type.IsAssignableFrom(p)&& !p.IsInterface);
+                
+            foreach (var commandType in types)
+            {
+               IBotCommand command = (IBotCommand)Activator.CreateInstance(commandType);
+               
+               if (command.MatchCommand!=null)
+                    foreach (var match in command.MatchCommand) { MatchCommand.Add((match,command)); }
+               if (command.MatchSubstring!=null)
+                    foreach (var match in command.MatchSubstring) { MatchSubstring.Add((match,command)); }
+               if (command.MatchRegex != null)     
+                    foreach (var match in command.MatchRegex) { MatchRegex.Add((match,command)); }
+
+            }    
+        }
+
+
+        public String GetHelpString(IBotCommand command)
+        {
+            var sb = new StringBuilder();
+            sb.Append("```");
+            HelpString(sb,command);
+            sb.Append("```");
+            return sb.ToString();
+        }
+
+        public String GetHelpString()
+        {
+            var sb = new StringBuilder();
+            sb.Append("```");
+            sb.AppendLine($"The Factoid Authority Bot                             {(DateTime.UtcNow-Program.AppStarted).ToDHMDisplay() }");
+            sb.AppendLine("-----------------------------------------------------------------------");
+            
+            foreach (var command in MatchCommand.Where(x=>x.Item2.HelpString!=null).OrderBy(x=>x.Item1))
+            {
+                HelpString(sb,command.Item2);
+            }
+            sb.Append("```");
+            return sb.ToString();
+                
+        }
+        
+        void HelpString(StringBuilder sb, IBotCommand command)
+        {
+                foreach (var lines in command.HelpString.Split(new []{'\n'}))
+                {
+                    var tabSplit = lines.Split(new string[]{@"\t"},2,StringSplitOptions.RemoveEmptyEntries);
+                    sb.Append(tabSplit[0].PadRight(30));
+                    if (tabSplit.Length>1) sb.Append(tabSplit[1]);
+                    sb.AppendLine();
+                }
+        }
+        
+        
+    }
+}

+ 178 - 0
TFA-Bot/Program.cs

@@ -0,0 +1,178 @@
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using DiscordBot;
+using RestSharp;
+using System.Collections.Generic;
+using System.Threading;
+using System.Text;
+using System.Linq;
+
+namespace TFABot
+{
+    public class Program
+    {
+        static enumRunState RunState = enumRunState.Run;
+        public enum enumRunState : int
+        {
+            Stop=0,
+            Run=1,
+            Restart=2,
+            Update=3,
+            PreviousVersion=4,
+            Error = 100
+        }
+    
+        static uint AlarmOffWarningMinutes=30;
+    
+        static public Dictionary<string,string> SettingsList = new Dictionary<string, string>();
+        static public Dictionary<string,clsUser> UserList = new Dictionary<string, clsUser>();
+        static public Dictionary<string,clsNetwork> NetworkList = new Dictionary<string, clsNetwork>();
+        static public Dictionary<string,clsNode> NodesList = new Dictionary<string, clsNode>();
+        static public Dictionary<string,clsNodeGroup> NodeGroupList = new Dictionary<string, clsNodeGroup>();
+        static public Dictionary<string,clsNotificationPolicy> NotificationPolicyList = new Dictionary<string, clsNotificationPolicy>();
+        
+        static public DateTime AppStarted = DateTime.UtcNow;
+        static public clsAlarmManager AlarmManager = new clsAlarmManager();
+        
+        static public ManualResetEvent ApplicationHold = new ManualResetEvent(false);
+        
+        static public clsSpreadsheet Spreadsheet;
+        static public clsBotClient Bot;
+           
+        public enum EnumAlarmState { Off, On, Silent }
+        
+        static public DateTime AlarmOffTime;
+        
+        static public  EnumAlarmState _alarmState = EnumAlarmState.On;
+        static public  EnumAlarmState AlarmState 
+        {    get
+             {
+                return _alarmState;
+             }
+        
+             set
+             {
+                if (_alarmState != value)
+                {
+                    _alarmState = value;
+                    if (value == EnumAlarmState.Off) AlarmOffTime = DateTime.UtcNow;
+                }
+             }
+        }
+           
+        public static int Main(string[] args)
+        {
+            AlarmState = EnumAlarmState.On;
+            
+            var BotURL = Environment.GetEnvironmentVariable("BOTURL");
+            
+            if (String.IsNullOrEmpty(BotURL))
+            {
+                Console.WriteLine("'BOTURL' Google Spreadsheet URL missing.");
+                return (int)enumRunState.Error;
+            }
+            
+            Spreadsheet = new clsSpreadsheet(BotURL);
+            Spreadsheet.LoadSettings();
+        
+            String value;            
+            if (SettingsList.TryGetValue("AlarmOffWarningMinutes", out value)) uint.TryParse(value,out AlarmOffWarningMinutes);
+
+            String DiscordToken;            
+            if (!SettingsList.TryGetValue("Discord-Token", out DiscordToken))
+            {
+                Console.WriteLine("Discord-Token not found");
+                return (int)enumRunState.Error;
+            }
+            
+                        
+            using (Bot = new clsBotClient(DiscordToken))
+            {
+                Bot.RunAsync();
+            
+            
+                while (RunState == enumRunState.Run)
+                {
+                
+                    foreach (var group in NodeGroupList.Values)
+                    {
+                        group.Monitor();
+                    }
+                    
+                    foreach(var network in NetworkList.Values)
+                    {
+                        network.CheckStall();
+                    }
+                    
+                    if (AlarmState == EnumAlarmState.Off && (DateTime.UtcNow - AlarmOffTime).TotalMinutes > AlarmOffWarningMinutes) 
+                    {
+                        Bot.Our_BotAlert.SendMessageAsync($"Warning, the Alarm has been off {(DateTime.UtcNow - AlarmOffTime).TotalMinutes:0} minutes.  Forget to reset it?");
+                    }
+                    
+                   AlarmManager.Process();
+                    
+                   ApplicationHold.WaitOne(5000);
+                }
+
+            }
+            
+            
+            Console.WriteLine($"Exit Code: {RunState} {Enum.GetName(typeof(enumRunState), RunState)}");
+            return (int)RunState;
+            
+            
+        }
+        
+        static public void SendAlert(String message)
+        {
+            if (AlarmState == EnumAlarmState.On || AlarmState == EnumAlarmState.Silent)
+                    Bot.Our_BotAlert.SendMessageAsync(message);
+        }
+        
+        static public void CallAlert()
+        {
+            if (AlarmState == EnumAlarmState.On) clsCaller.CallAlertList();
+        }
+
+        static public void SetRunState(enumRunState runState)
+        {
+            RunState = runState;
+            ApplicationHold.Set();
+        }
+
+
+        static public String GetNodes()
+        {
+                var sb = new StringBuilder();
+                sb.Append("```");
+                
+                sb.AppendLine("Node        | Host                 |   Height   | Ave reply");
+                sb.AppendLine("------------|----------------------|------------|-----------");
+                
+                foreach (var group in NodeGroupList)
+                {
+                
+                    foreach (var node in NodesList.Values.Where(x=>x.Group == group.Key))
+                    {
+                        var nodetext = node.ToString().Split('\t');
+                        if (nodetext.Length==4)
+                        {
+                            sb.Append($"{nodetext[0].PadRight(12)}");  //Node
+                            sb.Append($"| {nodetext[1].Replace("http://","").PadRight(21)}"); //URL
+                            sb.Append($"| {nodetext[2].PadRight(11)}"); //Height
+                            sb.Append($"| {nodetext[3]}"); //Ave reply
+                        }else
+                        {
+                            sb.Append(node);
+                        }
+                        sb.AppendLine();
+                    }
+                }
+
+                sb.Append("```");
+                return sb.ToString();
+        }
+       
+    }
+}

+ 26 - 0
TFA-Bot/Properties/AssemblyInfo.cs

@@ -0,0 +1,26 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+// Information about this assembly is defined by the following attributes. 
+// Change them to the values specific to your project.
+
+[assembly: AssemblyTitle("TFA-Bot")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("")]
+[assembly: AssemblyCopyright("The Factoid Authority")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
+// The form "{Major}.{Minor}.*" will automatically update the build and revision,
+// and "{Major}.{Minor}.{Build}.*" will update just the revision.
+
+[assembly: AssemblyVersion("1.0.*")]
+
+// The following attributes are used to specify the signing key for the assembly, 
+// if desired. See the Mono documentation for more information about signing.
+
+//[assembly: AssemblyDelaySign(false)]
+//[assembly: AssemblyKeyFile("")]

+ 31 - 0
TFA-Bot/Spreadsheet/ASheetColumnHeader.cs

@@ -0,0 +1,31 @@
+using System;
+using System.Linq;
+namespace TFABot
+{
+    public class ASheetColumnHeader : Attribute
+    {
+        String[] HeaderMatch;
+        public bool IsIndex {get; private set;}
+        
+        public ASheetColumnHeader(params String[] headerMatch )
+        {
+            HeaderMatch = headerMatch;
+        }
+    
+        public ASheetColumnHeader(bool index, params String[] headerMatch )
+        {
+            HeaderMatch = headerMatch;
+            IsIndex = index;
+        }
+        
+        public bool IsMatch(String text)
+        {
+            text = text.ToLower();
+            foreach (var item in HeaderMatch)
+            {
+                if (text.Contains(item)) return true;
+            }
+            return false;
+        }
+    }
+}

+ 9 - 0
TFA-Bot/Spreadsheet/ISpreadsheet.cs

@@ -0,0 +1,9 @@
+using System;
+namespace TFABot
+{
+    public interface ISpreadsheet<T> where T : class
+    {
+      void Update(T item); 
+      void PostPopulate();
+    }
+}

+ 126 - 0
TFA-Bot/Spreadsheet/clsSpreadsheet.cs

@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+namespace TFABot
+{
+    public class clsSpreadsheet
+    {
+        clsSpreadsheetReader sheet;
+    
+        public clsSpreadsheet(string sheeturl)
+        {
+            sheet = new clsSpreadsheetReader(sheeturl);
+        }
+        
+        public String LoadSettings()
+        {
+        
+            var sb = new StringBuilder();
+            
+            ReadSheetSettings("Settings",sb);
+            ReadSheet<clsNetwork>("Networks",Program.NetworkList,sb);
+            ReadSheet<clsUser>("Users",Program.UserList,sb);
+            ReadSheet<clsNodeGroup>("NodeGroups",Program.NodeGroupList,sb);
+            ReadSheet<clsNotificationPolicy>("NotificationPolicy",Program.NotificationPolicyList,sb);
+            ReadSheet<clsNode>("Nodes",Program.NodesList,sb);
+            return sb.ToString();
+
+        }
+        
+        public void ReadSheetSettings(String SheetName, StringBuilder sb)
+        {
+        
+            try
+            {
+                sb.Append($"Loading {SheetName}.....  ");
+                var userSS = sheet.ReadSheet<clsSetting>(SheetName);
+    
+                foreach (var setting in userSS)
+                {
+                    if (Program.SettingsList.ContainsKey(setting.Key))
+                    {
+                        Program.SettingsList[setting.Key] = setting.Value;
+                    }
+                    else
+                    {
+                        Program.SettingsList.Add(setting.Key,setting.Value);
+                    }
+                }
+                sb.AppendLine($"{Program.SettingsList.Count} items, OK");
+            }
+            catch (Exception ex)
+            {
+               sb.AppendLine($"Error: {ex.Message}");
+               Console.WriteLine(ex.Message);
+            }
+        }
+
+        
+        
+        public void ReadSheet<T>(String SheetName, Dictionary<string,T> MainList, StringBuilder sb)  where T : class
+        {
+        
+            try
+            {
+                sb.Append($"Loading {SheetName}.....  ");
+                var userSS = sheet.ReadSheet<T>(SheetName);
+                Merge(MainList,userSS);
+                sb.AppendLine($"{MainList.Count} items, OK");
+            }
+            catch (Exception ex)
+            {
+               sb.AppendLine($"Error: {ex.Message}");
+               Console.WriteLine(ex.Message);
+            }
+        }
+
+
+        //Merge Spreadsheet data 
+        public void Merge<T>(Dictionary<string,T> mainList, List<T> SList) where T : class
+        {
+
+            //Use reflection to get index field
+            var type = typeof(T);
+            var indexProperty = type.GetProperties().Where(x=>x.GetCustomAttribute<ASheetColumnHeader>()!=null && x.GetCustomAttribute<ASheetColumnHeader>().IsIndex).ElementAt(0);
+
+            //Copy the main listm and sort
+            var mainCopyList = mainList.Values.OrderBy(x=>indexProperty.GetValue(x,null)).ToList();
+            //Sort Spreadsheet list on it's index property
+            SList.Sort((x, y) => ((string)indexProperty.GetValue(x,null)).CompareTo(indexProperty.GetValue(y,null)));
+            
+                        
+            int u=0;
+            int s=0;           
+            while (u < mainCopyList.Count || s < SList.Count)
+            {
+               //as both our lists are sorted, we can match them, to see if there are new/missing/matching entries.
+                var match = mainCopyList.Count==0 ? 1 : ((string)indexProperty.GetValue(mainCopyList[u],null)).CompareTo(((string)indexProperty.GetValue(SList[s],null)));
+                                        
+                switch (match)
+                {
+                    case 0:    //match
+                        ((ISpreadsheet<T>)mainCopyList[u]).Update(SList[s]);
+                        u++;
+                        s++;
+                        break;
+                
+                    case 1:  //new
+                        mainList.Add(((string)indexProperty.GetValue(SList[s],null)),SList[s]);  //new
+                        s++;
+                        break;
+
+                    case -1:  //delete
+                        mainList.Remove(((string)indexProperty.GetValue(mainCopyList[u],null)));
+                        u++;
+                        break;
+                }
+            }
+        }
+        
+        
+        
+    }
+}

+ 130 - 0
TFA-Bot/Spreadsheet/clsSpreadsheetReader.cs

@@ -0,0 +1,130 @@
+using System;
+using System.Text.RegularExpressions;
+using RestSharp;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+
+namespace TFABot
+{
+    public class clsSpreadsheetReader
+    {
+        String URL;
+        
+        
+        
+        public clsSpreadsheetReader(String url)
+        {
+                var reg = new Regex(@"^https.*(\w){40,}");
+                var match = reg.Match(url);
+                URL=match.Value;
+        }
+        
+        
+        //Generic sheet reader.
+        public List<T> ReadSheet<T>(String SheetName) where T : class
+        {
+            //Create a new list
+            List<T> list = new List<T>();
+        
+            //Get Spreadsheet data
+            var data = GetSpreadsheetData(SheetName);
+           
+            //Get the custom spreadsheet attributes in our target class.
+            var type = typeof(T);    
+            var properties = type.GetProperties().Where(x=>x.GetCustomAttribute(typeof(ASheetColumnHeader), false)!=null).ToList(); 
+
+            //Match the spreadsheet headers, with our target class type
+            var columns = new Dictionary<int, PropertyInfo>();
+            var indexColumn = -1;
+            //Read the header line (Start from line 0, until we find the index)
+            int r=0;
+            for (; r < data.Length;r++)
+            {
+                for (int c=0; c < data[r].Length;c++)
+                {
+                    var property = properties.FirstOrDefault(x=>x.GetCustomAttribute<ASheetColumnHeader>().IsMatch(data[r][c]));
+                    if (property!=null)
+                    {
+                        columns.Add(c,property);
+                        if (property.GetCustomAttribute<ASheetColumnHeader>().IsIndex) indexColumn = c;
+                    }
+                }
+                if (indexColumn !=-1) break;  //We got our index, so move to read the userdata
+            } 
+            
+            //Read the data, and populate new target classes.
+            for (r++; r < data.Length;r++)
+            {
+                if (data[r].Contains("<END>")) break;  //End of userdata.
+            
+                T dataClass = Activator.CreateInstance<T>();
+                list.Add(dataClass);
+                
+                for (int c=0; c < data[r].Length;c++)
+                {
+                    PropertyInfo pi;
+                    if (columns.TryGetValue(c,out pi))
+                    {
+                        if ( columns[c].PropertyType == typeof(TimeSpan) )
+                        {
+                            columns[c].SetValue(dataClass,TimeSpan.Parse(data[r][c]));
+                        }
+                        else
+                        {
+                            columns[c].SetValue(dataClass,System.Convert.ChangeType(data[r][c],columns[c].PropertyType));
+                        }
+                    }
+                }
+                
+                ((ISpreadsheet<T>)dataClass).PostPopulate();
+            }
+           
+           return list;
+            
+        }
+        
+        
+        String[][] GetSpreadsheetData(String SheetName)
+        {
+        
+            var client = new RestClient(URL);
+            var request = new RestRequest("gviz/tq");
+            
+            request.AddParameter("tqx","out:csv");
+            request.AddParameter("sheet",SheetName);
+            request.AddParameter("headers","true");
+            //request.AddParameter("tq","select A,B,C,D,E,F,G,H");
+
+            
+            IRestResponse response = client.Execute(request);
+            
+            if (response.IsSuccessful)       
+            {        
+                String[][] stdata = new string[0][];
+                
+                var lines = response.Content.Split(new [] {'\n'});
+                if (lines[0].Contains("invalid_query")) new Exception("Invalid query");
+                
+                for (int f=0;f<lines.Length;f++)
+                {
+                    int e; //Strip away last empty fields.
+                    for (e = lines[f].Length; e > 0; e-=3) if (!lines[f].Substring(e-3,3).Contains(",\"\"")) break;
+                    
+                    var columns = lines[f].Substring(0,e).Trim('\"').Split(new String[] {"\",\"",","},StringSplitOptions.None);
+                    if (f==0) stdata = new string[lines.Length][];
+                    stdata[f] = columns;
+                }
+                
+                return stdata;
+            }
+            else
+            {
+                new Exception("Get spreadsheet data failed");
+                return null;
+            }
+            
+        }
+        
+    }
+}

+ 111 - 0
TFA-Bot/TFA-Bot.csproj

@@ -0,0 +1,111 @@
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">x86</Platform>
+    <ProjectGuid>{25609AB4-A77A-4B47-AAB8-B2DB39464A3F}</ProjectGuid>
+    <OutputType>Exe</OutputType>
+    <RootNamespace>TFABot</RootNamespace>
+    <AssemblyName>TFA-Bot</AssemblyName>
+    <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug</OutputPath>
+    <DefineConstants>DEBUG;</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ExternalConsole>true</ExternalConsole>
+    <PlatformTarget>x86</PlatformTarget>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ExternalConsole>true</ExternalConsole>
+    <PlatformTarget>x86</PlatformTarget>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.ValueTuple">
+      <HintPath>..\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll</HintPath>
+    </Reference>
+    <Reference Include="mscorlib" />
+    <Reference Include="System.Web" />
+    <Reference Include="Newtonsoft.Json">
+      <HintPath>..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
+    </Reference>
+    <Reference Include="DSharpPlus">
+      <HintPath>..\packages\DSharpPlus.3.2.3\lib\net46\DSharpPlus.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Net.Http" />
+    <Reference Include="DSharpPlus.CommandsNext">
+      <HintPath>..\packages\DSharpPlus.CommandsNext.3.2.3\lib\net46\DSharpPlus.CommandsNext.dll</HintPath>
+    </Reference>
+    <Reference Include="DSharpPlus.Interactivity">
+      <HintPath>..\packages\DSharpPlus.Interactivity.3.2.3\lib\net46\DSharpPlus.Interactivity.dll</HintPath>
+    </Reference>
+    <Reference Include="websocket-sharp">
+      <HintPath>..\packages\WebSocketSharp-NonPreRelease.1.0.0\lib\net35\websocket-sharp.dll</HintPath>
+    </Reference>
+    <Reference Include="DSharpPlus.WebSocket.WebSocketSharp">
+      <HintPath>..\packages\DSharpPlus.WebSocket.WebSocketSharp.3.2.3\lib\net46\DSharpPlus.WebSocket.WebSocketSharp.dll</HintPath>
+    </Reference>
+    <Reference Include="RestSharp">
+      <HintPath>..\packages\RestSharp.106.2.2\lib\net452\RestSharp.dll</HintPath>
+    </Reference>
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Program.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="Utils\clsRollingAverage.cs" />
+    <Compile Include="Utils\clsExtentions.cs" />
+    <Compile Include="clsCaller.cs" />
+    <Compile Include="clsAlarm.cs" />
+    <Compile Include="Spreadsheet\clsSpreadsheet.cs" />
+    <Compile Include="Spreadsheet\ASheetColumnHeader.cs" />
+    <Compile Include="Spreadsheet\clsSpreadsheetReader.cs" />
+    <Compile Include="Spreadsheet\ISpreadsheet.cs" />
+    <Compile Include="DataClasses\clsNodeGroup.cs" />
+    <Compile Include="DataClasses\clsNotificationPolicy.cs" />
+    <Compile Include="DataClasses\clsSetting.cs" />
+    <Compile Include="DataClasses\clsUser.cs" />
+    <Compile Include="DataClasses\clsNode.cs" />
+    <Compile Include="DataClasses\clsNetwork.cs" />
+    <Compile Include="clsAlarmManager.cs" />
+    <Compile Include="clsExtenstions.cs" />
+    <Compile Include="DiscordBot\Commands\clsListNodes.cs" />
+    <Compile Include="DiscordBot\Commands\IBotCommand.cs" />
+    <Compile Include="DiscordBot\Commands\clsUsers.cs" />
+    <Compile Include="DiscordBot\Commands\clsGoodAfternoon.cs" />
+    <Compile Include="DiscordBot\Commands\clsGoodnight.cs" />
+    <Compile Include="DiscordBot\Commands\clsSkyNet.cs" />
+    <Compile Include="DiscordBot\Commands\clsFembot.cs" />
+    <Compile Include="DiscordBot\Commands\clsAlarm.cs" />
+    <Compile Include="DiscordBot\clsBotClient.cs" />
+    <Compile Include="DiscordBot\clsCommands.cs" />
+    <Compile Include="DiscordBot\Commands\clsCall.cs" />
+    <Compile Include="DiscordBot\Commands\clsBotControl.cs" />
+    <Compile Include="DiscordBot\Commands\clsHelp.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
+  <ItemGroup>
+    <Folder Include="DiscordBot\" />
+    <Folder Include="DiscordBot\Commands\" />
+    <Folder Include="Data\" />
+    <Folder Include="Utils\" />
+    <Folder Include="Spreadsheet\" />
+    <Folder Include="DataClasses\" />
+  </ItemGroup>
+  <ItemGroup>
+    <Content Include="Data\dialplan.xml">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <Import Project="..\packages\Mono.Posix.5.4.0.201\build\net45\Mono.Posix.targets" Condition="Exists('..\packages\Mono.Posix.5.4.0.201\build\net45\Mono.Posix.targets')" />
+</Project>

+ 10 - 0
TFA-Bot/Utils.cs

@@ -0,0 +1,10 @@
+sing System;
+namespace FactomMonitor
+{
+    public class Utils
+    {
+        public Utils()
+        {
+        }
+    }
+}

+ 10 - 0
TFA-Bot/Utils/clsExtentions.cs

@@ -0,0 +1,10 @@
+using System;
+using System.Globalization;
+
+namespace TFABot
+{
+    public static class Extenstions
+    {
+
+    }
+}

+ 91 - 0
TFA-Bot/Utils/clsRollingAverage.cs

@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+
+namespace TFABot
+{
+
+public sealed class clsRollingAverage
+{
+    public clsRollingAverage(int maxRememberedNumbers)
+    {
+        if (maxRememberedNumbers <= 0)
+        {
+            throw new ArgumentException("maxRememberedNumbersmust be greater than 0.", "maxRememberedNumbers");
+        }
+
+        this.counts = new Queue<int>(maxRememberedNumbers);
+        this.maxSize = maxRememberedNumbers;
+        this.currentTotal = 0L;
+        this.padLock = new Object();
+    }
+
+    private const int defaultMaxRememberedNumbers = 10;
+
+    private readonly Queue<int> counts;
+    private readonly int maxSize;
+
+    private long currentTotal;
+
+    private object padLock;
+
+    public void Add(int value)
+    {
+        lock (this.padLock)
+        {
+        
+            if (value < LowestEver) LowestEver = value;
+            if (value > HighestEver) HighestEver = value;
+        
+            if (this.counts.Count == this.maxSize)
+            {
+                this.currentTotal -= (long)this.counts.Dequeue();
+            }
+
+            this.counts.Enqueue(value);
+
+            this.currentTotal += (long)value;
+        }
+    }
+
+
+    public int LowestEver {get; private set;}
+    public int HighestEver {get; private set;}
+
+    public int CurrentAverage
+    {
+        get
+        {
+            long lenCounts;
+            long observedTotal;
+
+            lock (this.padLock)
+            {
+                lenCounts = (long)this.counts.Count;
+                observedTotal = this.currentTotal;
+            }
+
+            if (lenCounts == 0)
+            {
+                return LowestEver = 0;
+            }
+            else if (lenCounts == 1)
+            {
+                return (int)observedTotal;
+            }
+            else
+            {
+                return (int)(observedTotal / lenCounts);
+            }
+        }
+    }
+
+    public void Clear()
+    {
+        lock (this.padLock)
+        {
+            this.currentTotal = 0L;
+            this.counts.Clear();
+        }
+    }
+}
+}

+ 118 - 0
TFA-Bot/clsAlarm.cs

@@ -0,0 +1,118 @@
+using System;
+using System.Linq;
+using DiscordBot;
+
+namespace TFABot
+{
+    public class clsAlarm
+    {
+        DateTime Opened = DateTime.UtcNow;
+        DateTime? TimeDiscord;
+        DateTime? TimeCall;
+        DateTime? Timeout;
+        
+        public clsNotificationPolicy notificationPolicy;
+        
+        public enumAlarmType AlarmType {get; private set;}
+        public enum enumAlarmType
+        {
+            Error,
+            NoResponse,
+            Height,
+            Latency,
+            Network
+        };
+        
+        public clsNode Node {get; private set;}
+        public clsNetwork Network {get; private set;}
+        public String Message;
+               
+        //New alarm, from node
+        public clsAlarm(enumAlarmType alarmType, String message, clsNode node)
+        {
+            AlarmType = alarmType;
+            Message = message;
+            Node = node;
+ 
+                    
+            switch(alarmType)
+            {
+                case enumAlarmType.NoResponse:
+                    Program.NotificationPolicyList.TryGetValue(Node.NodeGroup.Ping,out notificationPolicy); break;
+                case enumAlarmType.Height:
+                    Program.NotificationPolicyList.TryGetValue(Node.NodeGroup.Height,out notificationPolicy); break;
+                case enumAlarmType.Latency:
+                    Program.NotificationPolicyList.TryGetValue(Node.NodeGroup.Latency,out notificationPolicy); break;
+//                case enumAlarmType.Error:
+                case enumAlarmType.Network:
+                    Program.NotificationPolicyList.TryGetValue(Node.NodeGroup.NetworkString,out notificationPolicy); break;
+            }
+            
+        }
+        
+        //New alarm from Network
+        public clsAlarm(enumAlarmType alarmType, String message, clsNetwork network)
+        {
+            AlarmType = alarmType;
+            Message = message;
+            Network = network;
+                       
+           Program.NotificationPolicyList.TryGetValue(network.StallNotification,out notificationPolicy);
+        }
+        
+
+        ////New alarm 
+        //public clsAlarm(enumAlarmType alarmType, String message)
+        //{
+            
+        //}
+        
+        public void Clear(string message = null)
+        {
+            
+            if (TimeDiscord.HasValue)
+            {
+                if (String.IsNullOrEmpty(message)) message = $"{AlarmType} alarm cleared";
+                clsBotClient.Instance.Our_BotAlert.SendMessageAsync(message);
+            }
+        }
+        
+        public void Process()
+        {
+            if (notificationPolicy!=null)
+            {            
+                if (Program.AlarmState == Program.EnumAlarmState.On)
+                {
+                    if (!TimeCall.HasValue && notificationPolicy.Call>=0 && DateTime.UtcNow > Opened.AddSeconds(notificationPolicy.Call))
+                    {
+                        TimeCall = DateTime.UtcNow;
+                        clsCaller.CallAlertList();
+                    }
+                }
+    
+                if (Program.AlarmState != Program.EnumAlarmState.Off)
+                {
+                    if (!TimeDiscord.HasValue && notificationPolicy.Discord>=0 && DateTime.UtcNow > Opened.AddSeconds(notificationPolicy.Discord))
+                    {
+                        TimeDiscord = DateTime.UtcNow;
+                        clsBotClient.Instance.Our_BotAlert.SendMessageAsync(Message);
+                    }
+                }
+            }
+            else
+            {
+                if (!TimeDiscord.HasValue)
+                {
+                    Program.Bot.Our_BotAlert.SendMessageAsync(Message);
+                    TimeDiscord = DateTime.UtcNow;
+                }
+            }
+        }
+        
+        public new String ToString()
+        {
+            return $"{AlarmType.ToString().PadRight(15)} {Node?.Name.PadRight(15) ?? Network?.Name.PadRight(20) ?? ""} {Opened} {Message}";
+        }
+        
+    }
+}

+ 66 - 0
TFA-Bot/clsAlarmManager.cs

@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace TFABot
+{
+    public class clsAlarmManager
+    {
+       
+        List<clsAlarm> AlarmList = new List<clsAlarm>();
+    
+        public clsAlarmManager()
+        {
+        }
+        
+        public void New(clsAlarm Alarm)
+        {
+           Alarm.Process();
+           AlarmList.Add(Alarm);
+        }
+        
+        public void Clear(clsAlarm Alarm, String message = null)
+        {
+            if (AlarmList.Contains(Alarm))
+            {
+                Alarm.Clear(message);
+                AlarmList.Remove(Alarm);
+            }
+            
+        }
+        
+        public void Process()
+        {
+            //Process all Alarms, removing expired alarms.
+          //  AlarmList.RemoveAll(x=>!x.Process());
+          
+          foreach (var alarm in AlarmList)
+          {
+            alarm.Process();
+          }
+          
+        }
+        
+        public new String ToString()
+        {
+            var sb = new StringBuilder();
+            
+            if (AlarmList.Count==0)
+            {
+                sb.AppendLine("no alarms");
+            }
+            else
+            {
+                sb.Append("```");
+                foreach (var alarm in AlarmList)
+                {
+                    sb.AppendLine(alarm.ToString());
+                }
+                sb.Append("```");
+            }
+            return sb.ToString();
+        }
+
+    }
+}

+ 109 - 0
TFA-Bot/clsCaller.cs

@@ -0,0 +1,109 @@
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using DiscordBot;
+using DSharpPlus.EventArgs;
+using System.IO;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace TFABot
+{
+    static public class clsCaller
+    {
+        
+        static clsCaller()
+        {
+                
+        }
+        
+        static public void CallAlertList()
+        {
+            foreach (var user in Program.UserList.Values.Where(x=>x.OnDuty))
+            {
+                call(user.DiscordName);
+            }           
+        }
+        
+        
+        static public void call(String name, DSharpPlus.Entities.DiscordChannel ChBotAlert = null)
+        {
+            name = name.ToLower();
+            
+            clsUser user;
+            if (!Program.UserList.TryGetValue(name,out user))
+            {
+              user = Program.UserList.Values.FirstOrDefault(x=>x.DiscordName==name);
+            }
+            
+            if (user!=null) call(user.DiscordName,user.Tel,ChBotAlert);
+            
+        }
+        
+        static public void call(MessageCreateEventArgs e)
+        {
+            var toRing = e.Message.Content.ToLower();
+            call (toRing,e.Channel);
+        }
+
+        static public Task call(String Name, String Number, DSharpPlus.Entities.DiscordChannel ChBotAlert = null)
+        {
+
+			try
+			{
+
+				if (ChBotAlert == null) ChBotAlert = clsBotClient.Instance.Our_BotAlert;
+
+				String sipp = "/app/sipp/sipp";
+				String username = Program.SettingsList["SIP-Username"];
+                String password = Environment.GetEnvironmentVariable("SIP-PASSWORD") ?? Program.SettingsList["SIP-Password"];
+                
+				String host = Program.SettingsList["SIP-Host"];
+				String timeout = "30s";
+				String dialplanPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"dialplan.xml");
+
+				String perms = $"{host} -au {username} -ap {password} -l 1 -m 1 -sf {dialplanPath} -timeout {timeout} -s {Number.Replace(" ", "")}";
+				//sipp voiceless.aa.net.uk -au +443333402005 -ap tinydancer -l 1 -m 1 -sf /root/dialplan.xml -timeout 20s -s "+447973665024"
+				var tcs = new TaskCompletionSource<bool>();
+
+				var process = new Process
+				{
+					StartInfo = { FileName = sipp,
+							  Arguments = perms,
+							  UseShellExecute = false
+                           //   RedirectStandardOutput = true,
+                            //  RedirectStandardError = true
+                             },
+					EnableRaisingEvents = true
+
+				};
+
+				process.Exited += (sender, args) =>
+				{
+					ChBotAlert.SendMessageAsync($"{Name} Call Ended");
+					tcs.SetResult(true);
+					process.Dispose();
+				};
+
+				ChBotAlert.SendMessageAsync($"Calling {Name} {Number}");
+				process.Start();
+				//while (!process.StandardError.EndOfStream) {
+				//       e.Channel.SendMessageAsync(process.StandardError.ReadLine());    
+				//}
+
+				//while (!process.StandardOutput.EndOfStream) {
+				//       e.Channel.SendMessageAsync(process.StandardOutput.ReadLine());    
+				//}
+
+                
+				return tcs.Task;
+			}
+			catch (Exception ex)
+			{
+				Console.Write("Call error: " + ex.Message);
+				return null;
+			}
+        }
+      
+    }
+}

+ 52 - 0
TFA-Bot/clsExtenstions.cs

@@ -0,0 +1,52 @@
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace TFABot
+{
+    static public class clsExtenstions
+    {
+        static Regex UTCMatch = new Regex(@"(?<=UTC)\s{0,}[\+\-]\d*");
+               
+        static clsExtenstions()
+        {
+       
+        }
+        
+        
+        public static bool TimeBetween(this DateTime datetime, TimeSpan start, TimeSpan end)
+        {
+            // convert datetime to a TimeSpan
+            TimeSpan now = datetime.TimeOfDay;
+            // see if start comes before end
+            if (start < end) return start <= now && now <= end;
+            // start is after end, so do the inverse comparison
+            return !(end < now && now < start);
+        }
+        
+        
+        public static DateTime ToAbvTimeZone(this DateTime date,String abvTimeZone)
+        {
+                
+            abvTimeZone = abvTimeZone.ToUpper();
+            
+            if (abvTimeZone == "UTC") return TimeZoneInfo.ConvertTimeToUtc(date);
+                        
+            var match = UTCMatch.Match(abvTimeZone);
+            if (match.Success)
+            {
+                int offset = int.Parse(match.Value);
+                return DateTime.UtcNow.AddHours(offset);
+            }
+             
+            return  TimeZoneInfo.ConvertTimeBySystemTimeZoneId(date, abvTimeZone);
+        }
+    
+        public static string ToDHMDisplay(this TimeSpan ts)
+        {
+            return $"{ts.Days}d {ts.Hours}h {ts.Minutes}m";
+        }
+        
+    }
+}

+ 12 - 0
TFA-Bot/packages.config

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="DSharpPlus" version="3.2.3" targetFramework="net461" />
+  <package id="DSharpPlus.CommandsNext" version="3.2.3" targetFramework="net461" />
+  <package id="DSharpPlus.Interactivity" version="3.2.3" targetFramework="net461" />
+  <package id="DSharpPlus.WebSocket.WebSocketSharp" version="3.2.3" targetFramework="net461" />
+  <package id="Mono.Posix" version="5.4.0.201" targetFramework="net461" />
+  <package id="Newtonsoft.Json" version="11.0.2" targetFramework="net461" />
+  <package id="RestSharp" version="106.2.2" targetFramework="net461" />
+  <package id="System.ValueTuple" version="4.4.0" targetFramework="net461" />
+  <package id="WebSocketSharp-NonPreRelease" version="1.0.0" targetFramework="net461" />
+</packages>

+ 7 - 0
entrypoint.sh

@@ -0,0 +1,7 @@
+#!/bin/bash
+
+#msbuild -property:Configuration=Release
+
+
+cd /app
+mono Factom-Monitor.exe