From 7a1ff211a01319597aaf9eae4e20a11e9484bf37 Mon Sep 17 00:00:00 2001 From: Gary Sharp Date: Sun, 4 Dec 2022 13:34:55 +1100 Subject: [PATCH] feature: user contact details --- Disco.Models/Disco.Models.csproj | 2 + Disco.Models/Services/Messaging/Email.cs | 9 +- .../Services/Users/Contact/UserContact.cs | 161 ++++++++++++++++++ .../Services/Users/Contact/UserContactType.cs | 14 ++ Disco.Services/Disco.Services.csproj | 2 + .../DetailsProvider/UserContactFeature.cs | 13 ++ .../Users/Contact/UserContactService.cs | 104 +++++++++++ 7 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 Disco.Models/Services/Users/Contact/UserContact.cs create mode 100644 Disco.Models/Services/Users/Contact/UserContactType.cs create mode 100644 Disco.Services/Plugins/Features/DetailsProvider/UserContactFeature.cs create mode 100644 Disco.Services/Users/Contact/UserContactService.cs diff --git a/Disco.Models/Disco.Models.csproj b/Disco.Models/Disco.Models.csproj index 0e311e84..8112b623 100644 --- a/Disco.Models/Disco.Models.csproj +++ b/Disco.Models/Disco.Models.csproj @@ -146,6 +146,8 @@ + + diff --git a/Disco.Models/Services/Messaging/Email.cs b/Disco.Models/Services/Messaging/Email.cs index a7ba8ad0..e12ad07c 100644 --- a/Disco.Models/Services/Messaging/Email.cs +++ b/Disco.Models/Services/Messaging/Email.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; namespace Disco.Models.Services.Messaging { @@ -27,5 +29,10 @@ namespace Disco.Models.Services.Messaging Subject = subject; Body = body; } + + public static IEnumerable ParseEmailAddresses(string emailAddresses) + { + return emailAddresses.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()); + } } } diff --git a/Disco.Models/Services/Users/Contact/UserContact.cs b/Disco.Models/Services/Users/Contact/UserContact.cs new file mode 100644 index 00000000..f773f2c8 --- /dev/null +++ b/Disco.Models/Services/Users/Contact/UserContact.cs @@ -0,0 +1,161 @@ +using Disco.Models.Repository; +using System; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Disco.Models.Services.Users.Contact +{ + public abstract class UserContact + { + public User User { get; } + public UserContactType ContactType { get; } + public string Source { get; } + public string Name { get; } + + public abstract string Value { get; } + + public UserContact(User user, UserContactType contactType, string source, string name) + { + User = user; + ContactType = contactType; + Source = source; + Name = name; + } + + protected static bool TryParse(User user, string source, Regex validator, string value, Func generator, out T instance) where T : UserContact + { + if (string.IsNullOrWhiteSpace(value)) + { + instance = null; + return false; + } + + var match = validator.Match(value); + + if (!match.Success) + { + instance = null; + return false; + } + + var result = match.Value; + var name = default(string); + if (match.Index > 0) + { + name = value.Substring(0, match.Index).Trim(); + if (name.Length > 0) + { + switch (name.Last()) + { + case '<': + case '[': + case '(': + case '{': + case '-': + name = name.Substring(0, name.Length - 1).Trim(); + break; + } + } + if (name.Length == 0) + name = default; + } + + instance = generator(user, source, name, result); + return true; + } + + public override string ToString() + { + if (!string.IsNullOrWhiteSpace(Name)) + { + return $"{Name} <{Value}>"; + } + else + { + return Value; + } + } + } + + public sealed class UserContactEmail : UserContact + { + private static Regex validator = new Regex(@"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public string EmailAddress { get; } + public override string Value => EmailAddress; + + public UserContactEmail(User user, string source, string name, string emailAddress) + : base(user, UserContactType.Email, source, name) + { + if (string.IsNullOrWhiteSpace(emailAddress)) + throw new ArgumentNullException(nameof(emailAddress)); + + EmailAddress = emailAddress; + } + + public static bool TryParse(User user, string source, string value, out UserContactEmail contact) => + TryParse(user, source, validator, value, + (u, s, name, emailAddress) => new UserContactEmail(u, s, name, emailAddress), + out contact); + } + + public sealed class UserContactAustralianPhone : UserContact + { + private static Regex validator = new Regex(@"(?:\+?61\s*[0-9][ \-\.]*?|0[0-9][ \-\.]*?|[(\[]\s*0[0-9]\s*[)\]])\s*(?:[0-9][ \-\.]*?){8}(?=\s*[>\]})\-]?\s*$)", RegexOptions.Compiled); + + public string PhoneNumber { get; } + public override string Value => PhoneNumber; + + public UserContactAustralianPhone(User user, string source, string name, string phoneNumber) + : base(user, UserContactType.Phone, source, name) + { + if (string.IsNullOrWhiteSpace(phoneNumber)) + throw new ArgumentNullException(nameof(phoneNumber)); + + PhoneNumber = phoneNumber; + } + + public static bool TryParse(User user, string source, string value, out UserContactAustralianPhone contact) => + TryParse(user, source, validator, value, + (u, s, name, phoneNumber) => new UserContactAustralianPhone(u, s, name, phoneNumber), + out contact); + + public override string ToString() + { + if (!string.IsNullOrWhiteSpace(Name)) + return $"{Name} <{PhoneNumber}>"; + else + return PhoneNumber; + } + } + + public sealed class UserContactAustralianMobile : UserContact + { + private static Regex validator = new Regex(@"(?:\+?61\s*4[ \-\.]*?|04[ \-\.]*?|[(\[]\s*04\s*[)\]])\s*(?:[0-9][ \-\.]*?){8}(?=\s*[>\]})\-]?\s*$)", RegexOptions.Compiled); + + public string PhoneNumber { get; } + public override string Value => PhoneNumber; + + public UserContactAustralianMobile(User user, string source, string name, string phoneNumber) + : base(user, UserContactType.MobilePhone, source, name) + { + if (string.IsNullOrWhiteSpace(phoneNumber)) + throw new ArgumentNullException(nameof(phoneNumber)); + + PhoneNumber = phoneNumber; + } + + public static bool TryParse(User user, string source, string value, out UserContactAustralianPhone contact) => + TryParse(user, source, validator, value, + (u, s, name, phoneNumber) => new UserContactAustralianPhone(u, s, name, phoneNumber), + out contact); + + public override string ToString() + { + if (!string.IsNullOrWhiteSpace(Name)) + return $"{Name} <{PhoneNumber}>"; + else + return PhoneNumber; + } + } +} diff --git a/Disco.Models/Services/Users/Contact/UserContactType.cs b/Disco.Models/Services/Users/Contact/UserContactType.cs new file mode 100644 index 00000000..965a1b2f --- /dev/null +++ b/Disco.Models/Services/Users/Contact/UserContactType.cs @@ -0,0 +1,14 @@ +using System; + +namespace Disco.Models.Services.Users.Contact +{ + [Flags] + public enum UserContactType + { + Email = 1, + MobilePhone, + Phone, + AddressMail, + AddressHome, + } +} diff --git a/Disco.Services/Disco.Services.csproj b/Disco.Services/Disco.Services.csproj index 3d978e29..35313cca 100644 --- a/Disco.Services/Disco.Services.csproj +++ b/Disco.Services/Disco.Services.csproj @@ -396,6 +396,7 @@ + @@ -456,6 +457,7 @@ + diff --git a/Disco.Services/Plugins/Features/DetailsProvider/UserContactFeature.cs b/Disco.Services/Plugins/Features/DetailsProvider/UserContactFeature.cs new file mode 100644 index 00000000..68fe42a2 --- /dev/null +++ b/Disco.Services/Plugins/Features/DetailsProvider/UserContactFeature.cs @@ -0,0 +1,13 @@ +using Disco.Data.Repository; +using Disco.Models.Repository; +using Disco.Models.Services.Users.Contact; +using System.Collections.Generic; + +namespace Disco.Services.Plugins.Features.DetailsProvider +{ + [PluginFeatureCategory(DisplayName = "User Contact Providers")] + public abstract class UserContactFeature : PluginFeature + { + public abstract IEnumerable GetContacts(DiscoDataContext database, User user, UserContactType? contactType = null); + } +} diff --git a/Disco.Services/Users/Contact/UserContactService.cs b/Disco.Services/Users/Contact/UserContactService.cs new file mode 100644 index 00000000..06b1ec4e --- /dev/null +++ b/Disco.Services/Users/Contact/UserContactService.cs @@ -0,0 +1,104 @@ +using Disco.Data.Repository; +using Disco.Models.Repository; +using Disco.Models.Services.Users.Contact; +using Disco.Services.Plugins.Features.DetailsProvider; +using System.Collections.Generic; +using System.Linq; +using ZXing; + +namespace Disco.Services.Users.Contact +{ + public static class UserContactService + { + public static List GetContacts(DiscoDataContext database, User user) + => GetContacts(database, user, null); + + public static List GetContacts(DiscoDataContext database, User user, UserContactType? contactType = null) + { + var contacts = new List(); + + if (!contactType.HasValue || contactType.Value.HasFlag(UserContactType.Email)) + { + if (!string.IsNullOrWhiteSpace(user.EmailAddress) && + UserContactEmail.TryParse(user, "Active Directory", $"{user.DisplayName} <{user.EmailAddress}>", out var contact)) + { + contacts.Add(contact); + } + } + + var foundMobilePhone = false; + if (!contactType.HasValue || contactType.Value.HasFlag(UserContactType.MobilePhone)) + { + if (!string.IsNullOrWhiteSpace(user.PhoneNumber) && + UserContactAustralianMobile.TryParse(user, "Active Directory", $"{user.DisplayName} <{user.PhoneNumber}>", out var contact)) + { + contacts.Add(contact); + foundMobilePhone = true; + } + } + + if (!foundMobilePhone && (!contactType.HasValue || contactType.Value.HasFlag(UserContactType.Phone))) + { + if (!string.IsNullOrWhiteSpace(user.PhoneNumber) && + UserContactAustralianPhone.TryParse(user, "Active Directory", $"{user.DisplayName} <{user.PhoneNumber}>", out var contact)) + { + contacts.Add(contact); + } + } + + // from plugin feature + var features = Plugins.Plugins.GetPluginFeatures(typeof(UserContactFeature)); + foreach (var feature in features) + { + var instance = feature.CreateInstance(); + contacts.AddRange(instance.GetContacts(database, user, contactType)); + } + + // from user details + contacts.AddRange(GetContactsFromUserDetails(database, user, contactType)); + + return contacts; + } + + public static IEnumerable GetContactsFromUserDetails(DiscoDataContext database, User user, UserContactType? contactType = null) + { + var service = new DetailsProviderService(database); + + user = database.Users.First(u => u.UserId == user.UserId); + var details = service.GetDetails(user); + + if ((details?.Details?.Count ?? 0) == 0) + yield break; + + foreach (var item in details.Details) + { + if (!contactType.HasValue || contactType.Value.HasFlag(UserContactType.Email)) + { + if (UserContactEmail.TryParse(user, item.Key, item.Value, out var contact)) + { + yield return contact; + continue; + } + } + + if (!contactType.HasValue || contactType.Value.HasFlag(UserContactType.MobilePhone)) + { + if (UserContactAustralianMobile.TryParse(user, item.Key, item.Value, out var contact)) + { + yield return contact; + continue; + } + } + + if (!contactType.HasValue || contactType.Value.HasFlag(UserContactType.Phone)) + { + if (UserContactAustralianPhone.TryParse(user, item.Key, item.Value, out var contact)) + { + yield return contact; + continue; + } + } + } + } + } +}