From ad0a46175e7efbbd5bdf952aacc69d8a7750b05a Mon Sep 17 00:00:00 2001 From: TrueDoctor Date: Thu, 27 Sep 2018 23:46:43 +0200 Subject: implemented rudemetary groups --- DSACore/DSA_Game/Dsa.cs | 4 +- DSACore/DSA_Game/Save/Session.cs | 2 +- DSACore/Hubs/ChatHub.cs | 31 ++- DSACore/Properties/launchSettings.json | 2 +- DSACore/PropertiesDSACore-Audio-Sound.json | 7 + .../PropertiesDSACore-Auxiliary-CommandInfo.json | 101 +++++++ ...rtiesDSACore-DSA_Game-Characters-Character.json | 290 +++++++++++++++++++++ .../PropertiesNewtonsoft-Json-Linq-JProperty.json | 30 +++ DSACore/Startup.cs | 21 +- 9 files changed, 480 insertions(+), 8 deletions(-) create mode 100644 DSACore/PropertiesDSACore-Audio-Sound.json create mode 100644 DSACore/PropertiesDSACore-Auxiliary-CommandInfo.json create mode 100644 DSACore/PropertiesDSACore-DSA_Game-Characters-Character.json create mode 100644 DSACore/PropertiesNewtonsoft-Json-Linq-JProperty.json (limited to 'DSACore') diff --git a/DSACore/DSA_Game/Dsa.cs b/DSACore/DSA_Game/Dsa.cs index 7258e8c..94de194 100644 --- a/DSACore/DSA_Game/Dsa.cs +++ b/DSACore/DSA_Game/Dsa.cs @@ -13,7 +13,7 @@ namespace DSACore.DSA_Game public static class Dsa { - public const string rootPath = "DiscoBot\\DSACore\\"; + public const string rootPath = "";//"DiscoBot\\DSACore\\"; private static Session s_session; @@ -46,7 +46,7 @@ namespace DSACore.DSA_Game //new .Auxiliary.Calculator.StringSolver("1d100 - (1d200 + 1) * -50000").Solve(); /*Session = new Session();*/ // relation.Add("Papo", "Pump aus der Gosse"); - foreach (var filename in Directory.GetFiles("helden", "*.xml")) + foreach (var filename in Directory.GetFiles(rootPath + "helden", "*.xml")) { Chars.Add(new Character(filename)); (Chars.Last() as Character)?.Talente.Select(x => new Talent(x.Name, x.Probe, 0)) diff --git a/DSACore/DSA_Game/Save/Session.cs b/DSACore/DSA_Game/Save/Session.cs index d343920..b402656 100644 --- a/DSACore/DSA_Game/Save/Session.cs +++ b/DSACore/DSA_Game/Save/Session.cs @@ -9,7 +9,7 @@ namespace DSACore.DSA_Game.Save public class Session { - public static string DirectoryPath { get; set; } = @"sessions"; + public static string DirectoryPath { get; set; } = Dsa.rootPath + @"sessions"; public Dictionary Relation { get; set; } = new Dictionary(); // dictionary to match the char diff --git a/DSACore/Hubs/ChatHub.cs b/DSACore/Hubs/ChatHub.cs index 6af8c38..9cd7aed 100644 --- a/DSACore/Hubs/ChatHub.cs +++ b/DSACore/Hubs/ChatHub.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using DSACore.Models; using Microsoft.AspNetCore.SignalR; namespace DSACore.Hubs @@ -10,7 +11,35 @@ namespace DSACore.Hubs { public async Task SendMessage(string user, string message) { - await Clients.All.SendAsync("ReciveMessage", user, message); + string group = "default"; + var args = message.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(); + var ident = args.First().Replace("!", ""); + if(args.Count>0){args.RemoveAt(0);} + + await SendToGroup(group, user, Commands.CommandHandler.ExecuteCommand(new Command{CharId = 0,CmdIdentifier = ident, CmdTexts = args, Name = user})); } + + private Task SendToGroup(string group, string user, string message) + { + return Clients.Group(group).SendCoreAsync("ReceiveMessage", new object[] { user, message }); + } + + public async Task Login(string group, string password) + { + if (password == "valid") + { + await Groups.AddToGroupAsync(Context.ConnectionId, group); + } + + await SendToGroup(group, "", "Ein neuer Nutzer hat die Gruppe betreten"); + } + + public async Task Disconnect(string group, string user) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, group); + + await SendToGroup(group, user, "Connection lost"); + } + } } diff --git a/DSACore/Properties/launchSettings.json b/DSACore/Properties/launchSettings.json index 2da5fec..50d10a9 100644 --- a/DSACore/Properties/launchSettings.json +++ b/DSACore/Properties/launchSettings.json @@ -21,7 +21,7 @@ "commandName": "Project", "launchBrowser": true, "launchUrl": "api/commands", - "applicationUrl": "https://0.0.0.0:5001;http://0.0.0.0:5000", + "applicationUrl": "https://192.168.2.58:5001;http://192.168.2.58:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/DSACore/PropertiesDSACore-Audio-Sound.json b/DSACore/PropertiesDSACore-Audio-Sound.json new file mode 100644 index 0000000..87a0e6b --- /dev/null +++ b/DSACore/PropertiesDSACore-Audio-Sound.json @@ -0,0 +1,7 @@ +[ + { + "Name": "Test", + "Url": "http", + "Volume": 100 + } +] \ No newline at end of file diff --git a/DSACore/PropertiesDSACore-Auxiliary-CommandInfo.json b/DSACore/PropertiesDSACore-Auxiliary-CommandInfo.json new file mode 100644 index 0000000..b9941f2 --- /dev/null +++ b/DSACore/PropertiesDSACore-Auxiliary-CommandInfo.json @@ -0,0 +1,101 @@ +[ + { + "Name": "ich bin", + "Scope": "All", + "Brief": "Setzt den gespielten Charakter fest", + "Description": [ + "Mit \"!Ich bin\" kann der gespielte Charakter definiert, bzw. gewechselt werden.\n", + " Die Charaktere müssen als *.xml Dateien hinterlegt sein.\n\n", + " !ich Zeigt an welcher Charakter zur Zeit gespielt wird\n", + " !ich bin Zalibius Wechsel zum Helden Zalibius\n", + " !ich Rhoktar Orkische Variante von !ich bin.\n", + " Wechselt zu Rhoktar.\n\n", + " !list chars Zeigt die Liste verfügbarer Charaktere.\n", + " \n" + ] + }, + { + "Name": "List", + "Scope": "All", + "Brief": "Anzeige vonSpielrelevanten Listen", + "Description": [ + "Mit \"!list\" lassen sich spielrelevante Listen ausgeben. Die Angezeigte Liste wird nach einiger Zeit wieder gelöscht.\n", + "\n", + " !list chars Liste aller verfügbaren Helden (eingelesen per *.xml)\n", + " und NSCs.\n", + " (Mit \"!ich bin\" kann der Held ausgewählt werden.)\n", + " !list commands Liste aller verwendbaren Bot-Kommandos.\n", + " !list sounds Liste der Soundeffekte." + ] + }, + { + "Name": "Held", + "Scope": "All", + "Brief": "Anzeige von Heldenwerten", + "Description": [ + "Mit \"!Held\" lassen sich Heldenwerte ausgeben. Mehrere Werte können gleichzeitig angefordert werde. \"!Held LE Waffen\" liefert so z.B. Informationen zur Lebensenergie und den Kampfwerten.\n Bis auf wenige Ausnahmen wird die Angezeigte Liste nach einiger Zeit wieder gelöscht.\n", + "\n", + " !Held Zeigt den Heldenbrief an.\n", + " (Diese Liste wird nicht automatisch gelöscht)\n", + " !Held Eigenschaften Zeigt die Eigenschaften MU/KL/CH/IN/KK/GE/FF/KO.\n", + " !Held e Kurzform von \"!Held Eigenschaften\".\n", + " !Held LE Zeigt LE an.\n", + " !Held AE Zeigt AE an.\n", + " !Held stats Zeigt Eigenschaften und zusätzlich SO/LE/AE.\n", + " !Held Kampfwerte Zeigt AT/PA für aktivierte Waffentalente.\n", + " !Held Waffe/!list w Kurzformen von \"!Held Kampfwerte\".\n", + " !Held Vorteile Zeigt Vor- und Nachteile an.\n", + " !Held v Kurzform von \"!Held Vorteile\".\n", + " !Held Talente Zeigt die Liste aller aktivierten Talente, deren TaW,\n", + " sowie die Probe.\n", + " !Held t Kurzform von \"!Held Talente\".\n", + " !Held Zauber Zeigt die Liste aller aktivierten Zauber, deren ZaW,\n", + " sowie die Probe.\n", + " !Held z Kurzform von \"!Held Zauber\".\n" + ] + }, + { + "Name": "LE", + "Scope": "All", + "Brief": "Ändert dein Leben - im wahrsten Sinne des Wortes", + "Description": [ + "Mit !LE zeigt man die Lebensenergie an, ändert sie, oder setzt sie auf einen neuen Wert\n\n", + " !LE Zeigt Lebensenergie an\n", + " !LE 30 Setzt LE auf 30\n", + " !LE +5 Erhöht LE um 5 (bis zum Maximum)\n", + " !LE ++5 Erhöht LE um 5 (ignoriert Maximum)\n", + " !LE -5 Verringert LE um 5\n \n" + ] + }, + { + "Name": "AE", + "Scope": "All", + "Brief": "Ändert Astralenergie", + "Description": [ + "Mit !AE (oder !Asp) zeigt man die Astralenergie an, ändert sie, oder setzt sie auf einen neuen Wert\n\n", + " !AE Zeigt Astralenergie an\n", + " !AE 30 Setzt Asp auf 30\n", + " !AE +5 Erhöht Asp um 5 (bis zum Maximum)\n", + " !AE ++5 Erhöht Asp um 5 (ignoriert Maximum)\n", + " !AE -5 Verringert Asp um 5 (Minimum 0)\n" + ] + }, + { + "Name": "Gm", + "Scope": "Meister", + "Brief": "Kontrolliere andere Charaktere", + "Description": [ + "Mit !GM fürhrt man commands als eine andere Person aus.", + " !GM [charaktername] [command] [commandattribut] [mofifier]", + " Unterstützte [commands]'s:", + " !GM [name] LE", + " !GM [name] AE", + " !GM [name] Talent", + " !GM [name] Fernkampf", + " !GM [name] Eigenschaft", + " !GM [name] Zauber", + " !GM [name] Angriff", + " !GM [name] Parade" + ] + } +] \ No newline at end of file diff --git a/DSACore/PropertiesDSACore-DSA_Game-Characters-Character.json b/DSACore/PropertiesDSACore-DSA_Game-Characters-Character.json new file mode 100644 index 0000000..fd387f5 --- /dev/null +++ b/DSACore/PropertiesDSACore-DSA_Game-Characters-Character.json @@ -0,0 +1,290 @@ +[ + { + "Eigenschaften": {}, + "Talente": [], + "Zauber": [], + "Kampftalente": [], + "Vorteile": [], + "PropTable": { + "MU": "Mut", + "KL": "Klugheit", + "IN": "Intuition", + "CH": "Charisma", + "FF": "Fingerfertigkeit", + "GE": "Gewandtheit", + "KO": "Konstitution", + "KK": "Körperkraft" + }, + "Lebenspunkte_Basis": 30, + "Lebenspunkte_Aktuell": 30, + "Ausdauer_Basis": 0, + "Ausdauer_Aktuell": 0, + "Astralpunkte_Basis": 20, + "Astralpunkte_Aktuell": 20, + "Name": "Felis Exodus Schattenwald" + }, + { + "Eigenschaften": {}, + "Talente": [], + "Zauber": [], + "Kampftalente": [], + "Vorteile": [], + "PropTable": { + "MU": "Mut", + "KL": "Klugheit", + "IN": "Intuition", + "CH": "Charisma", + "FF": "Fingerfertigkeit", + "GE": "Gewandtheit", + "KO": "Konstitution", + "KK": "Körperkraft" + }, + "Lebenspunkte_Basis": 29, + "Lebenspunkte_Aktuell": 29, + "Ausdauer_Basis": 0, + "Ausdauer_Aktuell": 0, + "Astralpunkte_Basis": 0, + "Astralpunkte_Aktuell": 0, + "Name": "Gardist" + }, + { + "Eigenschaften": {}, + "Talente": [], + "Zauber": [], + "Kampftalente": [], + "Vorteile": [], + "PropTable": { + "MU": "Mut", + "KL": "Klugheit", + "IN": "Intuition", + "CH": "Charisma", + "FF": "Fingerfertigkeit", + "GE": "Gewandtheit", + "KO": "Konstitution", + "KK": "Körperkraft" + }, + "Lebenspunkte_Basis": 31, + "Lebenspunkte_Aktuell": 31, + "Ausdauer_Basis": 0, + "Ausdauer_Aktuell": 0, + "Astralpunkte_Basis": 0, + "Astralpunkte_Aktuell": 0, + "Name": "Hartmut Reiher" + }, + { + "Eigenschaften": {}, + "Talente": [], + "Zauber": [], + "Kampftalente": [], + "Vorteile": [], + "PropTable": { + "MU": "Mut", + "KL": "Klugheit", + "IN": "Intuition", + "CH": "Charisma", + "FF": "Fingerfertigkeit", + "GE": "Gewandtheit", + "KO": "Konstitution", + "KK": "Körperkraft" + }, + "Lebenspunkte_Basis": 21, + "Lebenspunkte_Aktuell": 21, + "Ausdauer_Basis": 0, + "Ausdauer_Aktuell": 0, + "Astralpunkte_Basis": 35, + "Astralpunkte_Aktuell": 35, + "Name": "Helga vom Drachenei, Tausendsasserin" + }, + { + "Eigenschaften": {}, + "Talente": [], + "Zauber": [], + "Kampftalente": [], + "Vorteile": [], + "PropTable": { + "MU": "Mut", + "KL": "Klugheit", + "IN": "Intuition", + "CH": "Charisma", + "FF": "Fingerfertigkeit", + "GE": "Gewandtheit", + "KO": "Konstitution", + "KK": "Körperkraft" + }, + "Lebenspunkte_Basis": 25, + "Lebenspunkte_Aktuell": 25, + "Ausdauer_Basis": 0, + "Ausdauer_Aktuell": 0, + "Astralpunkte_Basis": 0, + "Astralpunkte_Aktuell": 0, + "Name": "Krenko" + }, + { + "Eigenschaften": {}, + "Talente": [], + "Zauber": [], + "Kampftalente": [], + "Vorteile": [], + "PropTable": { + "MU": "Mut", + "KL": "Klugheit", + "IN": "Intuition", + "CH": "Charisma", + "FF": "Fingerfertigkeit", + "GE": "Gewandtheit", + "KO": "Konstitution", + "KK": "Körperkraft" + }, + "Lebenspunkte_Basis": 39, + "Lebenspunkte_Aktuell": 39, + "Ausdauer_Basis": 0, + "Ausdauer_Aktuell": 0, + "Astralpunkte_Basis": 0, + "Astralpunkte_Aktuell": 0, + "Name": "Ledur Torfinson" + }, + { + "Eigenschaften": {}, + "Talente": [], + "Zauber": [], + "Kampftalente": [], + "Vorteile": [], + "PropTable": { + "MU": "Mut", + "KL": "Klugheit", + "IN": "Intuition", + "CH": "Charisma", + "FF": "Fingerfertigkeit", + "GE": "Gewandtheit", + "KO": "Konstitution", + "KK": "Körperkraft" + }, + "Lebenspunkte_Basis": 26, + "Lebenspunkte_Aktuell": 26, + "Ausdauer_Basis": 0, + "Ausdauer_Aktuell": 0, + "Astralpunkte_Basis": 13, + "Astralpunkte_Aktuell": 13, + "Name": "Morla" + }, + { + "Eigenschaften": {}, + "Talente": [], + "Zauber": [], + "Kampftalente": [], + "Vorteile": [], + "PropTable": { + "MU": "Mut", + "KL": "Klugheit", + "IN": "Intuition", + "CH": "Charisma", + "FF": "Fingerfertigkeit", + "GE": "Gewandtheit", + "KO": "Konstitution", + "KK": "Körperkraft" + }, + "Lebenspunkte_Basis": 28, + "Lebenspunkte_Aktuell": 28, + "Ausdauer_Basis": 0, + "Ausdauer_Aktuell": 0, + "Astralpunkte_Basis": 40, + "Astralpunkte_Aktuell": 40, + "Name": "Numeri Illuminus" + }, + { + "Eigenschaften": {}, + "Talente": [], + "Zauber": [], + "Kampftalente": [], + "Vorteile": [], + "PropTable": { + "MU": "Mut", + "KL": "Klugheit", + "IN": "Intuition", + "CH": "Charisma", + "FF": "Fingerfertigkeit", + "GE": "Gewandtheit", + "KO": "Konstitution", + "KK": "Körperkraft" + }, + "Lebenspunkte_Basis": 39, + "Lebenspunkte_Aktuell": 39, + "Ausdauer_Basis": 0, + "Ausdauer_Aktuell": 0, + "Astralpunkte_Basis": 16, + "Astralpunkte_Aktuell": 16, + "Name": "Potus" + }, + { + "Eigenschaften": {}, + "Talente": [], + "Zauber": [], + "Kampftalente": [], + "Vorteile": [], + "PropTable": { + "MU": "Mut", + "KL": "Klugheit", + "IN": "Intuition", + "CH": "Charisma", + "FF": "Fingerfertigkeit", + "GE": "Gewandtheit", + "KO": "Konstitution", + "KK": "Körperkraft" + }, + "Lebenspunkte_Basis": 18, + "Lebenspunkte_Aktuell": 18, + "Ausdauer_Basis": 0, + "Ausdauer_Aktuell": 0, + "Astralpunkte_Basis": 13, + "Astralpunkte_Aktuell": 13, + "Name": "Pump aus der Gosse" + }, + { + "Eigenschaften": {}, + "Talente": [], + "Zauber": [], + "Kampftalente": [], + "Vorteile": [], + "PropTable": { + "MU": "Mut", + "KL": "Klugheit", + "IN": "Intuition", + "CH": "Charisma", + "FF": "Fingerfertigkeit", + "GE": "Gewandtheit", + "KO": "Konstitution", + "KK": "Körperkraft" + }, + "Lebenspunkte_Basis": 34, + "Lebenspunkte_Aktuell": 34, + "Ausdauer_Basis": 0, + "Ausdauer_Aktuell": 0, + "Astralpunkte_Basis": 17, + "Astralpunkte_Aktuell": 17, + "Name": "Rhoktar4" + }, + { + "Eigenschaften": {}, + "Talente": [], + "Zauber": [], + "Kampftalente": [], + "Vorteile": [], + "PropTable": { + "MU": "Mut", + "KL": "Klugheit", + "IN": "Intuition", + "CH": "Charisma", + "FF": "Fingerfertigkeit", + "GE": "Gewandtheit", + "KO": "Konstitution", + "KK": "Körperkraft" + }, + "Lebenspunkte_Basis": 28, + "Lebenspunkte_Aktuell": 28, + "Ausdauer_Basis": 0, + "Ausdauer_Aktuell": 0, + "Astralpunkte_Basis": 43, + "Astralpunkte_Aktuell": 43, + "Name": "Volant" + } +] \ No newline at end of file diff --git a/DSACore/PropertiesNewtonsoft-Json-Linq-JProperty.json b/DSACore/PropertiesNewtonsoft-Json-Linq-JProperty.json new file mode 100644 index 0000000..5b400e5 --- /dev/null +++ b/DSACore/PropertiesNewtonsoft-Json-Linq-JProperty.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:2170", + "sslPort": 44365 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/commands", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "DSACore": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/commands", + "applicationUrl": "https://192.168.2.58:5001;http://192.168.2.58:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/DSACore/Startup.cs b/DSACore/Startup.cs index 0628151..c189436 100644 --- a/DSACore/Startup.cs +++ b/DSACore/Startup.cs @@ -26,12 +26,23 @@ namespace DSACore // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddCors(options => options.AddPolicy("CorsPolicy", + builder => + { + builder.WithOrigins("https://dsa.truekuehli.de") + .WithHeaders("Access-Control-Allow-Origin") + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + })); + /* services.AddCors(options => { options.AddPolicy("AllowSpecificOrigin", - builder => builder.WithOrigins("https://console.firebase.google.com/project/heldenonline-4d828")); + builder => builder.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin().AllowCredentials()/*WithOrigins("https://dsa.truekuehli.de")#1#); }); +*/ services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.AddSignalR(); @@ -49,9 +60,13 @@ namespace DSACore app.UseHsts(); } + app.UseCors("CorsPolicy"); + app.UseSignalR(routes => { routes.MapHub("/chatHub"); }); - app.UseCors("AllowSpecificOrigin"); - app.UseHttpsRedirection(); + + + //app.UseCors("AllowSpecificOrigin"); + //app.UseHttpsRedirection(); app.UseMvc(); } } -- cgit v1.3.1 From 043cdb904dbe171b6feeb3addf08664a02ab7579 Mon Sep 17 00:00:00 2001 From: TrueDoctor Date: Fri, 28 Sep 2018 00:53:05 +0200 Subject: added FirestoreApis --- DSACore/DSACore.csproj | 2 ++ DSACore/Properties/PublishProfiles/FolderProfile1.pubxml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'DSACore') diff --git a/DSACore/DSACore.csproj b/DSACore/DSACore.csproj index 67bd4ab..da2feec 100644 --- a/DSACore/DSACore.csproj +++ b/DSACore/DSACore.csproj @@ -9,6 +9,8 @@ + + diff --git a/DSACore/Properties/PublishProfiles/FolderProfile1.pubxml b/DSACore/Properties/PublishProfiles/FolderProfile1.pubxml index e929098..e03b55a 100644 --- a/DSACore/Properties/PublishProfiles/FolderProfile1.pubxml +++ b/DSACore/Properties/PublishProfiles/FolderProfile1.pubxml @@ -13,7 +13,7 @@ indem Sie diese MSBuild-Datei bearbeiten. Weitere Informationen hierzu finden Si True False netcoreapp2.1 - win-x64 + linux-x64 35a5e2cc-0fd4-4bc0-acbf-38599caed1c4 false <_IsPortable>true -- cgit v1.3.1 From edb346d1e6599dbe48ec257f3536f6021163673c Mon Sep 17 00:00:00 2001 From: TrueDoctor Date: Fri, 28 Sep 2018 15:16:59 +0200 Subject: added some validation functionality --- DSACore/Hubs/ChatHub.cs | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'DSACore') diff --git a/DSACore/Hubs/ChatHub.cs b/DSACore/Hubs/ChatHub.cs index 9cd7aed..c429d52 100644 --- a/DSACore/Hubs/ChatHub.cs +++ b/DSACore/Hubs/ChatHub.cs @@ -24,6 +24,11 @@ namespace DSACore.Hubs return Clients.Group(group).SendCoreAsync("ReceiveMessage", new object[] { user, message }); } + public async Task GetGroups() + { + await Clients.Caller.SendCoreAsync("ListGroups", new object[] { "TheCrew", "Testdata" }); + } + public async Task Login(string group, string password) { if (password == "valid") -- cgit v1.3.1 From 04912687127303fd270b61f131cf68fd4aaae956 Mon Sep 17 00:00:00 2001 From: TrueDoctor Date: Sat, 29 Sep 2018 05:49:13 +0200 Subject: modified user group authenth --- DSACore/Hubs/ChatHub.cs | 14 +++++++++----- DSACore/Models/User.cs | 13 +++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 DSACore/Models/User.cs (limited to 'DSACore') diff --git a/DSACore/Hubs/ChatHub.cs b/DSACore/Hubs/ChatHub.cs index c429d52..a8bf883 100644 --- a/DSACore/Hubs/ChatHub.cs +++ b/DSACore/Hubs/ChatHub.cs @@ -9,14 +9,15 @@ namespace DSACore.Hubs { public class ChatHub : Hub { + private static Dictionary UserGroup; + public async Task SendMessage(string user, string message) { - string group = "default"; var args = message.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(); var ident = args.First().Replace("!", ""); if(args.Count>0){args.RemoveAt(0);} - await SendToGroup(group, user, Commands.CommandHandler.ExecuteCommand(new Command{CharId = 0,CmdIdentifier = ident, CmdTexts = args, Name = user})); + await SendToGroup(UserGroup[Context.ConnectionId].Group, user, Commands.CommandHandler.ExecuteCommand(new Command{CharId = 0,CmdIdentifier = ident, CmdTexts = args, Name = user})); } private Task SendToGroup(string group, string user, string message) @@ -27,23 +28,26 @@ namespace DSACore.Hubs public async Task GetGroups() { await Clients.Caller.SendCoreAsync("ListGroups", new object[] { "TheCrew", "Testdata" }); + throw new NotImplementedException("add database call to get groups"); } public async Task Login(string group, string password) { if (password == "valid") { + UserGroup.Add(Context.ConnectionId, new User{Group = group}); await Groups.AddToGroupAsync(Context.ConnectionId, group); } await SendToGroup(group, "", "Ein neuer Nutzer hat die Gruppe betreten"); } - public async Task Disconnect(string group, string user) + public async Task Disconnect() { - await Groups.RemoveFromGroupAsync(Context.ConnectionId, group); + var user = UserGroup[Context.ConnectionId]; + await Groups.RemoveFromGroupAsync(Context.ConnectionId, user.Group); - await SendToGroup(group, user, "Connection lost"); + await SendToGroup(user.Group, user.Name, "Connection lost"); } } diff --git a/DSACore/Models/User.cs b/DSACore/Models/User.cs new file mode 100644 index 0000000..105e564 --- /dev/null +++ b/DSACore/Models/User.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace DSACore.Models +{ + public class User + { + public string Name { get; set; } + public string Group { get; set; } + } +} -- cgit v1.3.1 From de0f076ef9ff546c9a90513259ad6c42cd2224b3 Mon Sep 17 00:00:00 2001 From: TrueDoctor Date: Sat, 29 Sep 2018 16:51:26 +0200 Subject: added firebase api --- DSACore/Commands/CommandHandler.cs | 3 +- DSACore/DSACore.csproj | 4 +- DSACore/FireBase/Database.cs | 24 ++ DiscoBot.sln | 39 +- FireBase/ExceptionEventArgs.cs | 28 ++ FireBase/Extensions/ObservableExtensions.cs | 40 ++ FireBase/Extensions/TaskExtensions.cs | 23 ++ FireBase/FireBase.csproj | 13 + FireBase/FirebaseClient.cs | 57 +++ FireBase/FirebaseException.cs | 63 +++ FireBase/FirebaseKeyGenerator.cs | 92 +++++ FireBase/FirebaseObject.cs | 31 ++ FireBase/FirebaseOptions.cs | 76 ++++ FireBase/Http/HttpClientExtensions.cs | 90 +++++ FireBase/Http/PostResult.cs | 17 + FireBase/ObservableExtensions.cs | 44 +++ FireBase/Offline/ConcurrentOfflineDatabase.cs | 207 ++++++++++ FireBase/Offline/DatabaseExtensions.cs | 195 +++++++++ FireBase/Offline/ISetHandler.cs | 11 + FireBase/Offline/InitialPullStrategy.cs | 23 ++ FireBase/Offline/Internals/MemberAccessVisitor.cs | 51 +++ FireBase/Offline/OfflineCacheAdapter.cs | 165 ++++++++ FireBase/Offline/OfflineDatabase.cs | 201 ++++++++++ FireBase/Offline/OfflineEntry.cs | 116 ++++++ FireBase/Offline/RealtimeDatabase.cs | 459 ++++++++++++++++++++++ FireBase/Offline/SetHandler.cs | 24 ++ FireBase/Offline/StreamingOptions.cs | 21 + FireBase/Offline/SyncOptions.cs | 28 ++ FireBase/Query/AuthQuery.cs | 33 ++ FireBase/Query/ChildQuery.cs | 56 +++ FireBase/Query/FilterQuery.cs | 81 ++++ FireBase/Query/FirebaseQuery.cs | 314 +++++++++++++++ FireBase/Query/IFirebaseQuery.cs | 43 ++ FireBase/Query/OrderQuery.cs | 34 ++ FireBase/Query/ParameterQuery.cs | 43 ++ FireBase/Query/QueryExtensions.cs | 207 ++++++++++ FireBase/Query/QueryFactoryExtensions.cs | 176 +++++++++ FireBase/Query/SilentQuery.cs | 18 + FireBase/Settings.StyleCop | 77 ++++ FireBase/Streaming/FirebaseCache.cs | 192 +++++++++ FireBase/Streaming/FirebaseEvent.cs | 40 ++ FireBase/Streaming/FirebaseEventSource.cs | 38 ++ FireBase/Streaming/FirebaseEventType.cs | 18 + FireBase/Streaming/FirebaseServerEventType.cs | 15 + FireBase/Streaming/FirebaseSubscription.cs | 221 +++++++++++ FireBase/Streaming/NonBlockingStreamReader.cs | 60 +++ WebInterface/NodeJSServer/dist/Web.config | 15 + ZooBOTanica/CritCreate.cs | 2 +- 48 files changed, 3817 insertions(+), 11 deletions(-) create mode 100644 DSACore/FireBase/Database.cs create mode 100644 FireBase/ExceptionEventArgs.cs create mode 100644 FireBase/Extensions/ObservableExtensions.cs create mode 100644 FireBase/Extensions/TaskExtensions.cs create mode 100644 FireBase/FireBase.csproj create mode 100644 FireBase/FirebaseClient.cs create mode 100644 FireBase/FirebaseException.cs create mode 100644 FireBase/FirebaseKeyGenerator.cs create mode 100644 FireBase/FirebaseObject.cs create mode 100644 FireBase/FirebaseOptions.cs create mode 100644 FireBase/Http/HttpClientExtensions.cs create mode 100644 FireBase/Http/PostResult.cs create mode 100644 FireBase/ObservableExtensions.cs create mode 100644 FireBase/Offline/ConcurrentOfflineDatabase.cs create mode 100644 FireBase/Offline/DatabaseExtensions.cs create mode 100644 FireBase/Offline/ISetHandler.cs create mode 100644 FireBase/Offline/InitialPullStrategy.cs create mode 100644 FireBase/Offline/Internals/MemberAccessVisitor.cs create mode 100644 FireBase/Offline/OfflineCacheAdapter.cs create mode 100644 FireBase/Offline/OfflineDatabase.cs create mode 100644 FireBase/Offline/OfflineEntry.cs create mode 100644 FireBase/Offline/RealtimeDatabase.cs create mode 100644 FireBase/Offline/SetHandler.cs create mode 100644 FireBase/Offline/StreamingOptions.cs create mode 100644 FireBase/Offline/SyncOptions.cs create mode 100644 FireBase/Query/AuthQuery.cs create mode 100644 FireBase/Query/ChildQuery.cs create mode 100644 FireBase/Query/FilterQuery.cs create mode 100644 FireBase/Query/FirebaseQuery.cs create mode 100644 FireBase/Query/IFirebaseQuery.cs create mode 100644 FireBase/Query/OrderQuery.cs create mode 100644 FireBase/Query/ParameterQuery.cs create mode 100644 FireBase/Query/QueryExtensions.cs create mode 100644 FireBase/Query/QueryFactoryExtensions.cs create mode 100644 FireBase/Query/SilentQuery.cs create mode 100644 FireBase/Settings.StyleCop create mode 100644 FireBase/Streaming/FirebaseCache.cs create mode 100644 FireBase/Streaming/FirebaseEvent.cs create mode 100644 FireBase/Streaming/FirebaseEventSource.cs create mode 100644 FireBase/Streaming/FirebaseEventType.cs create mode 100644 FireBase/Streaming/FirebaseServerEventType.cs create mode 100644 FireBase/Streaming/FirebaseSubscription.cs create mode 100644 FireBase/Streaming/NonBlockingStreamReader.cs create mode 100644 WebInterface/NodeJSServer/dist/Web.config (limited to 'DSACore') diff --git a/DSACore/Commands/CommandHandler.cs b/DSACore/Commands/CommandHandler.cs index 446f99e..4b6ca42 100644 --- a/DSACore/Commands/CommandHandler.cs +++ b/DSACore/Commands/CommandHandler.cs @@ -97,7 +97,6 @@ namespace DSACore.Commands private static string CheckCommand(string name, CommandTypes command, string waffe, int erschwernis = 0) { var chr = Dsa.GetCharacter(0); - throw new NotImplementedException("access char by id ore name and group id"); switch (command) { @@ -116,6 +115,8 @@ namespace DSACore.Commands } return $"{name} verwendet {waffe}"; + + throw new NotImplementedException("access char by id ore name and group id"); } } } diff --git a/DSACore/DSACore.csproj b/DSACore/DSACore.csproj index da2feec..ad760c2 100644 --- a/DSACore/DSACore.csproj +++ b/DSACore/DSACore.csproj @@ -2,6 +2,7 @@ netcoreapp2.1 + DSACore.Program @@ -9,13 +10,12 @@ - - + diff --git a/DSACore/FireBase/Database.cs b/DSACore/FireBase/Database.cs new file mode 100644 index 0000000..cd21c00 --- /dev/null +++ b/DSACore/FireBase/Database.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Firebase.Database; +using Firebase.Database.Query; + + +namespace DSACore.FireBase +{ + public static class Database + { + static Database() + { + var auth = "ABCDE"; // your app secret + var firebaseClient = new FirebaseClient( + "", + new FirebaseOptions + { + AuthTokenAsyncFactory = () => Task.FromResult(auth) + }); + } + } +} diff --git a/DiscoBot.sln b/DiscoBot.sln index 7aab504..93659a9 100644 --- a/DiscoBot.sln +++ b/DiscoBot.sln @@ -7,11 +7,32 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscoBot", "DiscoBot\DiscoB EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZooBOTanica", "ZooBOTanica\ZooBOTanica.csproj", "{58917D99-DC94-4CDD-AD2B-C6E0BAFFCF47}" EndProject -Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "NodeJSServer", "NodeJSServer\NodeJSServer.njsproj", "{57064377-C08C-4218-9C55-0552D40F3877}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DSALib", "DSALib\DSALib.csproj", "{388DD4ED-29C4-4127-AC8F-34DD3FE9F9B0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DSACore", "DSACore\DSACore.csproj", "{35A5E2CC-0FD4-4BC0-ACBF-38599CAED1C4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DSACore", "DSACore\DSACore.csproj", "{35A5E2CC-0FD4-4BC0-ACBF-38599CAED1C4}" +EndProject +Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "dist", "WebInterface\NodeJSServer\dist\", "{3FEC1233-072D-4031-BBEB-B9804C58BD15}" + ProjectSection(WebsiteProperties) = preProject + TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5" + Debug.AspNetCompiler.VirtualPath = "/localhost_1915" + Debug.AspNetCompiler.PhysicalPath = "WebInterface\NodeJSServer\dist\" + Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_1915\" + Debug.AspNetCompiler.Updateable = "true" + Debug.AspNetCompiler.ForceOverwrite = "true" + Debug.AspNetCompiler.FixedNames = "false" + Debug.AspNetCompiler.Debug = "True" + Release.AspNetCompiler.VirtualPath = "/localhost_1915" + Release.AspNetCompiler.PhysicalPath = "WebInterface\NodeJSServer\dist\" + Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_1915\" + Release.AspNetCompiler.Updateable = "true" + Release.AspNetCompiler.ForceOverwrite = "true" + Release.AspNetCompiler.FixedNames = "false" + Release.AspNetCompiler.Debug = "False" + VWDPort = "1915" + SlnRelativePath = "WebInterface\NodeJSServer\dist\" + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FireBase", "FireBase\FireBase.csproj", "{87CC30E6-CBEA-4282-A3CC-FD5119A1993B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,10 +48,6 @@ Global {58917D99-DC94-4CDD-AD2B-C6E0BAFFCF47}.Debug|Any CPU.Build.0 = Debug|Any CPU {58917D99-DC94-4CDD-AD2B-C6E0BAFFCF47}.Release|Any CPU.ActiveCfg = Release|Any CPU {58917D99-DC94-4CDD-AD2B-C6E0BAFFCF47}.Release|Any CPU.Build.0 = Release|Any CPU - {57064377-C08C-4218-9C55-0552D40F3877}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {57064377-C08C-4218-9C55-0552D40F3877}.Debug|Any CPU.Build.0 = Debug|Any CPU - {57064377-C08C-4218-9C55-0552D40F3877}.Release|Any CPU.ActiveCfg = Release|Any CPU - {57064377-C08C-4218-9C55-0552D40F3877}.Release|Any CPU.Build.0 = Release|Any CPU {388DD4ED-29C4-4127-AC8F-34DD3FE9F9B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {388DD4ED-29C4-4127-AC8F-34DD3FE9F9B0}.Debug|Any CPU.Build.0 = Debug|Any CPU {388DD4ED-29C4-4127-AC8F-34DD3FE9F9B0}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -39,6 +56,14 @@ Global {35A5E2CC-0FD4-4BC0-ACBF-38599CAED1C4}.Debug|Any CPU.Build.0 = Debug|Any CPU {35A5E2CC-0FD4-4BC0-ACBF-38599CAED1C4}.Release|Any CPU.ActiveCfg = Release|Any CPU {35A5E2CC-0FD4-4BC0-ACBF-38599CAED1C4}.Release|Any CPU.Build.0 = Release|Any CPU + {3FEC1233-072D-4031-BBEB-B9804C58BD15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3FEC1233-072D-4031-BBEB-B9804C58BD15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3FEC1233-072D-4031-BBEB-B9804C58BD15}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {3FEC1233-072D-4031-BBEB-B9804C58BD15}.Release|Any CPU.Build.0 = Debug|Any CPU + {87CC30E6-CBEA-4282-A3CC-FD5119A1993B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87CC30E6-CBEA-4282-A3CC-FD5119A1993B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87CC30E6-CBEA-4282-A3CC-FD5119A1993B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87CC30E6-CBEA-4282-A3CC-FD5119A1993B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FireBase/ExceptionEventArgs.cs b/FireBase/ExceptionEventArgs.cs new file mode 100644 index 0000000..f1c7fac --- /dev/null +++ b/FireBase/ExceptionEventArgs.cs @@ -0,0 +1,28 @@ +namespace Firebase.Database +{ + using System; + + /// + /// Event args holding the object. + /// + public class ExceptionEventArgs : EventArgs where T : Exception + { + public readonly T Exception; + + /// + /// Initializes a new instance of the class. + /// + /// The exception. + public ExceptionEventArgs(T exception) + { + this.Exception = exception; + } + } + + public class ExceptionEventArgs : ExceptionEventArgs + { + public ExceptionEventArgs(Exception exception) : base(exception) + { + } + } +} diff --git a/FireBase/Extensions/ObservableExtensions.cs b/FireBase/Extensions/ObservableExtensions.cs new file mode 100644 index 0000000..12cd5f3 --- /dev/null +++ b/FireBase/Extensions/ObservableExtensions.cs @@ -0,0 +1,40 @@ +namespace Firebase.Database.Extensions +{ + using System; + using System.Reactive.Linq; + + public static class ObservableExtensions + { + /// + /// Returns a cold observable which retries (re-subscribes to) the source observable on error until it successfully terminates. + /// + /// The source observable. + /// How long to wait between attempts. + /// A predicate determining for which exceptions to retry. Defaults to all + /// + /// A cold observable which retries (re-subscribes to) the source observable on error up to the + /// specified number of times or until it successfully terminates. + /// + public static IObservable RetryAfterDelay( + this IObservable source, + TimeSpan dueTime, + Func retryOnError) + where TException: Exception + { + int attempt = 0; + + return Observable.Defer(() => + { + return ((++attempt == 1) ? source : source.DelaySubscription(dueTime)) + .Select(item => new Tuple(true, item, null)) + .Catch, TException>(e => retryOnError(e) + ? Observable.Throw>(e) + : Observable.Return(new Tuple(false, default(T), e))); + }) + .Retry() + .SelectMany(t => t.Item1 + ? Observable.Return(t.Item2) + : Observable.Throw(t.Item3)); + } + } +} diff --git a/FireBase/Extensions/TaskExtensions.cs b/FireBase/Extensions/TaskExtensions.cs new file mode 100644 index 0000000..26bbde6 --- /dev/null +++ b/FireBase/Extensions/TaskExtensions.cs @@ -0,0 +1,23 @@ +namespace Firebase.Database.Extensions +{ + using System; + using System.Threading.Tasks; + + public static class TaskExtensions + { + /// + /// Instead of unwrapping it throws it as it is. + /// + public static async Task WithAggregateException(this Task source) + { + try + { + await source.ConfigureAwait(false); + } + catch (Exception ex) + { + throw source.Exception ?? ex; + } + } + } +} diff --git a/FireBase/FireBase.csproj b/FireBase/FireBase.csproj new file mode 100644 index 0000000..889c32f --- /dev/null +++ b/FireBase/FireBase.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp2.1 + + + + + + + + + diff --git a/FireBase/FirebaseClient.cs b/FireBase/FirebaseClient.cs new file mode 100644 index 0000000..a237c8d --- /dev/null +++ b/FireBase/FirebaseClient.cs @@ -0,0 +1,57 @@ +using System.Net.Http; + +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Firebase.Database.Tests")] + +namespace Firebase.Database +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + + using Firebase.Database.Offline; + using Firebase.Database.Query; + + /// + /// Firebase client which acts as an entry point to the online database. + /// + public class FirebaseClient : IDisposable + { + internal readonly HttpClient HttpClient; + internal readonly FirebaseOptions Options; + + private readonly string baseUrl; + + /// + /// Initializes a new instance of the class. + /// + /// The base url. + /// Offline database. + public FirebaseClient(string baseUrl, FirebaseOptions options = null) + { + this.HttpClient = new HttpClient(); + this.Options = options ?? new FirebaseOptions(); + + this.baseUrl = baseUrl; + + if (!this.baseUrl.EndsWith("/")) + { + this.baseUrl += "/"; + } + } + + /// + /// Queries for a child of the data root. + /// + /// Name of the child. + /// . + public ChildQuery Child(string resourceName) + { + return new ChildQuery(this, () => this.baseUrl + resourceName); + } + + public void Dispose() + { + HttpClient?.Dispose(); + } + } +} diff --git a/FireBase/FirebaseException.cs b/FireBase/FirebaseException.cs new file mode 100644 index 0000000..e4b782b --- /dev/null +++ b/FireBase/FirebaseException.cs @@ -0,0 +1,63 @@ +namespace Firebase.Database +{ + using System; + using System.Net; + + public class FirebaseException : Exception + { + public FirebaseException(string requestUrl, string requestData, string responseData, HttpStatusCode statusCode) + : base(GenerateExceptionMessage(requestUrl, requestData, responseData)) + { + this.RequestUrl = requestUrl; + this.RequestData = requestData; + this.ResponseData = responseData; + this.StatusCode = statusCode; + } + + public FirebaseException(string requestUrl, string requestData, string responseData, HttpStatusCode statusCode, Exception innerException) + : base(GenerateExceptionMessage(requestUrl, requestData, responseData), innerException) + { + this.RequestUrl = requestUrl; + this.RequestData = requestData; + this.ResponseData = responseData; + this.StatusCode = statusCode; + } + + /// + /// Post data passed to the authentication service. + /// + public string RequestData + { + get; + } + + /// + /// Original url of the request. + /// + public string RequestUrl + { + get; + } + + /// + /// Response from the authentication service. + /// + public string ResponseData + { + get; + } + + /// + /// Status code of the response. + /// + public HttpStatusCode StatusCode + { + get; + } + + private static string GenerateExceptionMessage(string requestUrl, string requestData, string responseData) + { + return $"Exception occured while processing the request.\nUrl: {requestUrl}\nRequest Data: {requestData}\nResponse: {responseData}"; + } + } +} diff --git a/FireBase/FirebaseKeyGenerator.cs b/FireBase/FirebaseKeyGenerator.cs new file mode 100644 index 0000000..acad399 --- /dev/null +++ b/FireBase/FirebaseKeyGenerator.cs @@ -0,0 +1,92 @@ +namespace Firebase.Database +{ + using System; + using System.Text; + + /// + /// Offline key generator which mimics the official Firebase generators. + /// Credit: https://github.com/bubbafat/FirebaseSharp/blob/master/src/FirebaseSharp.Portable/FireBasePushIdGenerator.cs + /// + public class FirebaseKeyGenerator + { + // Modeled after base64 web-safe chars, but ordered by ASCII. + private const string PushCharsString = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; + private static readonly char[] PushChars; + private static readonly DateTimeOffset Epoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero); + + private static readonly Random random = new Random(); + private static readonly byte[] lastRandChars = new byte[12]; + + // Timestamp of last push, used to prevent local collisions if you push twice in one ms. + private static long lastPushTime; + + static FirebaseKeyGenerator() + { + PushChars = Encoding.UTF8.GetChars(Encoding.UTF8.GetBytes(PushCharsString)); + } + + /// + /// Returns next firebase key based on current time. + /// + /// + /// The . + public static string Next() + { + // We generate 72-bits of randomness which get turned into 12 characters and + // appended to the timestamp to prevent collisions with other clients. We store the last + // characters we generated because in the event of a collision, we'll use those same + // characters except "incremented" by one. + var id = new StringBuilder(20); + var now = (long)(DateTimeOffset.Now - Epoch).TotalMilliseconds; + var duplicateTime = now == lastPushTime; + lastPushTime = now; + + var timeStampChars = new char[8]; + for (int i = 7; i >= 0; i--) + { + var index = (int)(now % PushChars.Length); + timeStampChars[i] = PushChars[index]; + now = (long)Math.Floor((double)now / PushChars.Length); + } + + if (now != 0) + { + throw new Exception("We should have converted the entire timestamp."); + } + + id.Append(timeStampChars); + + if (!duplicateTime) + { + for (int i = 0; i < 12; i++) + { + lastRandChars[i] = (byte)random.Next(0, PushChars.Length); + } + } + else + { + // If the timestamp hasn't changed since last push, use the same random number, + // except incremented by 1. + var lastIndex = 11; + for (; lastIndex >= 0 && lastRandChars[lastIndex] == PushChars.Length - 1; lastIndex--) + { + lastRandChars[lastIndex] = 0; + } + + lastRandChars[lastIndex]++; + } + + for (int i = 0; i < 12; i++) + { + id.Append(PushChars[lastRandChars[i]]); + } + + if (id.Length != 20) + { + throw new Exception("Length should be 20."); + } + + return id.ToString(); + } + } +} diff --git a/FireBase/FirebaseObject.cs b/FireBase/FirebaseObject.cs new file mode 100644 index 0000000..ea61893 --- /dev/null +++ b/FireBase/FirebaseObject.cs @@ -0,0 +1,31 @@ +namespace Firebase.Database +{ + /// + /// Holds the object of type along with its key. + /// + /// Type of the underlying object. + public class FirebaseObject + { + internal FirebaseObject(string key, T obj) + { + this.Key = key; + this.Object = obj; + } + + /// + /// Gets the key of . + /// + public string Key + { + get; + } + + /// + /// Gets the underlying object. + /// + public T Object + { + get; + } + } +} diff --git a/FireBase/FirebaseOptions.cs b/FireBase/FirebaseOptions.cs new file mode 100644 index 0000000..9905956 --- /dev/null +++ b/FireBase/FirebaseOptions.cs @@ -0,0 +1,76 @@ +namespace Firebase.Database +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Threading.Tasks; + + using Firebase.Database.Offline; + + using Newtonsoft.Json; + + public class FirebaseOptions + { + public FirebaseOptions() + { + this.OfflineDatabaseFactory = (t, s) => new Dictionary(); + this.SubscriptionStreamReaderFactory = s => new StreamReader(s); + this.JsonSerializerSettings = new JsonSerializerSettings(); + this.SyncPeriod = TimeSpan.FromSeconds(10); + } + + /// + /// Gets or sets the factory for Firebase offline database. Default is in-memory dictionary. + /// + public Func> OfflineDatabaseFactory + { + get; + set; + } + + /// + /// Gets or sets the method for retrieving auth tokens. Default is null. + /// + public Func> AuthTokenAsyncFactory + { + get; + set; + } + + /// + /// Gets or sets the factory for used for reading online streams. Default is . + /// + public Func SubscriptionStreamReaderFactory + { + get; + set; + } + + /// + /// Gets or sets the json serializer settings. + /// + public JsonSerializerSettings JsonSerializerSettings + { + get; + set; + } + + /// + /// Gets or sets the time between synchronization attempts for pulling and pushing offline entities. Default is 10 seconds. + /// + public TimeSpan SyncPeriod + { + get; + set; + } + + /// + /// Specify if token returned by factory will be used as "auth" url parameter or "access_token". + /// + public bool AsAccessToken + { + get; + set; + } + } +} diff --git a/FireBase/Http/HttpClientExtensions.cs b/FireBase/Http/HttpClientExtensions.cs new file mode 100644 index 0000000..444145b --- /dev/null +++ b/FireBase/Http/HttpClientExtensions.cs @@ -0,0 +1,90 @@ +namespace Firebase.Database.Http +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Net.Http; + using System.Threading.Tasks; + + using Newtonsoft.Json; + using System.Net; + + /// + /// The http client extensions for object deserializations. + /// + internal static class HttpClientExtensions + { + /// + /// The get object collection async. + /// + /// The client. + /// The request uri. + /// The specific JSON Serializer Settings. + /// The type of entities the collection should contain. + /// The . + public static async Task>> GetObjectCollectionAsync(this HttpClient client, string requestUri, + JsonSerializerSettings jsonSerializerSettings) + { + var responseData = string.Empty; + var statusCode = HttpStatusCode.OK; + + try + { + var response = await client.GetAsync(requestUri).ConfigureAwait(false); + statusCode = response.StatusCode; + responseData = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + var dictionary = JsonConvert.DeserializeObject>(responseData, jsonSerializerSettings); + + if (dictionary == null) + { + return new FirebaseObject[0]; + } + + return dictionary.Select(item => new FirebaseObject(item.Key, item.Value)).ToList(); + } + catch (Exception ex) + { + throw new FirebaseException(requestUri, string.Empty, responseData, statusCode, ex); + } + } + + /// + /// The get object collection async. + /// + /// The json data. + /// The type of entities the collection should contain. + /// The . + public static IEnumerable> GetObjectCollection(this string data, Type elementType) + { + var dictionaryType = typeof(Dictionary<,>).MakeGenericType(typeof(string), elementType); + IDictionary dictionary = null; + + if (data.StartsWith("[")) + { + var listType = typeof(List<>).MakeGenericType(elementType); + var list = JsonConvert.DeserializeObject(data, listType) as IList; + dictionary = Activator.CreateInstance(dictionaryType) as IDictionary; + var index = 0; + foreach (var item in list) dictionary.Add(index++.ToString(), item); + } + else + { + dictionary = JsonConvert.DeserializeObject(data, dictionaryType) as IDictionary; + } + + if (dictionary == null) + { + yield break; + } + + foreach (DictionaryEntry item in dictionary) + { + yield return new FirebaseObject((string)item.Key, item.Value); + } + } + } +} diff --git a/FireBase/Http/PostResult.cs b/FireBase/Http/PostResult.cs new file mode 100644 index 0000000..3f010d4 --- /dev/null +++ b/FireBase/Http/PostResult.cs @@ -0,0 +1,17 @@ +namespace Firebase.Database.Http +{ + /// + /// Represents data returned after a successful POST to firebase server. + /// + public class PostResult + { + /// + /// Gets or sets the generated key after a successful post. + /// + public string Name + { + get; + set; + } + } +} diff --git a/FireBase/ObservableExtensions.cs b/FireBase/ObservableExtensions.cs new file mode 100644 index 0000000..37c3ef7 --- /dev/null +++ b/FireBase/ObservableExtensions.cs @@ -0,0 +1,44 @@ +namespace Firebase.Database +{ + using System; + using System.Collections.ObjectModel; + + using Firebase.Database.Streaming; + + /// + /// Extensions for . + /// + public static class ObservableExtensions + { + /// + /// Starts observing on given firebase observable and propagates event into an . + /// + /// The observable. + /// Type of entity. + /// The . + public static ObservableCollection AsObservableCollection(this IObservable> observable) + { + var collection = new ObservableCollection(); + + observable.Subscribe(f => + { + if (f.EventType == FirebaseEventType.InsertOrUpdate) + { + var i = collection.IndexOf(f.Object); + if (i >= 0) + { + collection.RemoveAt(i); + } + + collection.Add(f.Object); + } + else + { + collection.Remove(f.Object); + } + }); + + return collection; + } + } +} diff --git a/FireBase/Offline/ConcurrentOfflineDatabase.cs b/FireBase/Offline/ConcurrentOfflineDatabase.cs new file mode 100644 index 0000000..226892d --- /dev/null +++ b/FireBase/Offline/ConcurrentOfflineDatabase.cs @@ -0,0 +1,207 @@ +namespace Firebase.Database.Offline +{ + using System; + using System.Collections; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.IO; + using System.Linq; + + using LiteDB; + + /// + /// The offline database. + /// + public class ConcurrentOfflineDatabase : IDictionary + { + private readonly LiteRepository db; + private readonly ConcurrentDictionary ccache; + + /// + /// Initializes a new instance of the class. + /// + /// The item type which is used to determine the database file name. + /// Custom string which will get appended to the file name. + public ConcurrentOfflineDatabase(Type itemType, string filenameModifier) + { + var fullName = this.GetFileName(itemType.ToString()); + if(fullName.Length > 100) + { + fullName = fullName.Substring(0, 100); + } + + BsonMapper mapper = BsonMapper.Global; + mapper.Entity().Id(o => o.Key); + + string root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + string filename = fullName + filenameModifier + ".db"; + var path = Path.Combine(root, filename); + this.db = new LiteRepository(new LiteDatabase(path, mapper)); + + var cache = db.Database + .GetCollection() + .FindAll() + .ToDictionary(o => o.Key, o => o); + + this.ccache = new ConcurrentDictionary(cache); + + } + + /// + /// Gets the number of elements contained in the . + /// + /// The number of elements contained in the . + public int Count => this.ccache.Count; + + /// + /// Gets a value indicating whether this is a read-only collection. + /// + public bool IsReadOnly => false; + + /// + /// Gets an containing the keys of the . + /// + /// An containing the keys of the object that implements . + public ICollection Keys => this.ccache.Keys; + + /// + /// Gets an containing the values in the . + /// + /// An containing the values in the object that implements . + public ICollection Values => this.ccache.Values; + + /// + /// Gets or sets the element with the specified key. + /// + /// The key of the element to get or set. + /// The element with the specified key. + public OfflineEntry this[string key] + { + get + { + return this.ccache[key]; + } + + set + { + this.ccache.AddOrUpdate(key, value, (k, existing) => value); + this.db.Upsert(value); + } + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that can be used to iterate through the collection. + public IEnumerator> GetEnumerator() + { + return this.ccache.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + /// + /// Adds an item to the . + /// + /// The object to add to the . + public void Add(KeyValuePair item) + { + this.Add(item.Key, item.Value); + } + + /// + /// Removes all items from the . + /// + public void Clear() + { + this.ccache.Clear(); + this.db.Delete(Query.All()); + } + + /// + /// Determines whether the contains a specific value. + /// + /// The object to locate in the . + /// True if is found in the ; otherwise, false. + public bool Contains(KeyValuePair item) + { + return this.ContainsKey(item.Key); + } + + /// + /// Copies the elements of the to an , starting at a particular index. + /// + /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. + /// The zero-based index in at which copying begins. + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + this.ccache.ToList().CopyTo(array, arrayIndex); + } + + /// + /// Removes the first occurrence of a specific object from the . + /// + /// The object to remove from the . + /// True if was successfully removed from the ; otherwise, false. This method also returns false if is not found in the original . + public bool Remove(KeyValuePair item) + { + return this.Remove(item.Key); + } + + /// + /// Determines whether the contains an element with the specified key. + /// + /// The key to locate in the . + /// True if the contains an element with the key; otherwise, false. + public bool ContainsKey(string key) + { + return this.ccache.ContainsKey(key); + } + + /// + /// Adds an element with the provided key and value to the . + /// + /// The object to use as the key of the element to add. + /// The object to use as the value of the element to add. + public void Add(string key, OfflineEntry value) + { + this.ccache.AddOrUpdate(key, value, (k, existing) => value); + this.db.Upsert(value); + } + + /// + /// Removes the element with the specified key from the . + /// + /// The key of the element to remove. + /// True if the element is successfully removed; otherwise, false. This method also returns false if was not found in the original . + public bool Remove(string key) + { + this.ccache.TryRemove(key, out OfflineEntry _); + return this.db.Delete(key); + } + + /// + /// Gets the value associated with the specified key. + /// + /// The key whose value to get.When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. + /// True if the object that implements contains an element with the specified key; otherwise, false. + public bool TryGetValue(string key, out OfflineEntry value) + { + return this.ccache.TryGetValue(key, out value); + } + + private string GetFileName(string fileName) + { + var invalidChars = new[] { '`', '[', ',', '=' }; + foreach(char c in invalidChars.Concat(System.IO.Path.GetInvalidFileNameChars()).Distinct()) + { + fileName = fileName.Replace(c, '_'); + } + + return fileName; + } + } +} diff --git a/FireBase/Offline/DatabaseExtensions.cs b/FireBase/Offline/DatabaseExtensions.cs new file mode 100644 index 0000000..4b04314 --- /dev/null +++ b/FireBase/Offline/DatabaseExtensions.cs @@ -0,0 +1,195 @@ +namespace Firebase.Database.Offline +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq.Expressions; + using System.Reflection; + using Firebase.Database.Query; + + public static class DatabaseExtensions + { + /// + /// Create new instances of the . + /// + /// Type of elements. + /// Custom string which will get appended to the file name. + /// Optional custom root element of received json items. + /// Realtime streaming options. + /// Specifies what strategy should be used for initial pulling of server data. + /// Specifies whether changed items should actually be pushed to the server. It this is false, then Put / Post / Delete will not affect server data. + /// The . + public static RealtimeDatabase AsRealtimeDatabase(this ChildQuery query, string filenameModifier = "", string elementRoot = "", StreamingOptions streamingOptions = StreamingOptions.LatestOnly, InitialPullStrategy initialPullStrategy = InitialPullStrategy.MissingOnly, bool pushChanges = true) + where T : class + { + return new RealtimeDatabase(query, elementRoot, query.Client.Options.OfflineDatabaseFactory, filenameModifier, streamingOptions, initialPullStrategy, pushChanges); + } + + /// + /// Create new instances of the . + /// + /// Type of elements. + /// Type of the custom to use. + /// Custom string which will get appended to the file name. + /// Optional custom root element of received json items. + /// Realtime streaming options. + /// Specifies what strategy should be used for initial pulling of server data. + /// Specifies whether changed items should actually be pushed to the server. It this is false, then Put / Post / Delete will not affect server data. + /// The . + public static RealtimeDatabase AsRealtimeDatabase(this ChildQuery query, string filenameModifier = "", string elementRoot = "", StreamingOptions streamingOptions = StreamingOptions.LatestOnly, InitialPullStrategy initialPullStrategy = InitialPullStrategy.MissingOnly, bool pushChanges = true) + where T : class + where TSetHandler : ISetHandler, new() + { + return new RealtimeDatabase(query, elementRoot, query.Client.Options.OfflineDatabaseFactory, filenameModifier, streamingOptions, initialPullStrategy, pushChanges, Activator.CreateInstance()); + } + + /// + /// Overwrites existing object with given key leaving any missing properties intact in firebase. + /// + /// The key. + /// The object to set. + /// Indicates whether the item should be synced online. + /// The priority. Objects with higher priority will be synced first. Higher number indicates higher priority. + public static void Patch(this RealtimeDatabase db, string key, T obj, bool syncOnline = true, int priority = 1) + where T: class + { + db.Set(key, obj, syncOnline ? SyncOptions.Patch : SyncOptions.None, priority); + } + + /// + /// Overwrites existing object with given key. + /// + /// The key. + /// The object to set. + /// Indicates whether the item should be synced online. + /// The priority. Objects with higher priority will be synced first. Higher number indicates higher priority. + public static void Put(this RealtimeDatabase db, string key, T obj, bool syncOnline = true, int priority = 1) + where T: class + { + db.Set(key, obj, syncOnline ? SyncOptions.Put : SyncOptions.None, priority); + } + + /// + /// Adds a new entity to the Database. + /// + /// The object to add. + /// Indicates whether the item should be synced online. + /// The priority. Objects with higher priority will be synced first. Higher number indicates higher priority. + /// The generated key for this object. + public static string Post(this RealtimeDatabase db, T obj, bool syncOnline = true, int priority = 1) + where T: class + { + var key = FirebaseKeyGenerator.Next(); + + db.Set(key, obj, syncOnline ? SyncOptions.Put : SyncOptions.None, priority); + + return key; + } + + /// + /// Deletes the entity with the given key. + /// + /// The key. + /// Indicates whether the item should be synced online. + /// The priority. Objects with higher priority will be synced first. Higher number indicates higher priority. + public static void Delete(this RealtimeDatabase db, string key, bool syncOnline = true, int priority = 1) + where T: class + { + db.Set(key, null, syncOnline ? SyncOptions.Put : SyncOptions.None, priority); + } + + /// + /// Do a Put for a nested property specified by of an object with key . + /// + /// Type of the root elements. + /// Type of the property being modified + /// Database instance. + /// Key of the root element to modify. + /// Expression on the root element leading to target value to modify. + /// Value to put. + /// Indicates whether the item should be synced online. + /// The priority. Objects with higher priority will be synced first. Higher number indicates higher priority. + public static void Put(this RealtimeDatabase db, string key, Expression> propertyExpression, TProperty value, bool syncOnline = true, int priority = 1) + where T: class + { + db.Set(key, propertyExpression, value, syncOnline ? SyncOptions.Put : SyncOptions.None, priority); + } + + /// + /// Do a Patch for a nested property specified by of an object with key . + /// + /// Type of the root elements. + /// Type of the property being modified + /// Database instance. + /// Key of the root element to modify. + /// Expression on the root element leading to target value to modify. + /// Value to patch. + /// Indicates whether the item should be synced online. + /// The priority. Objects with higher priority will be synced first. Higher number indicates higher priority. + public static void Patch(this RealtimeDatabase db, string key, Expression> propertyExpression, TProperty value, bool syncOnline = true, int priority = 1) + where T: class + { + db.Set(key, propertyExpression, value, syncOnline ? SyncOptions.Patch : SyncOptions.None, priority); + } + + /// + /// Delete a nested property specified by of an object with key . This basically does a Put with null value. + /// + /// Type of the root elements. + /// Type of the property being modified + /// Database instance. + /// Key of the root element to modify. + /// Expression on the root element leading to target value to modify. + /// Value to put. + /// Indicates whether the item should be synced online. + /// The priority. Objects with higher priority will be synced first. Higher number indicates higher priority. + public static void Delete(this RealtimeDatabase db, string key, Expression> propertyExpression, bool syncOnline = true, int priority = 1) + where T: class + where TProperty: class + { + db.Set(key, propertyExpression, null, syncOnline ? SyncOptions.Put : SyncOptions.None, priority); + } + + /// + /// Post a new entity into the nested dictionary specified by of an object with key . + /// The key of the new entity is automatically generated. + /// + /// Type of the root elements. + /// Type of the dictionary being modified + /// Type of the value within the dictionary being modified + /// Database instance. + /// Key of the root element to modify. + /// Expression on the root element leading to target value to modify. + /// Value to put. + /// Indicates whether the item should be synced online. + /// The priority. Objects with higher priority will be synced first. Higher number indicates higher priority. + public static void Post(this RealtimeDatabase db, string key, Expression> propertyExpression, TProperty value, bool syncOnline = true, int priority = 1) + where T: class + where TSelector: IDictionary + { + var nextKey = FirebaseKeyGenerator.Next(); + var expression = Expression.Lambda>(Expression.Call(propertyExpression.Body, typeof(TSelector).GetRuntimeMethod("get_Item", new[] { typeof(string) }), Expression.Constant(nextKey)), propertyExpression.Parameters); + db.Set(key, expression, value, syncOnline ? SyncOptions.Put : SyncOptions.None, priority); + } + + /// + /// Delete an entity with key in the nested dictionary specified by of an object with key . + /// The key of the new entity is automatically generated. + /// + /// Type of the root elements. + /// Type of the dictionary being modified + /// Type of the value within the dictionary being modified + /// Database instance. + /// Key of the root element to modify. + /// Expression on the root element leading to target value to modify. + /// Key within the nested dictionary to delete. + /// Indicates whether the item should be synced online. + /// The priority. Objects with higher priority will be synced first. Higher number indicates higher priority. + public static void Delete(this RealtimeDatabase db, string key, Expression>> propertyExpression, string dictionaryKey, bool syncOnline = true, int priority = 1) + where T: class + { + var expression = Expression.Lambda>(Expression.Call(propertyExpression.Body, typeof(IDictionary).GetRuntimeMethod("get_Item", new[] { typeof(string) }), Expression.Constant(dictionaryKey)), propertyExpression.Parameters); + db.Set(key, expression, null, syncOnline ? SyncOptions.Put : SyncOptions.None, priority); + } + } +} diff --git a/FireBase/Offline/ISetHandler.cs b/FireBase/Offline/ISetHandler.cs new file mode 100644 index 0000000..477c36b --- /dev/null +++ b/FireBase/Offline/ISetHandler.cs @@ -0,0 +1,11 @@ +namespace Firebase.Database.Offline +{ + using Firebase.Database.Query; + + using System.Threading.Tasks; + + public interface ISetHandler + { + Task SetAsync(ChildQuery query, string key, OfflineEntry entry); + } +} diff --git a/FireBase/Offline/InitialPullStrategy.cs b/FireBase/Offline/InitialPullStrategy.cs new file mode 100644 index 0000000..70f6b8c --- /dev/null +++ b/FireBase/Offline/InitialPullStrategy.cs @@ -0,0 +1,23 @@ +namespace Firebase.Database.Offline +{ + /// + /// Specifies the strategy for initial pull of server data. + /// + public enum InitialPullStrategy + { + /// + /// Don't pull anything. + /// + None, + + /// + /// Pull only what isn't already stored offline. + /// + MissingOnly, + + /// + /// Pull everything that exists on the server. + /// + Everything, + } +} diff --git a/FireBase/Offline/Internals/MemberAccessVisitor.cs b/FireBase/Offline/Internals/MemberAccessVisitor.cs new file mode 100644 index 0000000..1f7cb11 --- /dev/null +++ b/FireBase/Offline/Internals/MemberAccessVisitor.cs @@ -0,0 +1,51 @@ +namespace Firebase.Database.Offline.Internals +{ + using System.Collections.Generic; + using System.Linq.Expressions; + using System.Reflection; + + using Newtonsoft.Json; + + public class MemberAccessVisitor : ExpressionVisitor + { + private readonly IList propertyNames = new List(); + + private bool wasDictionaryAccess; + + public IEnumerable PropertyNames => this.propertyNames; + + public MemberAccessVisitor() + { + } + + public override Expression Visit(Expression expr) + { + if (expr?.NodeType == ExpressionType.MemberAccess) + { + if (this.wasDictionaryAccess) + { + this.wasDictionaryAccess = false; + } + else + { + var memberExpr = (MemberExpression)expr; + var jsonAttr = memberExpr.Member.GetCustomAttribute(); + + this.propertyNames.Add(jsonAttr?.PropertyName ?? memberExpr.Member.Name); + } + } + else if (expr?.NodeType == ExpressionType.Call) + { + var callExpr = (MethodCallExpression)expr; + if (callExpr.Method.Name == "get_Item" && callExpr.Arguments.Count == 1) + { + var e = Expression.Lambda(callExpr.Arguments[0]).Compile(); + this.propertyNames.Add(e.DynamicInvoke().ToString()); + this.wasDictionaryAccess = callExpr.Arguments[0].NodeType == ExpressionType.MemberAccess; + } + } + + return base.Visit(expr); + } + } +} diff --git a/FireBase/Offline/OfflineCacheAdapter.cs b/FireBase/Offline/OfflineCacheAdapter.cs new file mode 100644 index 0000000..a3761a0 --- /dev/null +++ b/FireBase/Offline/OfflineCacheAdapter.cs @@ -0,0 +1,165 @@ +namespace Firebase.Database.Offline +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + + internal class OfflineCacheAdapter : IDictionary, IDictionary + { + private readonly IDictionary database; + + public OfflineCacheAdapter(IDictionary database) + { + this.database = database; + } + + public void CopyTo(Array array, int index) + { + throw new NotImplementedException(); + } + + public int Count => this.database.Count; + + public bool IsSynchronized { get; } + + public object SyncRoot { get; } + + public bool IsReadOnly => this.database.IsReadOnly; + + object IDictionary.this[object key] + { + get + { + return this.database[key.ToString()].Deserialize(); + } + + set + { + var keyString = key.ToString(); + if (this.database.ContainsKey(keyString)) + { + this.database[keyString] = new OfflineEntry(keyString, value, this.database[keyString].Priority, this.database[keyString].SyncOptions); + } + else + { + this.database[keyString] = new OfflineEntry(keyString, value, 1, SyncOptions.None); + } + } + } + + public ICollection Keys => this.database.Keys; + + ICollection IDictionary.Values { get; } + + ICollection IDictionary.Keys { get; } + + public ICollection Values => this.database.Values.Select(o => o.Deserialize()).ToList(); + + public T this[string key] + { + get + { + return this.database[key].Deserialize(); + } + + set + { + if (this.database.ContainsKey(key)) + { + this.database[key] = new OfflineEntry(key, value, this.database[key].Priority, this.database[key].SyncOptions); + } + else + { + this.database[key] = new OfflineEntry(key, value, 1, SyncOptions.None); + } + } + } + + public bool Contains(object key) + { + return this.ContainsKey(key.ToString()); + } + + IDictionaryEnumerator IDictionary.GetEnumerator() + { + throw new NotImplementedException(); + } + + public void Remove(object key) + { + this.Remove(key.ToString()); + } + + public bool IsFixedSize => false; + + public IEnumerator> GetEnumerator() + { + return this.database.Select(d => new KeyValuePair(d.Key, d.Value.Deserialize())).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + public void Add(KeyValuePair item) + { + this.Add(item.Key, item.Value); + } + + public void Add(object key, object value) + { + this.Add(key.ToString(), (T)value); + } + + public void Clear() + { + this.database.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return this.ContainsKey(item.Key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public bool Remove(KeyValuePair item) + { + return this.database.Remove(item.Key); + } + + public void Add(string key, T value) + { + this.database.Add(key, new OfflineEntry(key, value, 1, SyncOptions.None)); + } + + public bool ContainsKey(string key) + { + return this.database.ContainsKey(key); + } + + public bool Remove(string key) + { + return this.database.Remove(key); + } + + public bool TryGetValue(string key, out T value) + { + OfflineEntry val; + + if (this.database.TryGetValue(key, out val)) + { + value = val.Deserialize(); + return true; + } + + value = default(T); + return false; + } + } +} diff --git a/FireBase/Offline/OfflineDatabase.cs b/FireBase/Offline/OfflineDatabase.cs new file mode 100644 index 0000000..9cebf9c --- /dev/null +++ b/FireBase/Offline/OfflineDatabase.cs @@ -0,0 +1,201 @@ +namespace Firebase.Database.Offline +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.IO; + using System.Linq; + + using LiteDB; + + /// + /// The offline database. + /// + public class OfflineDatabase : IDictionary + { + private readonly LiteRepository db; + private readonly IDictionary cache; + + /// + /// Initializes a new instance of the class. + /// + /// The item type which is used to determine the database file name. + /// Custom string which will get appended to the file name. + public OfflineDatabase(Type itemType, string filenameModifier) + { + var fullName = this.GetFileName(itemType.ToString()); + if(fullName.Length > 100) + { + fullName = fullName.Substring(0, 100); + } + + BsonMapper mapper = BsonMapper.Global; + mapper.Entity().Id(o => o.Key); + + string root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + string filename = fullName + filenameModifier + ".db"; + var path = Path.Combine(root, filename); + this.db = new LiteRepository(new LiteDatabase(path, mapper)); + + this.cache = db.Database.GetCollection().FindAll() + .ToDictionary(o => o.Key, o => o); + } + + /// + /// Gets the number of elements contained in the . + /// + /// The number of elements contained in the . + public int Count => this.cache.Count; + + /// + /// Gets a value indicating whether this is a read-only collection. + /// + public bool IsReadOnly => this.cache.IsReadOnly; + + /// + /// Gets an containing the keys of the . + /// + /// An containing the keys of the object that implements . + public ICollection Keys => this.cache.Keys; + + /// + /// Gets an containing the values in the . + /// + /// An containing the values in the object that implements . + public ICollection Values => this.cache.Values; + + /// + /// Gets or sets the element with the specified key. + /// + /// The key of the element to get or set. + /// The element with the specified key. + public OfflineEntry this[string key] + { + get + { + return this.cache[key]; + } + + set + { + this.cache[key] = value; + this.db.Upsert(value); + } + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that can be used to iterate through the collection. + public IEnumerator> GetEnumerator() + { + return this.cache.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + /// + /// Adds an item to the . + /// + /// The object to add to the . + public void Add(KeyValuePair item) + { + this.Add(item.Key, item.Value); + } + + /// + /// Removes all items from the . + /// + public void Clear() + { + this.cache.Clear(); + this.db.Delete(Query.All()); + } + + /// + /// Determines whether the contains a specific value. + /// + /// The object to locate in the . + /// True if is found in the ; otherwise, false. + public bool Contains(KeyValuePair item) + { + return this.ContainsKey(item.Key); + } + + /// + /// Copies the elements of the to an , starting at a particular index. + /// + /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. + /// The zero-based index in at which copying begins. + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + this.cache.CopyTo(array, arrayIndex); + } + + /// + /// Removes the first occurrence of a specific object from the . + /// + /// The object to remove from the . + /// True if was successfully removed from the ; otherwise, false. This method also returns false if is not found in the original . + public bool Remove(KeyValuePair item) + { + return this.Remove(item.Key); + } + + /// + /// Determines whether the contains an element with the specified key. + /// + /// The key to locate in the . + /// True if the contains an element with the key; otherwise, false. + public bool ContainsKey(string key) + { + return this.cache.ContainsKey(key); + } + + /// + /// Adds an element with the provided key and value to the . + /// + /// The object to use as the key of the element to add. + /// The object to use as the value of the element to add. + public void Add(string key, OfflineEntry value) + { + this.cache.Add(key, value); + this.db.Insert(value); + } + + /// + /// Removes the element with the specified key from the . + /// + /// The key of the element to remove. + /// True if the element is successfully removed; otherwise, false. This method also returns false if was not found in the original . + public bool Remove(string key) + { + this.cache.Remove(key); + return this.db.Delete(key); + } + + /// + /// Gets the value associated with the specified key. + /// + /// The key whose value to get.When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. + /// True if the object that implements contains an element with the specified key; otherwise, false. + public bool TryGetValue(string key, out OfflineEntry value) + { + return this.cache.TryGetValue(key, out value); + } + + private string GetFileName(string fileName) + { + var invalidChars = new[] { '`', '[', ',', '=' }; + foreach(char c in invalidChars.Concat(System.IO.Path.GetInvalidFileNameChars()).Distinct()) + { + fileName = fileName.Replace(c, '_'); + } + + return fileName; + } + } +} diff --git a/FireBase/Offline/OfflineEntry.cs b/FireBase/Offline/OfflineEntry.cs new file mode 100644 index 0000000..3b862cb --- /dev/null +++ b/FireBase/Offline/OfflineEntry.cs @@ -0,0 +1,116 @@ +namespace Firebase.Database.Offline +{ + using System; + + using Newtonsoft.Json; + + /// + /// Represents an object stored in offline storage. + /// + public class OfflineEntry + { + private object dataInstance; + + /// + /// Initializes a new instance of the class with an already serialized object. + /// + /// The key. + /// The object. + /// The priority. Objects with higher priority will be synced first. Higher number indicates higher priority. + /// The sync options. + public OfflineEntry(string key, object obj, string data, int priority, SyncOptions syncOptions, bool isPartial = false) + { + this.Key = key; + this.Priority = priority; + this.Data = data; + this.Timestamp = DateTime.UtcNow; + this.SyncOptions = syncOptions; + this.IsPartial = isPartial; + + this.dataInstance = obj; + } + + /// + /// Initializes a new instance of the class. + /// + /// The key. + /// The object. + /// The priority. Objects with higher priority will be synced first. Higher number indicates higher priority. + /// The sync options. + public OfflineEntry(string key, object obj, int priority, SyncOptions syncOptions, bool isPartial = false) + : this(key, obj, JsonConvert.SerializeObject(obj), priority, syncOptions, isPartial) + { + } + + /// + /// Initializes a new instance of the class. + /// + public OfflineEntry() + { + } + + /// + /// Gets or sets the key of this entry. + /// + public string Key + { + get; + set; + } + + /// + /// Gets or sets the priority. Objects with higher priority will be synced first. Higher number indicates higher priority. + /// + public int Priority + { + get; + set; + } + + /// + /// Gets or sets the timestamp when this entry was last touched. + /// + public DateTime Timestamp + { + get; + set; + } + + /// + /// Gets or sets the which define what sync state this entry is in. + /// + public SyncOptions SyncOptions + { + get; + set; + } + + /// + /// Gets or sets serialized JSON data. + /// + public string Data + { + get; + set; + } + + /// + /// Specifies whether this is only a partial object. + /// + public bool IsPartial + { + get; + set; + } + + /// + /// Deserializes into . The result is cached. + /// + /// Type of object to deserialize into. + /// Instance of . + public T Deserialize() + { + return (T)(this.dataInstance ?? (this.dataInstance = JsonConvert.DeserializeObject(this.Data))); + } + } +} diff --git a/FireBase/Offline/RealtimeDatabase.cs b/FireBase/Offline/RealtimeDatabase.cs new file mode 100644 index 0000000..61a7010 --- /dev/null +++ b/FireBase/Offline/RealtimeDatabase.cs @@ -0,0 +1,459 @@ +namespace Firebase.Database.Offline +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reactive.Linq; + using System.Reactive.Subjects; + using System.Threading; + using System.Threading.Tasks; + + using Firebase.Database.Extensions; + using Firebase.Database.Query; + using Firebase.Database.Streaming; + using System.Reactive.Threading.Tasks; + using System.Linq.Expressions; + using Internals; + using Newtonsoft.Json; + using System.Reflection; + using System.Reactive.Disposables; + + /// + /// The real-time Database which synchronizes online and offline data. + /// + /// Type of entities. + public partial class RealtimeDatabase : IDisposable where T : class + { + private readonly ChildQuery childQuery; + private readonly string elementRoot; + private readonly StreamingOptions streamingOptions; + private readonly Subject> subject; + private readonly InitialPullStrategy initialPullStrategy; + private readonly bool pushChanges; + private readonly FirebaseCache firebaseCache; + + private bool isSyncRunning; + private IObservable> observable; + private FirebaseSubscription firebaseSubscription; + + /// + /// Initializes a new instance of the class. + /// + /// The child query. + /// The element Root. + /// The offline database factory. + /// Custom string which will get appended to the file name. + /// Specifies whether changes should be streamed from the server. + /// Specifies if everything should be pull from the online storage on start. It only makes sense when is set to true. + /// Specifies whether changed items should actually be pushed to the server. If this is false, then Put / Post / Delete will not affect server data. + public RealtimeDatabase(ChildQuery childQuery, string elementRoot, Func> offlineDatabaseFactory, string filenameModifier, StreamingOptions streamingOptions, InitialPullStrategy initialPullStrategy, bool pushChanges, ISetHandler setHandler = null) + { + this.childQuery = childQuery; + this.elementRoot = elementRoot; + this.streamingOptions = streamingOptions; + this.initialPullStrategy = initialPullStrategy; + this.pushChanges = pushChanges; + this.Database = offlineDatabaseFactory(typeof(T), filenameModifier); + this.firebaseCache = new FirebaseCache(new OfflineCacheAdapter(this.Database)); + this.subject = new Subject>(); + + this.PutHandler = setHandler ?? new SetHandler(); + + this.isSyncRunning = true; + Task.Factory.StartNew(this.SynchronizeThread, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } + + /// + /// Event raised whenever an exception is thrown in the synchronization thread. Exception thrown in there are swallowed, so this event is the only way to get to them. + /// + public event EventHandler SyncExceptionThrown; + + /// + /// Gets the backing Database. + /// + public IDictionary Database + { + get; + private set; + } + + public ISetHandler PutHandler + { + private get; + set; + } + + /// + /// Overwrites existing object with given key. + /// + /// The key. + /// The object to set. + /// Indicates whether the item should be synced online. + /// The priority. Objects with higher priority will be synced first. Higher number indicates higher priority. + public void Set(string key, T obj, SyncOptions syncOptions, int priority = 1) + { + this.SetAndRaise(key, new OfflineEntry(key, obj, priority, syncOptions)); + } + + public void Set(string key, Expression> propertyExpression, object value, SyncOptions syncOptions, int priority = 1) + { + var fullKey = this.GenerateFullKey(key, propertyExpression, syncOptions); + var serializedObject = JsonConvert.SerializeObject(value).Trim('"', '\\'); + + if (fullKey.Item3) + { + if (typeof(TProperty) != typeof(string) || value == null) + { + // don't escape non-string primitives and null; + serializedObject = $"{{ \"{fullKey.Item2}\" : {serializedObject} }}"; + } + else + { + serializedObject = $"{{ \"{fullKey.Item2}\" : \"{serializedObject}\" }}"; + } + } + + var setObject = this.firebaseCache.PushData("/" + fullKey.Item1, serializedObject).First(); + + if (!this.Database.ContainsKey(key) || this.Database[key].SyncOptions != SyncOptions.Patch && this.Database[key].SyncOptions != SyncOptions.Put) + { + this.Database[fullKey.Item1] = new OfflineEntry(fullKey.Item1, value, serializedObject, priority, syncOptions, true); + } + + this.subject.OnNext(new FirebaseEvent(key, setObject.Object, setObject == null ? FirebaseEventType.Delete : FirebaseEventType.InsertOrUpdate, FirebaseEventSource.Offline)); + } + + /// + /// Fetches an object with the given key and adds it to the Database. + /// + /// The key. + /// The priority. Objects with higher priority will be synced first. Higher number indicates higher priority. + public void Pull(string key, int priority = 1) + { + if (!this.Database.ContainsKey(key)) + { + this.Database[key] = new OfflineEntry(key, null, priority, SyncOptions.Pull); + } + else if (this.Database[key].SyncOptions == SyncOptions.None) + { + // pull only if push isn't pending + this.Database[key].SyncOptions = SyncOptions.Pull; + } + } + + /// + /// Fetches everything from the remote database. + /// + public async Task PullAsync() + { + var existingEntries = await this.childQuery + .OnceAsync() + .ToObservable() + .RetryAfterDelay>, FirebaseException>( + this.childQuery.Client.Options.SyncPeriod, + ex => ex.StatusCode == System.Net.HttpStatusCode.OK) // OK implies the request couldn't complete due to network error. + .Select(e => this.ResetDatabaseFromInitial(e, false)) + .SelectMany(e => e) + .Do(e => + { + this.Database[e.Key] = new OfflineEntry(e.Key, e.Object, 1, SyncOptions.None); + this.subject.OnNext(new FirebaseEvent(e.Key, e.Object, FirebaseEventType.InsertOrUpdate, FirebaseEventSource.OnlinePull)); + }) + .ToList(); + + // Remove items not stored online + foreach (var item in this.Database.Keys.Except(existingEntries.Select(f => f.Key)).ToList()) + { + this.Database.Remove(item); + this.subject.OnNext(new FirebaseEvent(item, null, FirebaseEventType.Delete, FirebaseEventSource.OnlinePull)); + } + } + + /// + /// Retrieves all offline items currently stored in local database. + /// + public IEnumerable> Once() + { + return this.Database + .Where(kvp => !string.IsNullOrEmpty(kvp.Value.Data) && kvp.Value.Data != "null" && !kvp.Value.IsPartial) + .Select(kvp => new FirebaseObject(kvp.Key, kvp.Value.Deserialize())) + .ToList(); + } + + /// + /// Starts observing the real-time Database. Events will be fired both when change is done locally and remotely. + /// + /// Stream of . + public IObservable> AsObservable() + { + if (!this.isSyncRunning) + { + this.isSyncRunning = true; + Task.Factory.StartNew(this.SynchronizeThread, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } + + if (this.observable == null) + { + var initialData = Observable.Return(FirebaseEvent.Empty(FirebaseEventSource.Offline)); + if(this.Database.TryGetValue(this.elementRoot, out OfflineEntry oe)) + { + initialData = Observable.Return(oe) + .Where(offlineEntry => !string.IsNullOrEmpty(offlineEntry.Data) && offlineEntry.Data != "null" && !offlineEntry.IsPartial) + .Select(offlineEntry => new FirebaseEvent(offlineEntry.Key, offlineEntry.Deserialize(), FirebaseEventType.InsertOrUpdate, FirebaseEventSource.Offline)); + } + else if(this.Database.Count > 0) + { + initialData = this.Database + .Where(kvp => !string.IsNullOrEmpty(kvp.Value.Data) && kvp.Value.Data != "null" && !kvp.Value.IsPartial) + .Select(kvp => new FirebaseEvent(kvp.Key, kvp.Value.Deserialize(), FirebaseEventType.InsertOrUpdate, FirebaseEventSource.Offline)) + .ToList() + .ToObservable(); + } + + this.observable = initialData + .Merge(this.subject) + .Merge(this.GetInitialPullObservable() + .RetryAfterDelay>, FirebaseException>( + this.childQuery.Client.Options.SyncPeriod, + ex => ex.StatusCode == System.Net.HttpStatusCode.OK) // OK implies the request couldn't complete due to network error. + .Select(e => this.ResetDatabaseFromInitial(e)) + .SelectMany(e => e) + .Do(this.SetObjectFromInitialPull) + .Select(e => new FirebaseEvent(e.Key, e.Object, e.Object == null ? FirebaseEventType.Delete : FirebaseEventType.InsertOrUpdate, FirebaseEventSource.OnlineInitial)) + .Concat(Observable.Create>(observer => this.InitializeStreamingSubscription(observer)))) + .Do(next => { }, e => this.observable = null, () => this.observable = null) + .Replay() + .RefCount(); + } + + return this.observable; + } + + public void Dispose() + { + this.subject.OnCompleted(); + this.firebaseSubscription?.Dispose(); + } + + private IReadOnlyCollection> ResetDatabaseFromInitial(IReadOnlyCollection> collection, bool onlyWhenInitialEverything = true) + { + if (onlyWhenInitialEverything && this.initialPullStrategy != InitialPullStrategy.Everything) + { + return collection; + } + + // items which are in local db, but not in the online collection + var extra = this.Once() + .Select(f => f.Key) + .Except(collection.Select(c => c.Key)) + .Select(k => new FirebaseObject(k, null)); + + return collection.Concat(extra).ToList(); + } + + private void SetObjectFromInitialPull(FirebaseObject e) + { + // set object with no sync only if it doesn't exist yet + // and the InitialPullStrategy != Everything + // this attempts to deal with scenario when you are offline, have local changes and go online + // in this case having the InitialPullStrategy set to everything would basically purge all local changes + if (!this.Database.ContainsKey(e.Key) || this.Database[e.Key].SyncOptions == SyncOptions.None || this.Database[e.Key].SyncOptions == SyncOptions.Pull || this.initialPullStrategy != InitialPullStrategy.Everything) + { + this.Database[e.Key] = new OfflineEntry(e.Key, e.Object, 1, SyncOptions.None); + } + } + + private IObservable>> GetInitialPullObservable() + { + FirebaseQuery query; + switch (this.initialPullStrategy) + { + case InitialPullStrategy.MissingOnly: + query = this.childQuery.OrderByKey().StartAt(() => this.GetLatestKey()); + break; + case InitialPullStrategy.Everything: + query = this.childQuery; + break; + case InitialPullStrategy.None: + default: + return Observable.Empty>>(); + } + + if (string.IsNullOrWhiteSpace(this.elementRoot)) + { + return Observable.Defer(() => query.OnceAsync().ToObservable()); + } + + // there is an element root, which indicates the target location is not a collection but a single element + return Observable.Defer(async () => Observable.Return(await query.OnceSingleAsync()).Select(e => new[] { new FirebaseObject(this.elementRoot, e) })); + } + + private IDisposable InitializeStreamingSubscription(IObserver> observer) + { + var completeDisposable = Disposable.Create(() => this.isSyncRunning = false); + + switch (this.streamingOptions) + { + case StreamingOptions.LatestOnly: + // stream since the latest key + var queryLatest = this.childQuery.OrderByKey().StartAt(() => this.GetLatestKey()); + this.firebaseSubscription = new FirebaseSubscription(observer, queryLatest, this.elementRoot, this.firebaseCache); + this.firebaseSubscription.ExceptionThrown += this.StreamingExceptionThrown; + + return new CompositeDisposable(this.firebaseSubscription.Run(), completeDisposable); + case StreamingOptions.Everything: + // stream everything + var queryAll = this.childQuery; + this.firebaseSubscription = new FirebaseSubscription(observer, queryAll, this.elementRoot, this.firebaseCache); + this.firebaseSubscription.ExceptionThrown += this.StreamingExceptionThrown; + + return new CompositeDisposable(this.firebaseSubscription.Run(), completeDisposable); + default: + break; + } + + return completeDisposable; + } + + private void SetAndRaise(string key, OfflineEntry obj, FirebaseEventSource eventSource = FirebaseEventSource.Offline) + { + this.Database[key] = obj; + this.subject.OnNext(new FirebaseEvent(key, obj?.Deserialize(), string.IsNullOrEmpty(obj?.Data) || obj?.Data == "null" ? FirebaseEventType.Delete : FirebaseEventType.InsertOrUpdate, eventSource)); + } + + private async void SynchronizeThread() + { + while (this.isSyncRunning) + { + try + { + var validEntries = this.Database.Where(e => e.Value != null); + await this.PullEntriesAsync(validEntries.Where(kvp => kvp.Value.SyncOptions == SyncOptions.Pull)); + + if (this.pushChanges) + { + await this.PushEntriesAsync(validEntries.Where(kvp => kvp.Value.SyncOptions == SyncOptions.Put || kvp.Value.SyncOptions == SyncOptions.Patch)); + } + } + catch (Exception ex) + { + this.SyncExceptionThrown?.Invoke(this, new ExceptionEventArgs(ex)); + } + + await Task.Delay(this.childQuery.Client.Options.SyncPeriod); + } + } + + private string GetLatestKey() + { + var key = this.Database.OrderBy(o => o.Key, StringComparer.Ordinal).LastOrDefault().Key ?? string.Empty; + + if (!string.IsNullOrWhiteSpace(key)) + { + key = key.Substring(0, key.Length - 1) + (char)(key[key.Length - 1] + 1); + } + + return key; + } + + private async Task PushEntriesAsync(IEnumerable> pushEntries) + { + var groups = pushEntries.GroupBy(pair => pair.Value.Priority).OrderByDescending(kvp => kvp.Key).ToList(); + + foreach (var group in groups) + { + var tasks = group.OrderBy(kvp => kvp.Value.IsPartial).Select(kvp => + kvp.Value.IsPartial ? + this.ResetSyncAfterPush(this.PutHandler.SetAsync(this.childQuery, kvp.Key, kvp.Value), kvp.Key) : + this.ResetSyncAfterPush(this.PutHandler.SetAsync(this.childQuery, kvp.Key, kvp.Value), kvp.Key, kvp.Value.Deserialize())); + + try + { + await Task.WhenAll(tasks).WithAggregateException(); + } + catch (Exception ex) + { + this.SyncExceptionThrown?.Invoke(this, new ExceptionEventArgs(ex)); + } + } + } + + private async Task PullEntriesAsync(IEnumerable> pullEntries) + { + var taskGroups = pullEntries.GroupBy(pair => pair.Value.Priority).OrderByDescending(kvp => kvp.Key); + + foreach (var group in taskGroups) + { + var tasks = group.Select(pair => this.ResetAfterPull(this.childQuery.Child(pair.Key == this.elementRoot ? string.Empty : pair.Key).OnceSingleAsync(), pair.Key, pair.Value)); + + try + { + await Task.WhenAll(tasks).WithAggregateException(); + } + catch (Exception ex) + { + this.SyncExceptionThrown?.Invoke(this, new ExceptionEventArgs(ex)); + } + } + } + + private async Task ResetAfterPull(Task task, string key, OfflineEntry entry) + { + await task; + this.SetAndRaise(key, new OfflineEntry(key, task.Result, entry.Priority, SyncOptions.None), FirebaseEventSource.OnlinePull); + } + + private async Task ResetSyncAfterPush(Task task, string key, T obj) + { + await this.ResetSyncAfterPush(task, key); + + if (this.streamingOptions == StreamingOptions.None) + { + this.subject.OnNext(new FirebaseEvent(key, obj, obj == null ? FirebaseEventType.Delete : FirebaseEventType.InsertOrUpdate, FirebaseEventSource.OnlinePush)); + } + } + + private async Task ResetSyncAfterPush(Task task, string key) + { + await task; + this.ResetSyncOptions(key); + } + + private void ResetSyncOptions(string key) + { + var item = this.Database[key]; + + if (item.IsPartial) + { + this.Database.Remove(key); + } + else + { + item.SyncOptions = SyncOptions.None; + this.Database[key] = item; + } + } + + private void StreamingExceptionThrown(object sender, ExceptionEventArgs e) + { + this.SyncExceptionThrown?.Invoke(this, new ExceptionEventArgs(e.Exception)); + } + + private Tuple GenerateFullKey(string key, Expression> propertyGetter, SyncOptions syncOptions) + { + var visitor = new MemberAccessVisitor(); + visitor.Visit(propertyGetter); + var propertyType = typeof(TProperty).GetTypeInfo(); + var prefix = key == string.Empty ? string.Empty : key + "/"; + + // primitive types + if (syncOptions == SyncOptions.Patch && (propertyType.IsPrimitive || Nullable.GetUnderlyingType(typeof(TProperty)) != null || typeof(TProperty) == typeof(string))) + { + return Tuple.Create(prefix + string.Join("/", visitor.PropertyNames.Skip(1).Reverse()), visitor.PropertyNames.First(), true); + } + + return Tuple.Create(prefix + string.Join("/", visitor.PropertyNames.Reverse()), visitor.PropertyNames.First(), false); + } + + } +} diff --git a/FireBase/Offline/SetHandler.cs b/FireBase/Offline/SetHandler.cs new file mode 100644 index 0000000..1efa7b6 --- /dev/null +++ b/FireBase/Offline/SetHandler.cs @@ -0,0 +1,24 @@ +namespace Firebase.Database.Offline +{ + using Firebase.Database.Query; + + using System.Threading.Tasks; + + public class SetHandler : ISetHandler + { + public virtual async Task SetAsync(ChildQuery query, string key, OfflineEntry entry) + { + using (var child = query.Child(key)) + { + if (entry.SyncOptions == SyncOptions.Put) + { + await child.PutAsync(entry.Data); + } + else + { + await child.PatchAsync(entry.Data); + } + } + } + } +} diff --git a/FireBase/Offline/StreamingOptions.cs b/FireBase/Offline/StreamingOptions.cs new file mode 100644 index 0000000..9ed4e54 --- /dev/null +++ b/FireBase/Offline/StreamingOptions.cs @@ -0,0 +1,21 @@ +namespace Firebase.Database.Offline +{ + public enum StreamingOptions + { + /// + /// No realtime streaming. + /// + None, + + /// + /// Streaming of only new items - not the existing ones. + /// + LatestOnly, + + /// + /// Streaming of all items. This will also pull all existing items on start, so be mindful about the number of items in your DB. + /// When used, consider not setting the to because you would pointlessly pull everything twice. + /// + Everything + } +} diff --git a/FireBase/Offline/SyncOptions.cs b/FireBase/Offline/SyncOptions.cs new file mode 100644 index 0000000..b2f382a --- /dev/null +++ b/FireBase/Offline/SyncOptions.cs @@ -0,0 +1,28 @@ +namespace Firebase.Database.Offline +{ + /// + /// Specifies type of sync requested for given data. + /// + public enum SyncOptions + { + /// + /// No sync needed for given data. + /// + None, + + /// + /// Data should be pulled from firebase. + /// + Pull, + + /// + /// Data should be put to firebase. + /// + Put, + + /// + /// Data should be patched in firebase. + /// + Patch + } +} diff --git a/FireBase/Query/AuthQuery.cs b/FireBase/Query/AuthQuery.cs new file mode 100644 index 0000000..8a8d3e8 --- /dev/null +++ b/FireBase/Query/AuthQuery.cs @@ -0,0 +1,33 @@ +namespace Firebase.Database.Query +{ + using System; + + /// + /// Represents an auth parameter in firebase query, e.g. "?auth=xyz". + /// + public class AuthQuery : ParameterQuery + { + private readonly Func tokenFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The parent. + /// The authentication token factory. + /// The owner. + public AuthQuery(FirebaseQuery parent, Func tokenFactory, FirebaseClient client) : base(parent, () => client.Options.AsAccessToken ? "access_token" : "auth", client) + { + this.tokenFactory = tokenFactory; + } + + /// + /// Build the url parameter value of this child. + /// + /// The child of this child. + /// The . + protected override string BuildUrlParameter(FirebaseQuery child) + { + return this.tokenFactory(); + } + } +} diff --git a/FireBase/Query/ChildQuery.cs b/FireBase/Query/ChildQuery.cs new file mode 100644 index 0000000..1696ea8 --- /dev/null +++ b/FireBase/Query/ChildQuery.cs @@ -0,0 +1,56 @@ +namespace Firebase.Database.Query +{ + using System; + + /// + /// Firebase query which references the child of current node. + /// + public class ChildQuery : FirebaseQuery + { + private readonly Func pathFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The parent. + /// The path to the child node. + /// The owner. + public ChildQuery(FirebaseQuery parent, Func pathFactory, FirebaseClient client) + : base(parent, client) + { + this.pathFactory = pathFactory; + } + + /// + /// Initializes a new instance of the class. + /// + /// The client. + /// The path to the child node. + public ChildQuery(FirebaseClient client, Func pathFactory) + : this(null, pathFactory, client) + { + } + + /// + /// Build the url segment of this child. + /// + /// The child of this child. + /// The . + protected override string BuildUrlSegment(FirebaseQuery child) + { + var s = this.pathFactory(); + + if (s != string.Empty && !s.EndsWith("/")) + { + s += '/'; + } + + if (!(child is ChildQuery)) + { + return s + ".json"; + } + + return s; + } + } +} diff --git a/FireBase/Query/FilterQuery.cs b/FireBase/Query/FilterQuery.cs new file mode 100644 index 0000000..f9f6271 --- /dev/null +++ b/FireBase/Query/FilterQuery.cs @@ -0,0 +1,81 @@ +namespace Firebase.Database.Query +{ + using System; + using System.Globalization; + + /// + /// Represents a firebase filtering query, e.g. "?LimitToLast=10". + /// + public class FilterQuery : ParameterQuery + { + private readonly Func valueFactory; + private readonly Func doubleValueFactory; + private readonly Func boolValueFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The parent. + /// The filter. + /// The value for filter. + /// The owning client. + public FilterQuery(FirebaseQuery parent, Func filterFactory, Func valueFactory, FirebaseClient client) + : base(parent, filterFactory, client) + { + this.valueFactory = valueFactory; + } + + /// + /// Initializes a new instance of the class. + /// + /// The parent. + /// The filter. + /// The value for filter. + /// The owning client. + public FilterQuery(FirebaseQuery parent, Func filterFactory, Func valueFactory, FirebaseClient client) + : base(parent, filterFactory, client) + { + this.doubleValueFactory = valueFactory; + } + + /// + /// Initializes a new instance of the class. + /// + /// The parent. + /// The filter. + /// The value for filter. + /// The owning client. + public FilterQuery(FirebaseQuery parent, Func filterFactory, Func valueFactory, FirebaseClient client) + : base(parent, filterFactory, client) + { + this.boolValueFactory = valueFactory; + } + + /// + /// The build url parameter. + /// + /// The child. + /// Url parameter part of the resulting path. + protected override string BuildUrlParameter(FirebaseQuery child) + { + if (this.valueFactory != null) + { + if(this.valueFactory() == null) + { + return $"null"; + } + return $"\"{this.valueFactory()}\""; + } + else if (this.doubleValueFactory != null) + { + return this.doubleValueFactory().ToString(CultureInfo.InvariantCulture); + } + else if (this.boolValueFactory != null) + { + return $"{this.boolValueFactory().ToString().ToLower()}"; + } + + return string.Empty; + } + } +} diff --git a/FireBase/Query/FirebaseQuery.cs b/FireBase/Query/FirebaseQuery.cs new file mode 100644 index 0000000..0e1b84a --- /dev/null +++ b/FireBase/Query/FirebaseQuery.cs @@ -0,0 +1,314 @@ +namespace Firebase.Database.Query +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Reactive.Linq; + using System.Threading.Tasks; + + using Firebase.Database.Http; + using Firebase.Database.Offline; + using Firebase.Database.Streaming; + + using Newtonsoft.Json; + using System.Net; + + /// + /// Represents a firebase query. + /// + public abstract class FirebaseQuery : IFirebaseQuery, IDisposable + { + protected TimeSpan DEFAULT_HTTP_CLIENT_TIMEOUT = new TimeSpan(0, 0, 180); + + protected readonly FirebaseQuery Parent; + + private HttpClient client; + + /// + /// Initializes a new instance of the class. + /// + /// The parent of this query. + /// The owning client. + protected FirebaseQuery(FirebaseQuery parent, FirebaseClient client) + { + this.Client = client; + this.Parent = parent; + } + + /// + /// Gets the client. + /// + public FirebaseClient Client + { + get; + } + + /// + /// Queries the firebase server once returning collection of items. + /// + /// Optional timeout value. + /// Type of elements. + /// Collection of holding the entities returned by server. + public async Task>> OnceAsync(TimeSpan? timeout = null) + { + var url = string.Empty; + + try + { + url = await this.BuildUrlAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + throw new FirebaseException("Couldn't build the url", string.Empty, string.Empty, HttpStatusCode.OK, ex); + } + + return await this.GetClient(timeout).GetObjectCollectionAsync(url, Client.Options.JsonSerializerSettings) + .ConfigureAwait(false); + } + + + /// + /// Assumes given query is pointing to a single object of type and retrieves it. + /// + /// Optional timeout value. + /// Type of elements. + /// Single object of type . + public async Task OnceSingleAsync(TimeSpan? timeout = null) + { + var responseData = string.Empty; + var statusCode = HttpStatusCode.OK; + var url = string.Empty; + + try + { + url = await this.BuildUrlAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + throw new FirebaseException("Couldn't build the url", string.Empty, responseData, statusCode, ex); + } + + try + { + var response = await this.GetClient(timeout).GetAsync(url).ConfigureAwait(false); + statusCode = response.StatusCode; + responseData = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + response.Dispose(); + + return JsonConvert.DeserializeObject(responseData, Client.Options.JsonSerializerSettings); + } + catch (Exception ex) + { + throw new FirebaseException(url, string.Empty, responseData, statusCode, ex); + } + } + + /// + /// Starts observing this query watching for changes real time sent by the server. + /// + /// Type of elements. + /// Optional custom root element of received json items. + /// Observable stream of . + public IObservable> AsObservable(EventHandler> exceptionHandler = null, string elementRoot = "") + { + return Observable.Create>(observer => + { + var sub = new FirebaseSubscription(observer, this, elementRoot, new FirebaseCache()); + sub.ExceptionThrown += exceptionHandler; + return sub.Run(); + }); + } + + /// + /// Builds the actual URL of this query. + /// + /// The . + public async Task BuildUrlAsync() + { + // if token factory is present on the parent then use it to generate auth token + if (this.Client.Options.AuthTokenAsyncFactory != null) + { + var token = await this.Client.Options.AuthTokenAsyncFactory().ConfigureAwait(false); + return this.WithAuth(token).BuildUrl(null); + } + + return this.BuildUrl(null); + } + + /// + /// Posts given object to repository. + /// + /// The object. + /// Specifies whether the key should be generated offline instead of online. + /// Optional timeout value. + /// Type of + /// Resulting firebase object with populated key. + public async Task> PostAsync(string data, bool generateKeyOffline = true, TimeSpan? timeout = null) + { + // post generates a new key server-side, while put can be used with an already generated local key + if (generateKeyOffline) + { + var key = FirebaseKeyGenerator.Next(); + await new ChildQuery(this, () => key, this.Client).PutAsync(data).ConfigureAwait(false); + + return new FirebaseObject(key, data); + } + else + { + var c = this.GetClient(timeout); + var sendData = await this.SendAsync(c, data, HttpMethod.Post).ConfigureAwait(false); + var result = JsonConvert.DeserializeObject(sendData, Client.Options.JsonSerializerSettings); + + return new FirebaseObject(result.Name, data); + } + } + + /// + /// Patches data at given location instead of overwriting them. + /// + /// The object. + /// Optional timeout value. + /// Type of + /// The . + public async Task PatchAsync(string data, TimeSpan? timeout = null) + { + var c = this.GetClient(timeout); + + await this.Silent().SendAsync(c, data, new HttpMethod("PATCH")).ConfigureAwait(false); + } + + /// + /// Sets or overwrites data at given location. + /// + /// The object. + /// Optional timeout value. + /// Type of + /// The . + public async Task PutAsync(string data, TimeSpan? timeout = null) + { + var c = this.GetClient(timeout); + + await this.Silent().SendAsync(c, data, HttpMethod.Put).ConfigureAwait(false); + } + + /// + /// Deletes data from given location. + /// + /// Optional timeout value. + /// The . + public async Task DeleteAsync(TimeSpan? timeout = null) + { + var c = this.GetClient(timeout); + var url = string.Empty; + var responseData = string.Empty; + var statusCode = HttpStatusCode.OK; + + try + { + url = await this.BuildUrlAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + throw new FirebaseException("Couldn't build the url", string.Empty, responseData, statusCode, ex); + } + + try + { + var result = await c.DeleteAsync(url).ConfigureAwait(false); + statusCode = result.StatusCode; + responseData = await result.Content.ReadAsStringAsync().ConfigureAwait(false); + + result.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + throw new FirebaseException(url, string.Empty, responseData, statusCode, ex); + } + } + + /// + /// Disposes this instance. + /// + public void Dispose() + { + this.client?.Dispose(); + } + + /// + /// Build the url segment of this child. + /// + /// The child of this query. + /// The . + protected abstract string BuildUrlSegment(FirebaseQuery child); + + private string BuildUrl(FirebaseQuery child) + { + var url = this.BuildUrlSegment(child); + + if (this.Parent != null) + { + url = this.Parent.BuildUrl(this) + url; + } + + return url; + } + + private HttpClient GetClient(TimeSpan? timeout = null) + { + if (this.client == null) + { + this.client = new HttpClient(); + } + + if (!timeout.HasValue) + { + this.client.Timeout = DEFAULT_HTTP_CLIENT_TIMEOUT; + } + else + { + this.client.Timeout = timeout.Value; + } + + return this.client; + } + + private async Task SendAsync(HttpClient client, string data, HttpMethod method) + { + var responseData = string.Empty; + var statusCode = HttpStatusCode.OK; + var requestData = data; + var url = string.Empty; + + try + { + url = await this.BuildUrlAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + throw new FirebaseException("Couldn't build the url", requestData, responseData, statusCode, ex); + } + + var message = new HttpRequestMessage(method, url) + { + Content = new StringContent(requestData) + }; + + try + { + var result = await client.SendAsync(message).ConfigureAwait(false); + statusCode = result.StatusCode; + responseData = await result.Content.ReadAsStringAsync().ConfigureAwait(false); + + result.EnsureSuccessStatusCode(); + + return responseData; + } + catch (Exception ex) + { + throw new FirebaseException(url, requestData, responseData, statusCode, ex); + } + } + } +} diff --git a/FireBase/Query/IFirebaseQuery.cs b/FireBase/Query/IFirebaseQuery.cs new file mode 100644 index 0000000..2e8c671 --- /dev/null +++ b/FireBase/Query/IFirebaseQuery.cs @@ -0,0 +1,43 @@ +namespace Firebase.Database.Query +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + + using Firebase.Database.Streaming; + + /// + /// The FirebaseQuery interface. + /// + public interface IFirebaseQuery + { + /// + /// Gets the owning client of this query. + /// + FirebaseClient Client + { + get; + } + + /// + /// Retrieves items which exist on the location specified by this query instance. + /// + /// Optional timeout value. + /// Type of the items. + /// Collection of . + Task>> OnceAsync(TimeSpan? timeout = null); + + /// + /// Returns current location as an observable which allows to real-time listening to events from the firebase server. + /// + /// Type of the items. + /// Cold observable of . + IObservable> AsObservable(EventHandler> exceptionHandler, string elementRoot = ""); + + /// + /// Builds the actual url of this query. + /// + /// The . + Task BuildUrlAsync(); + } +} diff --git a/FireBase/Query/OrderQuery.cs b/FireBase/Query/OrderQuery.cs new file mode 100644 index 0000000..46ebd2c --- /dev/null +++ b/FireBase/Query/OrderQuery.cs @@ -0,0 +1,34 @@ +namespace Firebase.Database.Query +{ + using System; + + /// + /// Represents a firebase ordering query, e.g. "?OrderBy=Foo". + /// + public class OrderQuery : ParameterQuery + { + private readonly Func propertyNameFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The query parent. + /// The property name. + /// The owning client. + public OrderQuery(ChildQuery parent, Func propertyNameFactory, FirebaseClient client) + : base(parent, () => "orderBy", client) + { + this.propertyNameFactory = propertyNameFactory; + } + + /// + /// The build url parameter. + /// + /// The child. + /// The . + protected override string BuildUrlParameter(FirebaseQuery child) + { + return $"\"{this.propertyNameFactory()}\""; + } + } +} diff --git a/FireBase/Query/ParameterQuery.cs b/FireBase/Query/ParameterQuery.cs new file mode 100644 index 0000000..e3d9717 --- /dev/null +++ b/FireBase/Query/ParameterQuery.cs @@ -0,0 +1,43 @@ +namespace Firebase.Database.Query +{ + using System; + + /// + /// Represents a parameter in firebase query, e.g. "?data=foo". + /// + public abstract class ParameterQuery : FirebaseQuery + { + private readonly Func parameterFactory; + private readonly string separator; + + /// + /// Initializes a new instance of the class. + /// + /// The parent of this query. + /// The parameter. + /// The owning client. + protected ParameterQuery(FirebaseQuery parent, Func parameterFactory, FirebaseClient client) + : base(parent, client) + { + this.parameterFactory = parameterFactory; + this.separator = (this.Parent is ChildQuery) ? "?" : "&"; + } + + /// + /// Build the url segment represented by this query. + /// + /// The child. + /// The . + protected override string BuildUrlSegment(FirebaseQuery child) + { + return $"{this.separator}{this.parameterFactory()}={this.BuildUrlParameter(child)}"; + } + + /// + /// The build url parameter. + /// + /// The child. + /// The . + protected abstract string BuildUrlParameter(FirebaseQuery child); + } +} diff --git a/FireBase/Query/QueryExtensions.cs b/FireBase/Query/QueryExtensions.cs new file mode 100644 index 0000000..77db644 --- /dev/null +++ b/FireBase/Query/QueryExtensions.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Firebase.Database.Query +{ + /// + /// Query extensions providing linq like syntax for firebase server methods. + /// + public static class QueryExtensions + { + /// + /// Adds an auth parameter to the query. + /// + /// The child. + /// The auth token. + /// The . + internal static AuthQuery WithAuth(this FirebaseQuery node, string token) + { + return node.WithAuth(() => token); + } + + /// + /// Appends print=silent to save bandwidth. + /// + /// The child. + /// The . + internal static SilentQuery Silent(this FirebaseQuery node) + { + return new SilentQuery(node, node.Client); + } + + /// + /// References a sub child of the existing node. + /// + /// The child. + /// The path of sub child. + /// The . + public static ChildQuery Child(this ChildQuery node, string path) + { + return node.Child(() => path); + } + + /// + /// Order data by given . Note that this is used mainly for following filtering queries and due to firebase implementation + /// the data may actually not be ordered. + /// + /// The child. + /// The property name. + /// The . + public static OrderQuery OrderBy(this ChildQuery child, string propertyName) + { + return child.OrderBy(() => propertyName); + } + + /// + /// Instructs firebase to send data greater or equal to the . This must be preceded by an OrderBy query. + /// + /// Current node. + /// Value to start at. + /// The . + public static FilterQuery StartAt(this ParameterQuery child, string value) + { + return child.StartAt(() => value); + } + + /// + /// Instructs firebase to send data lower or equal to the . This must be preceded by an OrderBy query. + /// + /// Current node. + /// Value to start at. + /// The . + public static FilterQuery EndAt(this ParameterQuery child, string value) + { + return child.EndAt(() => value); + } + + /// + /// Instructs firebase to send data equal to the . This must be preceded by an OrderBy query. + /// + /// Current node. + /// Value to start at. + /// The . + public static FilterQuery EqualTo(this ParameterQuery child, string value) + { + return child.EqualTo(() => value); + } + + /// + /// Instructs firebase to send data greater or equal to the . This must be preceded by an OrderBy query. + /// + /// Current node. + /// Value to start at. + /// The . + public static FilterQuery StartAt(this ParameterQuery child, double value) + { + return child.StartAt(() => value); + } + + /// + /// Instructs firebase to send data lower or equal to the . This must be preceded by an OrderBy query. + /// + /// Current node. + /// Value to start at. + /// The . + public static FilterQuery EndAt(this ParameterQuery child, double value) + { + return child.EndAt(() => value); + } + + /// + /// Instructs firebase to send data equal to the . This must be preceded by an OrderBy query. + /// + /// Current node. + /// Value to start at. + /// The . + public static FilterQuery EqualTo(this ParameterQuery child, double value) + { + return child.EqualTo(() => value); + } + + /// + /// Instructs firebase to send data equal to the . This must be preceded by an OrderBy query. + /// + /// Current node. + /// Value to start at. + /// The . + public static FilterQuery EqualTo(this ParameterQuery child, bool value) + { + return child.EqualTo(() => value); + } + + /// + /// Instructs firebase to send data equal to null. This must be preceded by an OrderBy query. + /// + /// Current node. + /// The . + public static FilterQuery EqualTo(this ParameterQuery child) + { + return child.EqualTo(() => null); + } + + /// + /// Limits the result to first items. + /// + /// Current node. + /// Number of elements. + /// The . + public static FilterQuery LimitToFirst(this ParameterQuery child, int count) + { + return child.LimitToFirst(() => count); + } + + /// + /// Limits the result to last items. + /// + /// Current node. + /// Number of elements. + /// The . + public static FilterQuery LimitToLast(this ParameterQuery child, int count) + { + return child.LimitToLast(() => count); + } + + public static Task PutAsync(this FirebaseQuery query, T obj) + { + return query.PutAsync(JsonConvert.SerializeObject(obj, query.Client.Options.JsonSerializerSettings)); + } + + public static Task PatchAsync(this FirebaseQuery query, T obj) + { + return query.PatchAsync(JsonConvert.SerializeObject(obj, query.Client.Options.JsonSerializerSettings)); + } + + public static async Task> PostAsync(this FirebaseQuery query, T obj, bool generateKeyOffline = true) + { + var result = await query.PostAsync(JsonConvert.SerializeObject(obj, query.Client.Options.JsonSerializerSettings), generateKeyOffline); + + return new FirebaseObject(result.Key, obj); + } + + /// + /// Fan out given item to multiple locations at once. See https://firebase.googleblog.com/2015/10/client-side-fan-out-for-data-consistency_73.html for details. + /// + /// Type of object to fan out. + /// Current node. + /// Object to fan out. + /// Locations where to store the item. + public static async Task FanOut(this ChildQuery child, T item, params string[] relativePaths) + { + if (relativePaths == null) + { + throw new ArgumentNullException(nameof(relativePaths)); + } + + var fanoutObject = new Dictionary(relativePaths.Length); + + foreach (var path in relativePaths) + { + fanoutObject.Add(path, item); + } + + await child.PatchAsync(fanoutObject); + } + } +} diff --git a/FireBase/Query/QueryFactoryExtensions.cs b/FireBase/Query/QueryFactoryExtensions.cs new file mode 100644 index 0000000..b36e74a --- /dev/null +++ b/FireBase/Query/QueryFactoryExtensions.cs @@ -0,0 +1,176 @@ +namespace Firebase.Database.Query +{ + using System; + + /// + /// Query extensions providing linq like syntax for firebase server methods. + /// + public static class QueryFactoryExtensions + { + /// + /// Adds an auth parameter to the query. + /// + /// The child. + /// The auth token. + /// The . + internal static AuthQuery WithAuth(this FirebaseQuery node, Func tokenFactory) + { + return new AuthQuery(node, tokenFactory, node.Client); + } + + /// + /// References a sub child of the existing node. + /// + /// The child. + /// The path of sub child. + /// The . + public static ChildQuery Child(this ChildQuery node, Func pathFactory) + { + return new ChildQuery(node, pathFactory, node.Client); + } + + /// + /// Order data by given . Note that this is used mainly for following filtering queries and due to firebase implementation + /// the data may actually not be ordered. + /// + /// The child. + /// The property name. + /// The . + public static OrderQuery OrderBy(this ChildQuery child, Func propertyNameFactory) + { + return new OrderQuery(child, propertyNameFactory, child.Client); + } + + /// + /// Order data by $key. Note that this is used mainly for following filtering queries and due to firebase implementation + /// the data may actually not be ordered. + /// + /// The child. + /// The . + public static OrderQuery OrderByKey(this ChildQuery child) + { + return child.OrderBy("$key"); + } + + /// + /// Order data by $value. Note that this is used mainly for following filtering queries and due to firebase implementation + /// the data may actually not be ordered. + /// + /// The child. + /// The . + public static OrderQuery OrderByValue(this ChildQuery child) + { + return child.OrderBy("$value"); + } + + /// + /// Order data by $priority. Note that this is used mainly for following filtering queries and due to firebase implementation + /// the data may actually not be ordered. + /// + /// The child. + /// The . + public static OrderQuery OrderByPriority(this ChildQuery child) + { + return child.OrderBy("$priority"); + } + + /// + /// Instructs firebase to send data greater or equal to the . This must be preceded by an OrderBy query. + /// + /// Current node. + /// Value to start at. + /// The . + public static FilterQuery StartAt(this ParameterQuery child, Func valueFactory) + { + return new FilterQuery(child, () => "startAt", valueFactory, child.Client); + } + + /// + /// Instructs firebase to send data lower or equal to the . This must be preceded by an OrderBy query. + /// + /// Current node. + /// Value to start at. + /// The . + public static FilterQuery EndAt(this ParameterQuery child, Func valueFactory) + { + return new FilterQuery(child, () => "endAt", valueFactory, child.Client); + } + + /// + /// Instructs firebase to send data equal to the . This must be preceded by an OrderBy query. + /// + /// Current node. + /// Value to start at. + /// The . + public static FilterQuery EqualTo(this ParameterQuery child, Func valueFactory) + { + return new FilterQuery(child, () => "equalTo", valueFactory, child.Client); + } + + /// + /// Instructs firebase to send data greater or equal to the . This must be preceded by an OrderBy query. + /// + /// Current node. + /// Value to start at. + /// The . + public static FilterQuery StartAt(this ParameterQuery child, Func valueFactory) + { + return new FilterQuery(child, () => "startAt", valueFactory, child.Client); + } + + /// + /// Instructs firebase to send data lower or equal to the . This must be preceded by an OrderBy query. + /// + /// Current node. + /// Value to start at. + /// The . + public static FilterQuery EndAt(this ParameterQuery child, Func valueFactory) + { + return new FilterQuery(child, () => "endAt", valueFactory, child.Client); + } + + /// + /// Instructs firebase to send data equal to the . This must be preceded by an OrderBy query. + /// + /// Current node. + /// Value to start at. + /// The . + public static FilterQuery EqualTo(this ParameterQuery child, Func valueFactory) + { + return new FilterQuery(child, () => "equalTo", valueFactory, child.Client); + } + + /// + /// Instructs firebase to send data equal to the . This must be preceded by an OrderBy query. + /// + /// Current node. + /// Value to start at. + /// The . + public static FilterQuery EqualTo(this ParameterQuery child, Func valueFactory) + { + return new FilterQuery(child, () => "equalTo", valueFactory, child.Client); + } + + /// + /// Limits the result to first items. + /// + /// Current node. + /// Number of elements. + /// The . + public static FilterQuery LimitToFirst(this ParameterQuery child, Func countFactory) + { + return new FilterQuery(child, () => "limitToFirst", () => countFactory(), child.Client); + } + + /// + /// Limits the result to last items. + /// + /// Current node. + /// Number of elements. + /// The . + public static FilterQuery LimitToLast(this ParameterQuery child, Func countFactory) + { + return new FilterQuery(child, () => "limitToLast", () => countFactory(), child.Client); + } + } +} diff --git a/FireBase/Query/SilentQuery.cs b/FireBase/Query/SilentQuery.cs new file mode 100644 index 0000000..15584f6 --- /dev/null +++ b/FireBase/Query/SilentQuery.cs @@ -0,0 +1,18 @@ +namespace Firebase.Database.Query +{ + /// + /// Appends print=silent to the url. + /// + public class SilentQuery : ParameterQuery + { + public SilentQuery(FirebaseQuery parent, FirebaseClient client) + : base(parent, () => "print", client) + { + } + + protected override string BuildUrlParameter(FirebaseQuery child) + { + return "silent"; + } + } +} diff --git a/FireBase/Settings.StyleCop b/FireBase/Settings.StyleCop new file mode 100644 index 0000000..833aa39 --- /dev/null +++ b/FireBase/Settings.StyleCop @@ -0,0 +1,77 @@ + + + + auth + firebase + json + linq + oauth + + + + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + False + + + + + True + + + + + True + + + + + True + True + False + + + + + + + True + + + + + + + \ No newline at end of file diff --git a/FireBase/Streaming/FirebaseCache.cs b/FireBase/Streaming/FirebaseCache.cs new file mode 100644 index 0000000..ba7990b --- /dev/null +++ b/FireBase/Streaming/FirebaseCache.cs @@ -0,0 +1,192 @@ +namespace Firebase.Database.Streaming +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + using Firebase.Database.Http; + + using Newtonsoft.Json; + + /// + /// The firebase cache. + /// + /// Type of top-level entities in the cache. + public class FirebaseCache : IEnumerable> + { + private readonly IDictionary dictionary; + private readonly bool isDictionaryType; + private readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings() + { + ObjectCreationHandling = ObjectCreationHandling.Replace + }; + + /// + /// Initializes a new instance of the class. + /// + public FirebaseCache() + : this(new Dictionary()) + { + } + + /// + /// Initializes a new instance of the class and populates it with existing data. + /// + /// The existing items. + public FirebaseCache(IDictionary existingItems) + { + this.dictionary = existingItems; + this.isDictionaryType = typeof(IDictionary).GetTypeInfo().IsAssignableFrom(typeof(T).GetTypeInfo()); + } + + /// + /// The push data. + /// + /// The path of incoming data, separated by slash. + /// The data in json format as returned by firebase. + /// Collection of top-level entities which were affected by the push. + public IEnumerable> PushData(string path, string data, bool removeEmptyEntries = true) + { + object obj = this.dictionary; + Action primitiveObjSetter = null; + Action objDeleter = null; + + var pathElements = path.Split(new[] { "/" }, removeEmptyEntries ? StringSplitOptions.RemoveEmptyEntries : StringSplitOptions.None); + + // first find where we should insert the data to + foreach (var element in pathElements) + { + if (obj is IDictionary) + { + // if it's a dictionary, then it's just a matter of inserting into it / accessing existing object by key + var dictionary = obj as IDictionary; + var valueType = obj.GetType().GenericTypeArguments[1]; + + primitiveObjSetter = (d) => dictionary[element] = d; + objDeleter = () => dictionary.Remove(element); + + if (dictionary.Contains(element)) + { + obj = dictionary[element]; + } + else + { + dictionary[element] = this.CreateInstance(valueType); + obj = dictionary[element]; + } + } + else + { + // if it's not a dictionary, try to find the property of current object with the matching name + var objParent = obj; + var property = objParent + .GetType() + .GetRuntimeProperties() + .First(p => p.Name.Equals(element, StringComparison.OrdinalIgnoreCase) || element == p.GetCustomAttribute()?.PropertyName); + + objDeleter = () => property.SetValue(objParent, null); + primitiveObjSetter = (d) => property.SetValue(objParent, d); + obj = property.GetValue(obj); + if (obj == null) + { + obj = this.CreateInstance(property.PropertyType); + property.SetValue(objParent, obj); + } + } + } + + // if data is null (=empty string) delete it + if (string.IsNullOrWhiteSpace(data) || data == "null") + { + var key = pathElements[0]; + var target = this.dictionary[key]; + + objDeleter(); + + yield return new FirebaseObject(key, target); + yield break; + } + + // now insert the data + if (obj is IDictionary && !this.isDictionaryType) + { + // insert data into dictionary and return it as a collection of FirebaseObject + var dictionary = obj as IDictionary; + var valueType = obj.GetType().GenericTypeArguments[1]; + var objectCollection = data.GetObjectCollection(valueType); + + foreach (var item in objectCollection) + { + dictionary[item.Key] = item.Object; + + // top level dictionary changed + if (!pathElements.Any()) + { + yield return new FirebaseObject(item.Key, (T)item.Object); + } + } + + // nested dictionary changed + if (pathElements.Any()) + { + this.dictionary[pathElements[0]] = this.dictionary[pathElements[0]]; + yield return new FirebaseObject(pathElements[0], this.dictionary[pathElements[0]]); + } + } + else + { + // set the data on a property of the given object + var valueType = obj.GetType(); + + // firebase sends strings without double quotes + var targetObject = valueType == typeof(string) ? data.ToString() : JsonConvert.DeserializeObject(data, valueType); + + if ((valueType.GetTypeInfo().IsPrimitive || valueType == typeof(string)) && primitiveObjSetter != null) + { + // handle primitive (value) types separately + primitiveObjSetter(targetObject); + } + else + { + JsonConvert.PopulateObject(data, obj, this.serializerSettings); + } + + this.dictionary[pathElements[0]] = this.dictionary[pathElements[0]]; + yield return new FirebaseObject(pathElements[0], this.dictionary[pathElements[0]]); + } + } + + public bool Contains(string key) + { + return this.dictionary.Keys.Contains(key); + } + + private object CreateInstance(Type type) + { + if (type == typeof(string)) + { + return string.Empty; + } + else + { + return Activator.CreateInstance(type); + } + } + + #region IEnumerable + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + public IEnumerator> GetEnumerator() + { + return this.dictionary.Select(p => new FirebaseObject(p.Key, p.Value)).GetEnumerator(); + } + + #endregion + } +} diff --git a/FireBase/Streaming/FirebaseEvent.cs b/FireBase/Streaming/FirebaseEvent.cs new file mode 100644 index 0000000..c2338ca --- /dev/null +++ b/FireBase/Streaming/FirebaseEvent.cs @@ -0,0 +1,40 @@ +namespace Firebase.Database.Streaming +{ + /// + /// Firebase event which hold and the object affected by the event. + /// + /// Type of object affected by the event. + public class FirebaseEvent : FirebaseObject + { + /// + /// Initializes a new instance of the class. + /// + /// The key of the object. + /// The object. + /// The event type. + public FirebaseEvent(string key, T obj, FirebaseEventType eventType, FirebaseEventSource eventSource) + : base(key, obj) + { + this.EventType = eventType; + this.EventSource = eventSource; + } + + /// + /// Gets the source of the event. + /// + public FirebaseEventSource EventSource + { + get; + } + + /// + /// Gets the event type. + /// + public FirebaseEventType EventType + { + get; + } + + public static FirebaseEvent Empty(FirebaseEventSource source) => new FirebaseEvent(string.Empty, default(T), FirebaseEventType.InsertOrUpdate, source); + } +} diff --git a/FireBase/Streaming/FirebaseEventSource.cs b/FireBase/Streaming/FirebaseEventSource.cs new file mode 100644 index 0000000..98df977 --- /dev/null +++ b/FireBase/Streaming/FirebaseEventSource.cs @@ -0,0 +1,38 @@ +namespace Firebase.Database.Streaming +{ + /// + /// Specifies the origin of given + /// + public enum FirebaseEventSource + { + /// + /// Event comes from an offline source. + /// + Offline, + + /// + /// Event comes from online source fetched during initial pull (valid only for RealtimeDatabase). + /// + OnlineInitial, + + /// + /// Event comes from online source received thru active stream. + /// + OnlineStream, + + /// + /// Event comes from online source being fetched manually. + /// + OnlinePull, + + /// + /// Event raised after successful online push (valid only for RealtimeDatabase which isn't streaming). + /// + OnlinePush, + + /// + /// Event comes from an online source. + /// + Online = OnlineInitial | OnlinePull | OnlinePush | OnlineStream + } +} diff --git a/FireBase/Streaming/FirebaseEventType.cs b/FireBase/Streaming/FirebaseEventType.cs new file mode 100644 index 0000000..5fb21ef --- /dev/null +++ b/FireBase/Streaming/FirebaseEventType.cs @@ -0,0 +1,18 @@ +namespace Firebase.Database.Streaming +{ + /// + /// The type of event. + /// + public enum FirebaseEventType + { + /// + /// Item was inserted or updated. + /// + InsertOrUpdate, + + /// + /// Item was deleted. + /// + Delete + } +} diff --git a/FireBase/Streaming/FirebaseServerEventType.cs b/FireBase/Streaming/FirebaseServerEventType.cs new file mode 100644 index 0000000..1f10bc8 --- /dev/null +++ b/FireBase/Streaming/FirebaseServerEventType.cs @@ -0,0 +1,15 @@ +namespace Firebase.Database.Streaming +{ + internal enum FirebaseServerEventType + { + Put, + + Patch, + + KeepAlive, + + Cancel, + + AuthRevoked + } +} diff --git a/FireBase/Streaming/FirebaseSubscription.cs b/FireBase/Streaming/FirebaseSubscription.cs new file mode 100644 index 0000000..4b5e643 --- /dev/null +++ b/FireBase/Streaming/FirebaseSubscription.cs @@ -0,0 +1,221 @@ +namespace Firebase.Database.Streaming +{ + using System; + using System.Diagnostics; + using System.Linq; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Threading; + using System.Threading.Tasks; + + using Firebase.Database.Query; + + using Newtonsoft.Json.Linq; + using System.Net; + + /// + /// The firebase subscription. + /// + /// Type of object to be streaming back to the called. + internal class FirebaseSubscription : IDisposable + { + private readonly CancellationTokenSource cancel; + private readonly IObserver> observer; + private readonly IFirebaseQuery query; + private readonly FirebaseCache cache; + private readonly string elementRoot; + private readonly FirebaseClient client; + + private static HttpClient http; + + static FirebaseSubscription() + { + var handler = new HttpClientHandler + { + AllowAutoRedirect = true, + MaxAutomaticRedirections = 10, + CookieContainer = new CookieContainer() + }; + + var httpClient = new HttpClient(handler, true); + + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream")); + + http = httpClient; + } + + /// + /// Initializes a new instance of the class. + /// + /// The observer. + /// The query. + /// The cache. + public FirebaseSubscription(IObserver> observer, IFirebaseQuery query, string elementRoot, FirebaseCache cache) + { + this.observer = observer; + this.query = query; + this.elementRoot = elementRoot; + this.cancel = new CancellationTokenSource(); + this.cache = cache; + this.client = query.Client; + } + + public event EventHandler> ExceptionThrown; + + public void Dispose() + { + this.cancel.Cancel(); + } + + public IDisposable Run() + { + Task.Run(() => this.ReceiveThread()); + + return this; + } + + private async void ReceiveThread() + { + while (true) + { + var url = string.Empty; + var line = string.Empty; + var statusCode = HttpStatusCode.OK; + + try + { + this.cancel.Token.ThrowIfCancellationRequested(); + + // initialize network connection + url = await this.query.BuildUrlAsync().ConfigureAwait(false); + var request = new HttpRequestMessage(HttpMethod.Get, url); + var serverEvent = FirebaseServerEventType.KeepAlive; + + var client = this.GetHttpClient(); + var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, this.cancel.Token).ConfigureAwait(false); + + statusCode = response.StatusCode; + response.EnsureSuccessStatusCode(); + + using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var reader = this.client.Options.SubscriptionStreamReaderFactory(stream)) + { + while (true) + { + this.cancel.Token.ThrowIfCancellationRequested(); + + line = reader.ReadLine()?.Trim(); + + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + var tuple = line.Split(new[] { ':' }, 2, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray(); + + switch (tuple[0].ToLower()) + { + case "event": + serverEvent = this.ParseServerEvent(serverEvent, tuple[1]); + break; + case "data": + this.ProcessServerData(url, serverEvent, tuple[1]); + break; + } + + if (serverEvent == FirebaseServerEventType.AuthRevoked) + { + // auth token no longer valid, reconnect + break; + } + } + } + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) when (statusCode != HttpStatusCode.OK) + { + this.observer.OnError(new FirebaseException(url, string.Empty, line, statusCode, ex)); + this.Dispose(); + break; + } + catch (Exception ex) + { + this.ExceptionThrown?.Invoke(this, new ExceptionEventArgs(new FirebaseException(url, string.Empty, line, statusCode, ex))); + + await Task.Delay(2000).ConfigureAwait(false); + } + } + } + + private FirebaseServerEventType ParseServerEvent(FirebaseServerEventType serverEvent, string eventName) + { + switch (eventName) + { + case "put": + serverEvent = FirebaseServerEventType.Put; + break; + case "patch": + serverEvent = FirebaseServerEventType.Patch; + break; + case "keep-alive": + serverEvent = FirebaseServerEventType.KeepAlive; + break; + case "cancel": + serverEvent = FirebaseServerEventType.Cancel; + break; + case "auth_revoked": + serverEvent = FirebaseServerEventType.AuthRevoked; + break; + } + + return serverEvent; + } + + private void ProcessServerData(string url, FirebaseServerEventType serverEvent, string serverData) + { + switch (serverEvent) + { + case FirebaseServerEventType.Put: + case FirebaseServerEventType.Patch: + var result = JObject.Parse(serverData); + var path = result["path"].ToString(); + var data = result["data"].ToString(); + + // If an elementRoot parameter is provided, but it's not in the cache, it was already deleted. So we can return an empty object. + if(string.IsNullOrWhiteSpace(this.elementRoot) || !this.cache.Contains(this.elementRoot)) + { + if(path == "/" && data == string.Empty) + { + this.observer.OnNext(FirebaseEvent.Empty(FirebaseEventSource.OnlineStream)); + return; + } + } + + var eventType = string.IsNullOrWhiteSpace(data) ? FirebaseEventType.Delete : FirebaseEventType.InsertOrUpdate; + + var items = this.cache.PushData(this.elementRoot + path, data); + + foreach (var i in items.ToList()) + { + this.observer.OnNext(new FirebaseEvent(i.Key, i.Object, eventType, FirebaseEventSource.OnlineStream)); + } + + break; + case FirebaseServerEventType.KeepAlive: + break; + case FirebaseServerEventType.Cancel: + this.observer.OnError(new FirebaseException(url, string.Empty, serverData, HttpStatusCode.Unauthorized)); + this.Dispose(); + break; + } + } + + private HttpClient GetHttpClient() + { + return http; + } + } +} diff --git a/FireBase/Streaming/NonBlockingStreamReader.cs b/FireBase/Streaming/NonBlockingStreamReader.cs new file mode 100644 index 0000000..2ac83fd --- /dev/null +++ b/FireBase/Streaming/NonBlockingStreamReader.cs @@ -0,0 +1,60 @@ +namespace Firebase.Database.Streaming +{ + using System.IO; + using System.Text; + + /// + /// When a regular is used in a UWP app its method tends to take a long + /// time for data larger then 2 KB. This extremly simple implementation of can be used instead to boost performance + /// in your UWP app. Use to inject an instance of this class into your . + /// + public class NonBlockingStreamReader : TextReader + { + private const int DefaultBufferSize = 16000; + + private readonly Stream stream; + private readonly byte[] buffer; + private readonly int bufferSize; + + private string cachedData; + + public NonBlockingStreamReader(Stream stream, int bufferSize = DefaultBufferSize) + { + this.stream = stream; + this.bufferSize = bufferSize; + this.buffer = new byte[bufferSize]; + + this.cachedData = string.Empty; + } + + public override string ReadLine() + { + var currentString = this.TryGetNewLine(); + + while (currentString == null) + { + var read = this.stream.Read(this.buffer, 0, this.bufferSize); + var str = Encoding.UTF8.GetString(buffer, 0, read); + + cachedData += str; + currentString = this.TryGetNewLine(); + } + + return currentString; + } + + private string TryGetNewLine() + { + var newLine = cachedData.IndexOf('\n'); + + if (newLine >= 0) + { + var r = cachedData.Substring(0, newLine + 1); + this.cachedData = cachedData.Remove(0, r.Length); + return r.Trim(); + } + + return null; + } + } +} diff --git a/WebInterface/NodeJSServer/dist/Web.config b/WebInterface/NodeJSServer/dist/Web.config new file mode 100644 index 0000000..741b7d8 --- /dev/null +++ b/WebInterface/NodeJSServer/dist/Web.config @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/ZooBOTanica/CritCreate.cs b/ZooBOTanica/CritCreate.cs index f570c9e..08a6ff0 100644 --- a/ZooBOTanica/CritCreate.cs +++ b/ZooBOTanica/CritCreate.cs @@ -23,7 +23,7 @@ namespace ZooBOTanica this.AllowDrop = true; } - private void Load(string path) + private new void Load(string path) { this.critter = Critter.Load(path); -- cgit v1.3.1