diff --git a/SpeckleCore/Converter.cs b/SpeckleCore/Conversion/Converter.cs similarity index 93% rename from SpeckleCore/Converter.cs rename to SpeckleCore/Conversion/Converter.cs index 307071b..deaeb26 100644 --- a/SpeckleCore/Converter.cs +++ b/SpeckleCore/Conversion/Converter.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; +using System.Drawing; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -10,6 +11,7 @@ using System.Text; using System.Threading.Tasks; + namespace SpeckleCore { /// @@ -74,6 +76,33 @@ public static byte[ ] base64ToBytes( string str ) return Convert.FromBase64String( str ); } + /// + /// Returns a stringifed MD5 hash of a string. + /// + /// String from which to generate the hash + /// If 0, the full hasdh will be returned, otherwise it will be trimmed to the specified lenght + /// + public static string getMd5Hash( string str, int length = 0 ) + { + using ( System.IO.MemoryStream ms = new System.IO.MemoryStream() ) + { + new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter().Serialize( ms, str ); + byte[ ] hash; + using ( System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create() ) + { + hash = md5.ComputeHash( ms.ToArray() ); + StringBuilder sb = new StringBuilder(); + foreach ( byte bbb in hash ) + sb.Append( bbb.ToString( "X2" ) ); + + if ( length != 0 ) + return sb.ToString().ToLower().Substring( 0, length ); + else + return sb.ToString().ToLower(); + } + } + } + // https://stackoverflow.com/a/299526/3446736 private static IEnumerable GetExtensionMethods( Assembly assembly, Type extendedType, string methodName ) { @@ -157,8 +186,17 @@ public static object Deserialise( SpeckleObject obj, object root = null ) if ( absObj._type == "ref" ) return null; + //var shortName = absObj._assembly.Split( ',' )[ 0 ]; + var assembly = System.AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault( a => a.FullName == absObj._assembly ); + //try again + if ( assembly == null ) + { + var shortName = absObj._assembly.Split( ',' )[ 0 ]; + assembly = System.AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault( a => a.FullName.Contains( shortName ) ); + } + if ( assembly == null ) // we can't deserialise for sure return Converter.ShallowConvert( absObj ); @@ -205,7 +243,7 @@ public static object Deserialise( SpeckleObject obj, object root = null ) try { - if ( (prop!=null && prop.PropertyType.IsArray) || (field!=null && field.FieldType.IsArray) ) + if ( ( prop != null && prop.PropertyType.IsArray ) || ( field != null && field.FieldType.IsArray ) ) { value = ( ( List ) value ).ToArray(); } @@ -628,4 +666,5 @@ private static object WriteValue( object myObject, int recursionDepth, Dictionar } } + } diff --git a/SpeckleCore/JsonConverters.cs b/SpeckleCore/Conversion/JsonConverters.cs similarity index 100% rename from SpeckleCore/JsonConverters.cs rename to SpeckleCore/Conversion/JsonConverters.cs diff --git a/SpeckleCore/ClassesForAbstractTests.cs b/SpeckleCore/Generic Utils/ClassesForAbstractTests.cs similarity index 100% rename from SpeckleCore/ClassesForAbstractTests.cs rename to SpeckleCore/Generic Utils/ClassesForAbstractTests.cs diff --git a/SpeckleCore/GzipContent.cs b/SpeckleCore/Generic Utils/GzipContent.cs similarity index 100% rename from SpeckleCore/GzipContent.cs rename to SpeckleCore/Generic Utils/GzipContent.cs diff --git a/SpeckleCore/LocalData/Models.cs b/SpeckleCore/LocalData/Models.cs new file mode 100644 index 0000000..e7126fd --- /dev/null +++ b/SpeckleCore/LocalData/Models.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SQLite; + +namespace SpeckleCore +{ + /// + /// A class for a generic speckle account, composed of all the identification props for a user & server combination. + /// + public class Account + { + [PrimaryKey, AutoIncrement] + public int AccountId { get; set; } + + public string ServerName { get; set; } + + [Indexed] + public string RestApi { get; set; } + + [Indexed] + public string Email { get; set; } + + public string Token { get; set; } + + public bool IsDefault { get; set; } = false; + } + + /// + /// Special class for efficiently storing sent objects. Why? We do not want to store them fully as they are already stored in the users's file. Kind of duplicates the CachedObject. + /// + public class SentObject + { + /// + /// Represents the api this object came from + /// + [Indexed] + public string RestApi { get; set; } + + [Indexed] + public string DatabaseId { get; set; } + + [Indexed] + public string Hash { get; set; } + } + + /// + /// A class for storing cached objects (that have been retrieved from a database). + /// + public class CachedObject + { + /// + /// Represents hash(databaseId + restApi) + /// + [PrimaryKey, Indexed( Unique = true )] + public string CombinedHash { get; set; } + + /// + /// Represents the api this object came from + /// + [Indexed] + public string RestApi { get; set; } + + [Indexed] + public string DatabaseId { get; set; } + + [Indexed] + public string Hash { get; set; } + + public DateTime AddedOn {get;set;} + + public byte[ ] Bytes { get; set; } + + /// + /// Returns the speckle object from cache. + /// + /// + public SpeckleObject ToSpeckle( ) + { + return SpeckleCore.Converter.getObjFromBytes( this.Bytes ) as SpeckleObject; + } + } + + public class CachedStream + { + /// + /// Represents hash(streamId + restApi) + /// + [PrimaryKey, Indexed( Unique = true )] + public string CombinedHash { get; set; } + + /// + /// Represents the api this object came from + /// + [Indexed] + public string RestApi { get; set; } + + [Indexed] + public string StreamId { get; set; } + + public DateTime AddedOn { get; set; } + + public DateTime UpdatedOn { get; set; } + + public byte[ ] Bytes { get; set; } + + public SpeckleStream ToSpeckle() + { + return SpeckleStream.FromJson( SpeckleCore.Converter.getObjFromBytes( this.Bytes ) as string ); // ((SpeckleCore.Converter.getObjFromBytes( this.Bytes ) as SpeckleStream; + } + } +} diff --git a/SpeckleCore/LocalData/SpeckleLocalContext.cs b/SpeckleCore/LocalData/SpeckleLocalContext.cs new file mode 100644 index 0000000..d960b85 --- /dev/null +++ b/SpeckleCore/LocalData/SpeckleLocalContext.cs @@ -0,0 +1,453 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using SQLite; + +namespace SpeckleCore +{ + /// + /// This class holds the keys to the local sqlite database that acts as a local cache for various speckle things. + /// You can access accounts from here across speckle integrations, as well as the local object cache. + /// The cache holds the following tables: Accounts, CachedObjects, SentObjects, CachedStreams. + /// Cached objects are objects that a receiver requested. They are stored fully (with a binary blob of their speckle representation). SentObjects are objects that have been previously sent by a sender and are stored without their speckle representation (they're just refs) as a log against which to diff what to send or not. + /// + public static partial class LocalContext + { + private static bool IsInit = false; + + private static SQLiteConnection Database; + + public static string DbPath = System.Environment.GetFolderPath( System.Environment.SpecialFolder.LocalApplicationData ) + @"\SpeckleSettings\SpeckleCache.db"; + + public static string SettingsFolderPath = System.Environment.GetFolderPath( System.Environment.SpecialFolder.LocalApplicationData ) + @"\SpeckleSettings\"; + + /// + /// Initialises the database context, ensures tables are created and powers up the rocket engines. + /// + public static void Init( ) + { + if ( IsInit ) return; + + if ( !Directory.Exists( SettingsFolderPath ) ) + Directory.CreateDirectory( SettingsFolderPath ); + + Database = new SQLiteConnection( DbPath ); + Database.CreateTable(); + Database.CreateTable(); + Database.CreateTable(); + Database.CreateTable(); + + MigrateAccounts(); + + IsInit = true; + } + + public static void Close( ) + { + Database?.Close(); + } + + #region Cleanup & Table Purging + /// + /// Purges the sent objects table. WARNING: Don't do this unless you know what you're doing. + /// + public static void PurgeSentObjects( ) + { + LocalContext.Init(); + Database?.Execute( "DELETE FROM SentObject" ); + } + + /// + /// Purges the received objects table. WARNING: Don't do this unless you know what you're doing. + /// + public static void PurgeCachedObjects( ) + { + LocalContext.Init(); + Database?.Execute( "DELETE FROM CachedObject" ); + } + + /// + /// Purges the accounts. WARNING: Don't do this unless you know what you're doing. + /// + public static void PurgeAccounts( ) + { + LocalContext.Init(); + Database?.Execute( "DELETE FROM Account" ); + } + + /// + /// Purges the streams table. WARNING: Don't do this unless you know what you're doing. + /// + public static void PurgeCachedStreams( ) + { + LocalContext.Init(); + Database?.Execute( "DELETE FROM CachedStream" ); + } + + #endregion + + #region Accounts + /// + /// Migrates existing accounts stored in text files to the sqlite db. + /// + private static void MigrateAccounts( ) + { + List accounts = new List(); + + if ( Directory.Exists( SettingsFolderPath ) && Directory.EnumerateFiles( SettingsFolderPath, "*.txt" ).Count() > 0 ) + { + foreach ( string file in Directory.EnumerateFiles( SettingsFolderPath, "*.txt" ) ) + { + string content = File.ReadAllText( file ); + string[ ] pieces = content.TrimEnd( '\r', '\n' ).Split( ',' ); + + accounts.Add( new Account() { Email = pieces[ 0 ], Token = pieces[ 1 ], ServerName = pieces[ 2 ], RestApi = pieces[ 3 ] } ); + } + + var res = Database.InsertAll( accounts ); + + Directory.CreateDirectory( SettingsFolderPath + @"\MigratedAccounts\" ); + + foreach ( string file in Directory.EnumerateFiles( SettingsFolderPath, "*.txt" ) ) + { + try + { + var newName = Path.Combine( SettingsFolderPath, "MigratedAccounts", Path.GetFileName( file ) ); + if ( File.Exists( newName ) ) + { + File.Delete( newName ); + } + + File.Move( file, newName ); + } + catch ( Exception e ) + { + Debug.WriteLine( "yolo" ); + } + } + + } + else + { + Debug.WriteLine( "No existing account text files found." ); + } + } + + /// + /// Adds a new account. + /// + /// + public static void AddAccount( Account account ) + { + LocalContext.Init(); + var res = Database.Insert( account ); + } + + /// + /// Gets all accounts present. + /// + /// + public static List GetAllAccounts( ) + { + LocalContext.Init(); + return Database.Query( "SELECT * FROM Account" ); + } + + /// + /// Gets all the accounts associated with the provided rest api. + /// + /// + /// + public static List GetAccountsByRestApi( string RestApi ) + { + LocalContext.Init(); + return Database.Query( "SELECT * from Account WHERE RestApi = ?", RestApi ); + } + + /// + /// Gets all the accounts associated with the provided email. + /// + /// + /// + public static List GetAccountsByEmail( string email ) + { + LocalContext.Init(); + return Database.Query( "SELECT * from Account WHERE Email = ?", email ); + } + + /// + /// If more accounts present, will return the first one only. + /// + /// + /// + /// null if no account is found. + public static Account GetAccountByEmailAndRestApi( string email, string restApi ) + { + LocalContext.Init(); + var res = Database.Query( String.Format( "SELECT * from Account WHERE RestApi = '{0}' AND Email='{1}'", restApi, email ) ); + if ( res.Count >= 1 ) + { + return res[ 0 ]; + } + else + { + return null; + //throw new Exception("Could not find account."); + } + } + + /// + /// Returns the default account, if any. Otherwise throws an error. + /// + /// + public static Account GetDefaultAccount( ) + { + LocalContext.Init(); + var res = Database.Query( "SELECT * FROM Account WHERE IsDefault='true' LIMIT 1" ); + if ( res.Count == 1 ) + { + return res[ 0 ]; + } + else + { + throw new Exception( "No default account set." ); + } + } + + /// + /// Sets an account as being the default one, and de-sets defaultness on all others. + /// + /// + public static void SetDefaultAccount( Account account ) + { + LocalContext.Init(); + + Database.Execute( "UPDATE Account SET IsDefault=0" ); + + account.IsDefault = true; + Database.Update( account ); + } + + public static void RemoveAccount( Account ac ) + { + LocalContext.Init(); + Database.Delete( ac.AccountId ); + } + + #endregion + + #region Received Objects (CachedObject) + + /// + /// Adds a speckle object to the local cache. + /// + /// The object to add. + /// The server url of where it has been persisted. + public static void AddCachedObject( SpeckleObject obj, string restApi ) + { + LocalContext.Init(); + var bytes = SpeckleCore.Converter.getBytes( obj ); + var combinedHash = Converter.getMd5Hash( obj._id + restApi ); + var cached = new CachedObject() + { + RestApi = restApi, + Bytes = bytes, + DatabaseId = obj._id, + CombinedHash = combinedHash, + Hash = obj.Hash, + AddedOn = DateTime.Now + }; + + try + { + Database.Insert( cached ); + } + catch + { + // object was already there + } + } + + /// + /// Does a cache check on a list of speckle object placeholders. It will populate the original list with any objects it can find in the cache. If none are found, the list is returned unmodified. + /// + /// Speckle object placeholders to check against the cache. + /// The rest api these objects are expected to come from. + /// + public static List GetCachedObjects( List objs, string restApi ) + { + LocalContext.Init(); + var MaxSqlVars = 900; + + var combinedHashes = objs.Select( obj => Converter.getMd5Hash( obj._id + restApi ) ).ToList(); + + var partitionedList = new List>(); + + for ( int i = 0; i < combinedHashes.Count; i += MaxSqlVars ) + { + partitionedList.Add( combinedHashes.GetRange( i, Math.Min( MaxSqlVars, combinedHashes.Count - i ) ) ); + } + + var fullRes = new List(); + + foreach ( var subList in partitionedList ) + { + var res = Database.Table().Where( obj => subList.Contains( obj.CombinedHash ) ).Select( o => o.ToSpeckle() ).ToList(); + fullRes.AddRange( res ); + } + + // populate the original list with whatever objects we found in the database. + for ( int i = 0; i < objs.Count; i++ ) + { + var placeholder = objs[ i ]; + var myObject = fullRes.Find( o => o._id == placeholder._id ); + if ( myObject != null ) + { + objs[ i ] = myObject; + } + } + + return objs; + } + + #endregion + + #region Sent Objects (SentObject) + + /// + /// Adds an object that has been sent by a sender in the local cache. + /// This does not store the full object, it's just a log that it has been sent + /// to a server so it does not get sent again. + /// + /// Object to store as sent ref in the local database. + /// The server's url. + public static void AddSentObject( SpeckleObject obj, string restApi ) + { + LocalContext.Init(); + var sentObj = new SentObject() + { + RestApi = restApi, + DatabaseId = obj._id, + Hash = obj.Hash + }; + + try + { + Database.Insert( sentObj ); + } + catch(Exception e) + { + var dick = e; + // object was already there, no panic! + } + } + + /// + /// Replaces any objects in the given list with placeholders if they're found in the local cache, as this means they were sent before and most probably exist on the server. + /// + /// + /// + /// (Optinoal) The modified list. + public static List PruneExistingObjects( List objs, string restApi ) + { + LocalContext.Init(); + // MAX SQL Vars is 900 + var MaxSqlVars = 900; + + var objHashes = objs.Select( obj => true ? obj.Hash : restApi ).ToList(); + + var partitionedList = new List>(); + + for ( int i = 0; i < objHashes.Count; i += MaxSqlVars ) + { + partitionedList.Add( objHashes.GetRange( i, Math.Min( MaxSqlVars, objHashes.Count - i ) ) ); + } + + var fullRes = new List(); + + foreach ( var subList in partitionedList ) + { + var res = Database.Table().Where( obj => subList.Contains( obj.Hash ) && obj.RestApi == restApi ).ToList(); + fullRes.AddRange( res ); + } + + for ( int i = 0; i < objs.Count; i++ ) + { + var placeholder = objs[ i ]; + var myObject = fullRes.Find( o => o.Hash == objs[ i ].Hash ); + if ( myObject != null ) + { + objs[ i ] = new SpecklePlaceholder() { _id = myObject.DatabaseId }; + } + } + + return objs; + } + + #endregion + + #region Streams (CachedStream) + + /// + /// Updates or inserts a stream in the local cache. + /// + /// + /// + public static void AddOrUpdateStream( SpeckleStream stream, string restApi ) + { + LocalContext.Init(); + var bytes = SpeckleCore.Converter.getBytes( stream.ToJson() ); + var combinedHash = Converter.getMd5Hash( stream._id + restApi ); + + var cacheRes = Database.Table().Where( existing => existing.CombinedHash == combinedHash ).ToList(); + + if ( cacheRes.Count >= 1 ) + { + var toUpdate = cacheRes[ 0 ]; + toUpdate.Bytes = bytes; + toUpdate.UpdatedOn = DateTime.Now; + Database.Update( toUpdate ); + } + else + { + var toCache = new CachedStream() + { + CombinedHash = combinedHash, + Bytes = bytes, + RestApi = restApi, + StreamId = stream.StreamId, + AddedOn = DateTime.Now, + UpdatedOn = DateTime.Now + }; + Database.Insert( toCache ); + } + //throw new NotImplementedException(); + } + + /// + /// Gets a stream from the local cache. + /// + /// + /// + /// Null, if nothing found, or the speckle stream. + public static SpeckleStream GetStream( string streamId, string restApi ) + { + LocalContext.Init(); + var combinedHash = Converter.getMd5Hash( streamId + restApi ); + var res = Database.Table().Where( str => str.CombinedHash == combinedHash ).ToArray(); + if ( res.Length > 0 ) + { + return res[ 0 ].ToSpeckle(); + } + + return null; + } + + #endregion + } + + + +} diff --git a/SpeckleCore/ModelBase.cs b/SpeckleCore/Models/ModelBase.cs similarity index 99% rename from SpeckleCore/ModelBase.cs rename to SpeckleCore/Models/ModelBase.cs index e1b7e5c..8ab348e 100644 --- a/SpeckleCore/ModelBase.cs +++ b/SpeckleCore/Models/ModelBase.cs @@ -251,17 +251,17 @@ public partial class SpeckleStream : ResourceBase /// Units, tolerances, etc. [Newtonsoft.Json.JsonProperty( "baseProperties", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore )] - public object BaseProperties { get; set; } + public dynamic BaseProperties { get; set; } /// Any performance measures can go in here. [Newtonsoft.Json.JsonProperty( "globalMeasures", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore )] - public object GlobalMeasures { get; set; } + public dynamic GlobalMeasures { get; set; } [Newtonsoft.Json.JsonProperty( "isComputedResult", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore )] public bool? IsComputedResult { get; set; } [Newtonsoft.Json.JsonProperty( "viewerLayers", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore )] - public List ViewerLayers { get; set; } + public List ViewerLayers { get; set; } /// If this stream is a child, the parent's streamId. [Newtonsoft.Json.JsonProperty( "parent", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore )] diff --git a/SpeckleCore/ModelObjects.cs b/SpeckleCore/Models/ModelObjects.cs similarity index 99% rename from SpeckleCore/ModelObjects.cs rename to SpeckleCore/Models/ModelObjects.cs index 45dfb12..0c046b2 100644 --- a/SpeckleCore/ModelObjects.cs +++ b/SpeckleCore/Models/ModelObjects.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; @@ -115,7 +116,7 @@ public enum SpeckleObjectType [JsonInheritanceAttribute( "SpeckleBlock", typeof( SpeckleBlock ) )] [System.CodeDom.Compiler.GeneratedCode( "NJsonSchema", "9.10.41.0 (Newtonsoft.Json v9.0.0.0)" )] [Serializable] - public partial class SpeckleObject : ResourceBase + public partial class SpeckleObject : ResourceBase, IEqualityComparer { [Newtonsoft.Json.JsonProperty( "type", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore )] [Newtonsoft.Json.JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] @@ -164,7 +165,16 @@ public static SpeckleObject FromJson( string data ) return Newtonsoft.Json.JsonConvert.DeserializeObject( data ); } - } + public bool Equals( SpeckleObject x, SpeckleObject y ) + { + return x.Hash == y.Hash; + } + + public int GetHashCode( SpeckleObject obj ) + { + return obj.Hash.GetHashCode(); + } + } [System.CodeDom.Compiler.GeneratedCode( "NJsonSchema", "9.10.41.0 (Newtonsoft.Json v9.0.0.0)" )] [Serializable] diff --git a/SpeckleCore/ModelObjectsExt.cs b/SpeckleCore/Models/ModelObjectsExt.cs similarity index 90% rename from SpeckleCore/ModelObjectsExt.cs rename to SpeckleCore/Models/ModelObjectsExt.cs index a1f4b08..caba07f 100644 --- a/SpeckleCore/ModelObjectsExt.cs +++ b/SpeckleCore/Models/ModelObjectsExt.cs @@ -13,6 +13,19 @@ namespace SpeckleCore { + public class SpeckleObjectComparer : IEqualityComparer + { + public bool Equals( SpeckleObject x, SpeckleObject y ) + { + return x.Hash == y.Hash; + } + + public int GetHashCode( SpeckleObject obj ) + { + return obj.Hash.GetHashCode(); + } + } + public partial class SpeckleObject { /// @@ -21,7 +34,7 @@ public partial class SpeckleObject /// public string GetMd5FromObject( object fromWhat, int length = 0 ) { - if(fromWhat == null) + if ( fromWhat == null ) { return "null"; } @@ -69,12 +82,12 @@ public virtual void Scale( double factor ) /// /// /// - internal Dictionary ScaleProperties(Dictionary dict, double factor) + internal Dictionary ScaleProperties( Dictionary dict, double factor ) { if ( dict == null ) return null; - foreach(var kvp in dict) + foreach ( var kvp in dict ) { - switch(kvp.Value) + switch ( kvp.Value ) { case Dictionary d: dict[ kvp.Key ] = ScaleProperties( d, factor ); @@ -532,8 +545,8 @@ public override void Scale( double factor ) public override void GenerateHash( ) { base.GenerateHash(); - this.GeometryHash += GetMd5FromObject( BasePlane.GeometryHash + XSize.GeometryHash + YSize.GeometryHash + ZSize.GeometryHash ); - this.Hash = GetMd5FromObject( this.GeometryHash + GetMd5FromObject( this.Properties ) ); + this.GeometryHash += GetMd5FromObject( BasePlane.ToJson() + XSize.ToJson() + YSize.ToJson() + ZSize.ToJson() ); + this.Hash = GetMd5FromObject( this ); } } @@ -568,7 +581,7 @@ public override void GenerateHash( ) public partial class SpecklePolycurve { - public SpecklePolycurve() { } + public SpecklePolycurve( ) { } public override void Scale( double factor ) { @@ -713,7 +726,7 @@ public SpeckleExtrusion( SpeckleObject profile, double length, bool capped, stri public override void Scale( double factor ) { this.Length *= factor; - switch(this.Profile) + switch ( this.Profile ) { case SpeckleCurve c: c.Scale( factor ); @@ -734,12 +747,12 @@ public override void Scale( double factor ) e.Scale( factor ); break; case SpeckleLine l: - l.Scale( factor); + l.Scale( factor ); break; default: break; } - + this.Properties = ScaleProperties( this.Properties, factor ); GenerateHash(); } @@ -829,50 +842,59 @@ public int GetHashCode( Layer obj ) } } -public partial class SpeckleInput : SpeckleObject -{ - public SpeckleInput() { } - - [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + + // These two classes are used for the stream controller functionality. + + // Input parameter (ie, width, height) + public partial class SpeckleInput : SpeckleObject + { + public SpeckleInput( ) { } + + [Newtonsoft.Json.JsonProperty( "name", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore )] public string Name { get; set; } - [Newtonsoft.Json.JsonProperty("guid", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + [Newtonsoft.Json.JsonProperty( "guid", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore )] public string Guid { get; set; } - [Newtonsoft.Json.JsonProperty("value", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] - public float Value { get; set; } + [Newtonsoft.Json.JsonProperty( "value", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore )] + public double Value { get; set; } - [Newtonsoft.Json.JsonProperty("inputType", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + [Newtonsoft.Json.JsonProperty( "inputType", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore )] public string InputType { get; set; } - [Newtonsoft.Json.JsonProperty("max", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] - public float Max { get; set; } + [Newtonsoft.Json.JsonProperty( "max", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore )] + public double Max { get; set; } - [Newtonsoft.Json.JsonProperty("min", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] - public float Min { get; set; } + [Newtonsoft.Json.JsonProperty( "min", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore )] + public double Min { get; set; } - public SpeckleInput(string name, float min, float max, float value, string inputType, string guid) + [Newtonsoft.Json.JsonProperty( "step", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore )] + public double Step { get; set; } + + public SpeckleInput( string name, float min, float max, float value, string inputType, string guid ) { - this.Name = name; - this.Guid = guid; - this.Min = min; - this.Max = max; - this.Value = value; - this.InputType = inputType; + this.Name = name; + this.Guid = guid; + this.Min = min; + this.Max = max; + this.Value = value; + this.InputType = inputType; } -} + } + + // Output parameter (price, area) public partial class SpeckleOutput : SpeckleObject - { - public SpeckleOutput() { } - public string Name { get; set; } - public string Guid { get; set; } - public string Value { get; set; } + { + public SpeckleOutput( ) { } + public string Name { get; set; } + public string Guid { get; set; } + public string Value { get; set; } - public SpeckleOutput(string name, string value, string guid) - { - this.Name = name; - this.Guid = guid; - this.Value = value; - } + public SpeckleOutput( string name, string value, string guid ) + { + this.Name = name; + this.Guid = guid; + this.Value = value; } + } } diff --git a/SpeckleCore/ModelResponses.cs b/SpeckleCore/Models/ModelResponses.cs similarity index 100% rename from SpeckleCore/ModelResponses.cs rename to SpeckleCore/Models/ModelResponses.cs diff --git a/SpeckleCore/SpeckleApiClientExtension.cs b/SpeckleCore/SpeckleApiClientExtension.cs index b31a190..986c17b 100644 --- a/SpeckleCore/SpeckleApiClientExtension.cs +++ b/SpeckleCore/SpeckleApiClientExtension.cs @@ -72,6 +72,15 @@ public SpeckleApiClient( string baseUrl, bool isPersistent = false ) : base() SetReadyTimer(); } + /// + /// Initialises this client as a receiver for a specific stream. + /// + /// + /// + /// + /// + /// + /// public async Task IntializeReceiver( string streamId, string documentName, string documentType, string documentGuid, string authToken = null ) { if ( Role != null ) @@ -104,7 +113,14 @@ public async Task IntializeReceiver( string streamId, string documentName, strin } - + /// + /// Initialises this client as a Sender by creating a new stream. + /// + /// + /// + /// + /// + /// public async Task IntializeSender( string authToken, string documentName, string documentType, string documentGuid ) { if ( Role != null ) @@ -186,6 +202,9 @@ private void SetWsReconnectTimer( ) }; } + /// + /// Sets up the websocket client & its events.. + /// public void SetupWebsocket( ) { SetWsReconnectTimer(); @@ -231,6 +250,11 @@ public void SetupWebsocket( ) WebsocketClient.Connect(); } + /// + /// Sends a direct message to another websocket client. + /// + /// The clientId of the socket you want to send the message to. + /// What you want to send. Make it serialisable and small. public void SendMessage( string receipientId, dynamic args ) { if ( !WsConnected ) @@ -250,7 +274,12 @@ public void SendMessage( string receipientId, dynamic args ) WebsocketClient.Send( JsonConvert.SerializeObject( eventData ) ); } - + + /// + /// Broadcasts a message to the default streamId room. + /// + /// The message. Make it serialisable and small. + [Obsolete( "This method will be deprecated in favour of BroadcastMessage( string resourceType, string resourceId, dynamic args )" )] public void BroadcastMessage( dynamic args ) { if ( !WsConnected ) @@ -270,6 +299,37 @@ public void BroadcastMessage( dynamic args ) WebsocketClient.Send( JsonConvert.SerializeObject( eventData ) ); } + /// + /// Broadcasts a message in a specific websocket room, as defined by resourceType and resourceId. + /// + /// Can be stream, object, project, comment, user. + /// The database id of the resource. + /// The message. Make it serialisable and small. + public void BroadcastMessage( string resourceType, string resourceId, dynamic args ) + { + if ( !WsConnected ) + { + OnError?.Invoke( this, new SpeckleEventArgs() { EventName = "Websocket client not connected.", EventData = "Websocket client not connected." } ); + return; + } + + var eventData = new + { + eventName = "broadcast", + senderId = ClientId, + resourceType = resourceType, + resourceId = resourceId, + args = args + }; + + WebsocketClient.Send( JsonConvert.SerializeObject( eventData ) ); + } + + /// + /// Joins a websocket room based on a streamId. + /// + /// The streamId you want to join. + [Obsolete("This method will be deprecated in favour of JoinRoom(resourceType, resourceId).")] public void JoinRoom( string streamId ) { if ( !WsConnected ) @@ -281,13 +341,42 @@ public void JoinRoom( string streamId ) var eventData = new { eventName = "join", - senderId = ClientId, + senderId = ClientId, streamId = streamId }; WebsocketClient.Send( JsonConvert.SerializeObject( eventData ) ); } + /// + /// Joins a websocket room based a resource type and its id. This will subscribe you to any broadcasts in that room. + /// + /// Can be stream, object, project, comment, user. + /// The database id of the resource. + public void JoinRoom( string resourceType, string resourceId ) + { + if ( !WsConnected ) + { + OnError?.Invoke( this, new SpeckleEventArgs() { EventName = "Websocket client not connected.", EventData = "Websocket client not connected." } ); + return; + } + + var eventData = new + { + eventName = "join", + senderId = ClientId, + resourceType = resourceType, + resourceId = resourceId + }; + + WebsocketClient.Send( JsonConvert.SerializeObject( eventData ) ); + } + + /// + /// Leaves a room based on its streamId. + /// + /// + [Obsolete( "This method will be deprecated in favour of LeaveRoom(resourceType, resourceId)." )] public void LeaveRoom( string streamId ) { if ( !WsConnected ) @@ -306,6 +395,29 @@ public void LeaveRoom( string streamId ) WebsocketClient.Send( JsonConvert.SerializeObject( eventData ) ); } + /// + /// Leaves a websocket room based a resource type and its id. This will stop you from receiving any broadcasts in that room. + /// + /// Can be stream, object, project, comment, user. + /// The database id of the resource. + public void LeaveRoom( string resourceType, string resourceId ) + { + if ( !WsConnected ) + { + OnError?.Invoke( this, new SpeckleEventArgs() { EventName = "Websocket client not connected.", EventData = "Websocket client not connected." } ); + return; + } + + var eventData = new + { + eventName = "leave", + resourceType = resourceType, + resourceId = resourceId + }; + + WebsocketClient.Send( JsonConvert.SerializeObject( eventData ) ); + } + public void LogError( SpeckleException err ) { OnError?.Invoke( this, new SpeckleEventArgs() { EventName = err.StatusCode.ToString(), EventData = err.Message, EventObject = err } ); @@ -325,9 +437,37 @@ protected SpeckleApiClient( SerializationInfo info, StreamingContext context ) BaseUrl = info.GetString( "BaseUrl" ); StreamId = info.GetString( "StreamId" ); Role = ( ClientRole ) info.GetInt32( "Role" ); - AuthToken = info.GetString( "ApiToken" ); ClientId = info.GetString( "ClientId" ); + //AuthToken = info.GetString( "ApiToken" ); + //string userEmail = null; + + // old clients will not have a user email field :/ + try + { + var userEmail = info.GetString( "UserEmail" ); + var acc = LocalContext.GetAccountByEmailAndRestApi( userEmail, BaseUrl ); + if ( acc != null ) + { + AuthToken = acc.Token; + User = new User() { Email = acc.Email }; + } + } + catch + { + var accs = LocalContext.GetAccountsByRestApi( BaseUrl ); + var sorted = accs.OrderByDescending( acc => acc.IsDefault ).ToList(); + if ( sorted.Count == 0 ) + { + throw new Exception( "You do not have an account that matches this stream's server." ); + } + else + { + AuthToken = accs[ 0 ].Token; + User = new User() { Email = sorted[ 0 ].Email }; + } + } + Stream = StreamGetAsync( StreamId, null ).Result.Resource; // does not need waiting for, as we already have a clientid. @@ -340,11 +480,13 @@ protected SpeckleApiClient( SerializationInfo info, StreamingContext context ) public void GetObjectData( SerializationInfo info, StreamingContext context ) { + info.AddValue( "UserEmail", User.Email ); info.AddValue( "BaseUrl", BaseUrl ); info.AddValue( "StreamId", StreamId ); info.AddValue( "Role", Role ); - info.AddValue( "ApiToken", AuthToken ); info.AddValue( "ClientId", ClientId ); + + //info.AddValue( "ApiToken", AuthToken ); } public void Dispose( bool delete = false ) @@ -357,8 +499,7 @@ public void Dispose( bool delete = false ) WebsocketClient?.Close(); return; } - - ClientUpdateAsync( ClientId, new AppClient() { Online = false, Deleted = true } ); + ClientDeleteAsync( ClientId ); WebsocketClient?.Close(); } } diff --git a/SpeckleCore/SpeckleCore.csproj b/SpeckleCore/SpeckleCore.csproj index 92f45d8..c0f57f8 100644 --- a/SpeckleCore/SpeckleCore.csproj +++ b/SpeckleCore/SpeckleCore.csproj @@ -33,6 +33,7 @@ + @@ -42,15 +43,17 @@ - - - - - - - + + + + + + + + + - + @@ -59,6 +62,9 @@ 11.0.1 + + 1.5.231 + 1.0.3-rc11