diff --git a/src/Eurofurence.App.Domain.Model.MongoDb/DependencyResolution/AutofacModule.cs b/src/Eurofurence.App.Domain.Model.MongoDb/DependencyResolution/AutofacModule.cs index 5f9f6f16..3bcc60cb 100644 --- a/src/Eurofurence.App.Domain.Model.MongoDb/DependencyResolution/AutofacModule.cs +++ b/src/Eurofurence.App.Domain.Model.MongoDb/DependencyResolution/AutofacModule.cs @@ -11,10 +11,12 @@ using Eurofurence.App.Domain.Model.Fursuits.CollectingGame; using Eurofurence.App.Domain.Model.Images; using Eurofurence.App.Domain.Model.Knowledge; +using Eurofurence.App.Domain.Model.LostAndFound; using Eurofurence.App.Domain.Model.Maps; using Eurofurence.App.Domain.Model.MongoDb.ArtShow; using Eurofurence.App.Domain.Model.MongoDb.Fursuits; using Eurofurence.App.Domain.Model.MongoDb.Fursuits.CollectingGame; +using Eurofurence.App.Domain.Model.MongoDb.LostAndFound; using Eurofurence.App.Domain.Model.MongoDb.Repositories; using Eurofurence.App.Domain.Model.MongoDb.Security; using Eurofurence.App.Domain.Model.MongoDb.Telegram; @@ -174,6 +176,7 @@ protected override void Load(ContainerBuilder builder) Builders.IndexKeys.Ascending(a => a.ImportHash)))); Register, TableRegistrationRecord>(builder); + Register, LostAndFoundRecord>(builder); } } } \ No newline at end of file diff --git a/src/Eurofurence.App.Domain.Model.MongoDb/LostAndFound/LostAndFoundRepository.cs b/src/Eurofurence.App.Domain.Model.MongoDb/LostAndFound/LostAndFoundRepository.cs new file mode 100644 index 00000000..d6995905 --- /dev/null +++ b/src/Eurofurence.App.Domain.Model.MongoDb/LostAndFound/LostAndFoundRepository.cs @@ -0,0 +1,14 @@ +using Eurofurence.App.Domain.Model.LostAndFound; +using Eurofurence.App.Domain.Model.MongoDb.Repositories; +using MongoDB.Driver; + +namespace Eurofurence.App.Domain.Model.MongoDb.LostAndFound +{ + public class LostAndFoundRepository : MongoDbEntityRepositoryBase + { + public LostAndFoundRepository(IMongoCollection collection) + : base(collection) + { + } + } +} \ No newline at end of file diff --git a/src/Eurofurence.App.Domain.Model/LostAndFound/LostAndFoundRecord.cs b/src/Eurofurence.App.Domain.Model/LostAndFound/LostAndFoundRecord.cs new file mode 100644 index 00000000..fd20a3a2 --- /dev/null +++ b/src/Eurofurence.App.Domain.Model/LostAndFound/LostAndFoundRecord.cs @@ -0,0 +1,44 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Eurofurence.App.Domain.Model.LostAndFound +{ + [DataContract] + public class LostAndFoundRecord : EntityBase + { + public enum LostAndFoundStatusEnum + { + Unknown, + Lost, + Found, + Returned + } + + [Required] + [DataMember] + public int ExternalId { get; set; } + + [DataMember] + public string ImageUrl { get; set; } + + [DataMember] + public string Title { get; set; } + + [DataMember] + public string Description { get; set; } + + [DataMember] + public LostAndFoundStatusEnum Status { get; set; } + + [DataMember] + public DateTime? LostDateTimeUtc { get; set; } + [DataMember] + public DateTime? FoundDateTimeUtc { get; set; } + [DataMember] + public DateTime? ReturnDateTimeUtc { get; set; } + + + + } +} \ No newline at end of file diff --git a/src/Eurofurence.App.Server.Services/Abstractions/Lassie/ILassieApiClient.cs b/src/Eurofurence.App.Server.Services/Abstractions/Lassie/ILassieApiClient.cs new file mode 100644 index 00000000..7c78d65b --- /dev/null +++ b/src/Eurofurence.App.Server.Services/Abstractions/Lassie/ILassieApiClient.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Eurofurence.App.Server.Services.Abstractions.Lassie +{ + public interface ILassieApiClient + { + public Task QueryLostAndFoundDbAsync(string command = "lostandfound"); + } +} diff --git a/src/Eurofurence.App.Server.Services/Abstractions/Lassie/LassieConfiguration.cs b/src/Eurofurence.App.Server.Services/Abstractions/Lassie/LassieConfiguration.cs new file mode 100644 index 00000000..a6029b50 --- /dev/null +++ b/src/Eurofurence.App.Server.Services/Abstractions/Lassie/LassieConfiguration.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Configuration; + +namespace Eurofurence.App.Server.Services.Abstractions.Lassie +{ + public class LassieConfiguration + { + public bool IsConfigured => !string.IsNullOrEmpty(ApiKey); + public string BaseApiUrl { get; set; } + public string ApiKey { get; set; } + + public static LassieConfiguration FromConfiguration(IConfiguration configuration) + => new LassieConfiguration + { + BaseApiUrl = configuration["lassie:baseApiUrl"], + ApiKey = configuration["lassie:apiKey"] + }; + } +} diff --git a/src/Eurofurence.App.Server.Services/Abstractions/Lassie/LostAndFoundResponse.cs b/src/Eurofurence.App.Server.Services/Abstractions/Lassie/LostAndFoundResponse.cs new file mode 100644 index 00000000..140dff86 --- /dev/null +++ b/src/Eurofurence.App.Server.Services/Abstractions/Lassie/LostAndFoundResponse.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; +using System; + +namespace Eurofurence.App.Server.Services.Abstractions.Lassie +{ + public class LostAndFoundResponse + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("image")] + public string ImageUrl { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("lost_timestamp")] + public DateTime? LostDateTimeLocal { get; set; } + + [JsonProperty("found_timestamp")] + public DateTime? FoundDateTimeLocal { get; set; } + + [JsonProperty("return_timestamp")] + public DateTime? ReturnDateTimeLocal { get; set; } + } +} diff --git a/src/Eurofurence.App.Server.Services/Abstractions/LostAndFound/ILostAndFoundLassieImporter.cs b/src/Eurofurence.App.Server.Services/Abstractions/LostAndFound/ILostAndFoundLassieImporter.cs new file mode 100644 index 00000000..09df9e7c --- /dev/null +++ b/src/Eurofurence.App.Server.Services/Abstractions/LostAndFound/ILostAndFoundLassieImporter.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Eurofurence.App.Server.Services.Abstractions.LostAndFound +{ + public interface ILostAndFoundLassieImporter + { + Task RunImportAsync(); + } +} diff --git a/src/Eurofurence.App.Server.Services/Abstractions/LostAndFound/ILostAndFoundService.cs b/src/Eurofurence.App.Server.Services/Abstractions/LostAndFound/ILostAndFoundService.cs new file mode 100644 index 00000000..c7a3493b --- /dev/null +++ b/src/Eurofurence.App.Server.Services/Abstractions/LostAndFound/ILostAndFoundService.cs @@ -0,0 +1,10 @@ +using Eurofurence.App.Domain.Model.LostAndFound; + +namespace Eurofurence.App.Server.Services.Abstractions.LostAndFound +{ + public interface ILostAndFoundService : + IEntityServiceOperations, + IPatchOperationProcessor + { + } +} \ No newline at end of file diff --git a/src/Eurofurence.App.Server.Services/DependencyResolution/AutofacModule.cs b/src/Eurofurence.App.Server.Services/DependencyResolution/AutofacModule.cs index fbe9413f..04c6d35b 100644 --- a/src/Eurofurence.App.Server.Services/DependencyResolution/AutofacModule.cs +++ b/src/Eurofurence.App.Server.Services/DependencyResolution/AutofacModule.cs @@ -9,6 +9,8 @@ using Eurofurence.App.Server.Services.Abstractions.Fursuits; using Eurofurence.App.Server.Services.Abstractions.Images; using Eurofurence.App.Server.Services.Abstractions.Knowledge; +using Eurofurence.App.Server.Services.Abstractions.Lassie; +using Eurofurence.App.Server.Services.Abstractions.LostAndFound; using Eurofurence.App.Server.Services.Abstractions.Maps; using Eurofurence.App.Server.Services.Abstractions.PushNotifications; using Eurofurence.App.Server.Services.Abstractions.Security; @@ -23,6 +25,8 @@ using Eurofurence.App.Server.Services.Fursuits; using Eurofurence.App.Server.Services.Images; using Eurofurence.App.Server.Services.Knowledge; +using Eurofurence.App.Server.Services.Lassie; +using Eurofurence.App.Server.Services.LostAndFound; using Eurofurence.App.Server.Services.Maps; using Eurofurence.App.Server.Services.PushNotifications; using Eurofurence.App.Server.Services.Security; @@ -60,6 +64,7 @@ private void RegisterConfigurations(ContainerBuilder builder) builder.RegisterInstance(TelegramConfiguration.FromConfiguration(_configuration)); builder.RegisterInstance(CollectionGameConfiguration.FromConfiguration(_configuration)); builder.RegisterInstance(ArtistAlleyConfiguration.FromConfiguration(_configuration)); + builder.RegisterInstance(LassieConfiguration.FromConfiguration(_configuration)); } private void RegisterServices(ContainerBuilder builder) @@ -83,7 +88,10 @@ private void RegisterServices(ContainerBuilder builder) builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As(); + builder.RegisterType().As(); builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType() .As() diff --git a/src/Eurofurence.App.Server.Services/Lassie/LassieApiClient.cs b/src/Eurofurence.App.Server.Services/Lassie/LassieApiClient.cs new file mode 100644 index 00000000..e9b3ca03 --- /dev/null +++ b/src/Eurofurence.App.Server.Services/Lassie/LassieApiClient.cs @@ -0,0 +1,44 @@ +using Eurofurence.App.Server.Services.Abstractions.Lassie; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Eurofurence.App.Server.Services.Lassie +{ + public class LassieApiClient : ILassieApiClient + { + private class DataResponseWrapper + { + public T[] Data { get; set; } + } + + private LassieConfiguration _configuration; + + public LassieApiClient(LassieConfiguration configuration) + { + _configuration = configuration; + } + + public async Task QueryLostAndFoundDbAsync(string command = "lostandfound") + { + var outgoingQuery = new List>() + { + new KeyValuePair("apikey", _configuration.ApiKey), + new KeyValuePair("request", "lostandfounddb"), + new KeyValuePair("command", command) + }; + + using (var client = new HttpClient()) + { + var response = await client.PostAsync(_configuration.BaseApiUrl, new FormUrlEncodedContent(outgoingQuery)); + var content = await response.Content.ReadAsStringAsync(); + + var dataResponse = JsonConvert.DeserializeObject>(content, + new JsonSerializerSettings() { DateTimeZoneHandling = DateTimeZoneHandling.Local }); + + return dataResponse.Data; + } + } + } +} \ No newline at end of file diff --git a/src/Eurofurence.App.Server.Services/LostAndFound/LostAndFoundLassieImporter.cs b/src/Eurofurence.App.Server.Services/LostAndFound/LostAndFoundLassieImporter.cs new file mode 100644 index 00000000..6938f732 --- /dev/null +++ b/src/Eurofurence.App.Server.Services/LostAndFound/LostAndFoundLassieImporter.cs @@ -0,0 +1,59 @@ +using Eurofurence.App.Common.DataDiffUtils; +using Eurofurence.App.Domain.Model.LostAndFound; +using Eurofurence.App.Server.Services.Abstractions.Lassie; +using Eurofurence.App.Server.Services.Abstractions.LostAndFound; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Eurofurence.App.Server.Services.LostAndFound +{ + public class LostAndFoundLassieImporter : ILostAndFoundLassieImporter + { + private ILostAndFoundService _lostAndFoundService; + private ILassieApiClient _lassieApiClient; + + public LostAndFoundLassieImporter( + ILostAndFoundService lostAndFoundService, + ILassieApiClient lassieApiClient + ) + { + _lostAndFoundService = lostAndFoundService; + _lassieApiClient = lassieApiClient; + } + + public async Task RunImportAsync() + { + var existingRecords = await _lostAndFoundService.FindAllAsync(); + var newRecords = await _lassieApiClient.QueryLostAndFoundDbAsync(); + + var patch = new PatchDefinition((source, list) => + list.SingleOrDefault(a => a.ExternalId == source.Id)); + + var statusConverter = new Func(input => + { + switch(input.ToLowerInvariant()) + { + case "l": return LostAndFoundRecord.LostAndFoundStatusEnum.Lost; + case "f": return LostAndFoundRecord.LostAndFoundStatusEnum.Found; + case "r": return LostAndFoundRecord.LostAndFoundStatusEnum.Returned; + default: return LostAndFoundRecord.LostAndFoundStatusEnum.Unknown; + } + }); + + patch + .Map(s => s.Id, t => t.ExternalId) + .Map(s => s.Title, t => t.Title) + .Map(s => s.Description, t => t.Description) + .Map(s => s.ImageUrl, t => t.ImageUrl) + .Map(s => statusConverter(s.Status), t => t.Status) + .Map(s => s.LostDateTimeLocal?.ToUniversalTime(), t => t.LostDateTimeUtc) + .Map(s => s.ReturnDateTimeLocal?.ToUniversalTime(), t => t.ReturnDateTimeUtc) + .Map(s => s.FoundDateTimeLocal?.ToUniversalTime(), t => t.FoundDateTimeUtc); + + var patchResult = patch.Patch(newRecords, existingRecords); + + await _lostAndFoundService.ApplyPatchOperationAsync(patchResult); + } + } +} diff --git a/src/Eurofurence.App.Server.Services/LostAndFound/LostAndFoundService.cs b/src/Eurofurence.App.Server.Services/LostAndFound/LostAndFoundService.cs new file mode 100644 index 00000000..5f47e680 --- /dev/null +++ b/src/Eurofurence.App.Server.Services/LostAndFound/LostAndFoundService.cs @@ -0,0 +1,18 @@ +using Eurofurence.App.Domain.Model.Abstractions; +using Eurofurence.App.Domain.Model.LostAndFound; +using Eurofurence.App.Server.Services.Abstractions; +using Eurofurence.App.Server.Services.Abstractions.LostAndFound; + +namespace Eurofurence.App.Server.Services.LostAndFound +{ + public class LostAndFoundService : EntityServiceBase, ILostAndFoundService + { + public LostAndFoundService( + IEntityRepository entityRepository, + IStorageServiceFactory storageServiceFactory + ) + : base(entityRepository, storageServiceFactory) + { + } + } +} diff --git a/src/Eurofurence.App.Server.Web/Controllers/LostAndFoundController.cs b/src/Eurofurence.App.Server.Web/Controllers/LostAndFoundController.cs new file mode 100644 index 00000000..38cb53e3 --- /dev/null +++ b/src/Eurofurence.App.Server.Web/Controllers/LostAndFoundController.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Eurofurence.App.Domain.Model.LostAndFound; +using Eurofurence.App.Server.Services.Abstractions.LostAndFound; +using Eurofurence.App.Server.Services.Abstractions.Security; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Eurofurence.App.Server.Web.Controllers +{ + [Route("Api/[controller]")] + public class LostAndFoundController : BaseController + { + private readonly ILostAndFoundService _lostAndFoundService; + private readonly IApiPrincipal _apiPrincipal; + + public LostAndFoundController( + ILostAndFoundService lostAndFoundService, + IApiPrincipal apiPrincipal + ) + { + _lostAndFoundService = lostAndFoundService; + _apiPrincipal = apiPrincipal; + } + + [Authorize(Roles = "Attendee")] + [HttpGet("Items")] + public Task> GetItemsAsync() + { + return _lostAndFoundService.FindAllAsync(); + } + } +} \ No newline at end of file diff --git a/src/Eurofurence.App.Server.Web/Jobs/JobRegistry.cs b/src/Eurofurence.App.Server.Web/Jobs/JobRegistry.cs index e561f5ae..5945dcb1 100644 --- a/src/Eurofurence.App.Server.Web/Jobs/JobRegistry.cs +++ b/src/Eurofurence.App.Server.Web/Jobs/JobRegistry.cs @@ -12,6 +12,7 @@ public JobRegistry(IConfiguration configuration) Schedule().ToRunEvery(1).Seconds(); Schedule().ToRunNow().AndEvery(Convert.ToInt32(configuration["updateNews:secondsInterval"])).Seconds(); + Schedule().ToRunNow().AndEvery(60).Seconds(); } } } diff --git a/src/Eurofurence.App.Server.Web/Jobs/UpdateLostAndFoundJob.cs b/src/Eurofurence.App.Server.Web/Jobs/UpdateLostAndFoundJob.cs new file mode 100644 index 00000000..df106c98 --- /dev/null +++ b/src/Eurofurence.App.Server.Web/Jobs/UpdateLostAndFoundJob.cs @@ -0,0 +1,39 @@ +using Eurofurence.App.Server.Services.Abstractions.LostAndFound; +using FluentScheduler; +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; + +namespace Eurofurence.App.Server.Web.Jobs +{ + public class UpdateLostAndFoundJob : IJob + { + private ILostAndFoundLassieImporter _lostAndFoundLassieImporter; + private ILogger _logger; + + public UpdateLostAndFoundJob( + ILoggerFactory loggerFactory, + ILostAndFoundLassieImporter lostAndFoundLassieImporter) + { + _lostAndFoundLassieImporter = lostAndFoundLassieImporter; + _logger = loggerFactory.CreateLogger(GetType()); + } + + public void Execute() + { + try + { + ExecuteAsync().Wait(); + } + catch (Exception e) + { + _logger.LogError("Job failed with exception {Message} {StackTrace}", e.Message, e.StackTrace); + } + } + + private async Task ExecuteAsync() + { + await _lostAndFoundLassieImporter.RunImportAsync(); + } + } +} diff --git a/src/Eurofurence.App.Server.Web/Startup.cs b/src/Eurofurence.App.Server.Web/Startup.cs index b267dd96..9bd78deb 100644 --- a/src/Eurofurence.App.Server.Web/Startup.cs +++ b/src/Eurofurence.App.Server.Web/Startup.cs @@ -193,6 +193,7 @@ public IServiceProvider ConfigureServices(IServiceCollection services) .As(); builder.RegisterType().WithAttributeFiltering().AsSelf(); + builder.RegisterType().WithAttributeFiltering().AsSelf(); builder.RegisterType().AsSelf(); builder.Register(c => Configuration.GetSection("jobs:updateNews")) .Keyed("updateNews").As(); @@ -310,12 +311,12 @@ IHostApplicationLifetime appLifetime c.EnableDeepLinking(); }); - //if (env.IsProduction()) - //{ + if (env.IsProduction()) + { _logger.LogDebug("Starting JobManager to run jobs"); JobManager.JobFactory = new ServiceProviderJobFactory(app.ApplicationServices); JobManager.Initialize(new JobRegistry(Configuration.GetSection("jobs"))); - //} + } _logger.LogInformation($"Startup complete ({env.EnvironmentName})"); }