diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..712664c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,8 @@ +[submodule "messengerapi"] + path = messengerapi + url = https://gitea.masita.net/mc/messengerapi.git + branch = feature/v2 +[submodule "sub/messengerapi"] + path = sub/messengerapi + url = https://gitea.masita.net/mc/messengerapi.git + branch = feature/v2 diff --git a/code/.dockerignore b/code/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/code/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/code/MessengerBroker.Db/BrokerDbContext.cs b/code/MessengerBroker.Db/BrokerDbContext.cs new file mode 100644 index 0000000..53168f8 --- /dev/null +++ b/code/MessengerBroker.Db/BrokerDbContext.cs @@ -0,0 +1,32 @@ +using MessengerBroker.Db.Model; +using Microsoft.EntityFrameworkCore; + +namespace MessengerBroker.Db +{ + public class BrokerDbContext : DbContext + { + private string connectionString; + + public DbSet Syncs { get; } + + public DbSet Users { get; } + + public DbSet Messages { get; } + + public BrokerDbContext(string connectionString) + { + this.connectionString = connectionString; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlServer(this.connectionString); + } + } +} diff --git a/code/MessengerBroker.Db/MessengerBroker.Db.csproj b/code/MessengerBroker.Db/MessengerBroker.Db.csproj new file mode 100644 index 0000000..47c1576 --- /dev/null +++ b/code/MessengerBroker.Db/MessengerBroker.Db.csproj @@ -0,0 +1,14 @@ + + + + net9.0 + enable + enable + + + + + + + + diff --git a/code/MessengerBroker.Db/Model/Contracts/IForeignEntity.cs b/code/MessengerBroker.Db/Model/Contracts/IForeignEntity.cs new file mode 100644 index 0000000..13a1a08 --- /dev/null +++ b/code/MessengerBroker.Db/Model/Contracts/IForeignEntity.cs @@ -0,0 +1,15 @@ +namespace MessengerBroker.Db.Model.Contracts +{ + public interface IForeignEntity + { + Guid Id { get; } + + Guid BrokerId { get; } + + bool IsDeleted { get; } + + Sync? FirstSync { get; } + + Sync? LastSync { get; } + } +} \ No newline at end of file diff --git a/code/MessengerBroker.Db/Model/Message.cs b/code/MessengerBroker.Db/Model/Message.cs new file mode 100644 index 0000000..b33455f --- /dev/null +++ b/code/MessengerBroker.Db/Model/Message.cs @@ -0,0 +1,9 @@ +namespace MessengerBroker.Db.Model +{ + public class Message + { + public Guid Id { get; set; } + + public Guid BrokerId { get; set; } + } +} diff --git a/code/MessengerBroker.Db/Model/Sync.cs b/code/MessengerBroker.Db/Model/Sync.cs new file mode 100644 index 0000000..c60bd81 --- /dev/null +++ b/code/MessengerBroker.Db/Model/Sync.cs @@ -0,0 +1,15 @@ +namespace MessengerBroker.Db.Model +{ + public class Sync + { + public int Id { get; set; } + + public Guid BrokerId { get; set; } + + public DateTime StartedUtc { get; set; } + + public DateTime? FinishedUtc { get; set; } + + public long? Changes { get; set; } + } +} diff --git a/code/MessengerBroker.Db/Model/User.cs b/code/MessengerBroker.Db/Model/User.cs new file mode 100644 index 0000000..9a559dd --- /dev/null +++ b/code/MessengerBroker.Db/Model/User.cs @@ -0,0 +1,11 @@ +using MessengerBroker.Db.Model.Contracts; + +namespace MessengerBroker.Db.Model +{ + public class User + { + public Guid Id { get; set; } + + public Guid BrokerId { get; set; } + } +} \ No newline at end of file diff --git a/code/MessengerBroker.sln b/code/MessengerBroker.sln new file mode 100644 index 0000000..7fdbe7e --- /dev/null +++ b/code/MessengerBroker.sln @@ -0,0 +1,45 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36127.28 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerBroker", "MessengerBroker\MessengerBroker.csproj", "{1DE4146C-00DD-4141-84C6-4B74A2F2D0E1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerBroker.Db", "MessengerBroker.Db\MessengerBroker.Db.csproj", "{52CF80F3-A938-437B-B9DD-5E64A206A641}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Db", "Db", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MessengerApi", "MessengerApi", "{D520DC2F-BD81-4588-8818-D493553ACD3D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Db", "..\sub\messengerapi\code\MessengerApi.Db\MessengerApi.Db.csproj", "{2F366BD9-AA41-7C4B-C6FA-1C5CCDF56E34}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1DE4146C-00DD-4141-84C6-4B74A2F2D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1DE4146C-00DD-4141-84C6-4B74A2F2D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1DE4146C-00DD-4141-84C6-4B74A2F2D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1DE4146C-00DD-4141-84C6-4B74A2F2D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {52CF80F3-A938-437B-B9DD-5E64A206A641}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52CF80F3-A938-437B-B9DD-5E64A206A641}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52CF80F3-A938-437B-B9DD-5E64A206A641}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52CF80F3-A938-437B-B9DD-5E64A206A641}.Release|Any CPU.Build.0 = Release|Any CPU + {2F366BD9-AA41-7C4B-C6FA-1C5CCDF56E34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F366BD9-AA41-7C4B-C6FA-1C5CCDF56E34}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F366BD9-AA41-7C4B-C6FA-1C5CCDF56E34}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F366BD9-AA41-7C4B-C6FA-1C5CCDF56E34}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {52CF80F3-A938-437B-B9DD-5E64A206A641} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {2F366BD9-AA41-7C4B-C6FA-1C5CCDF56E34} = {D520DC2F-BD81-4588-8818-D493553ACD3D} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F0F93DDE-2CDC-4A7D-9D70-A7A12B3AF9CE} + EndGlobalSection +EndGlobal diff --git a/code/MessengerBroker/Dockerfile b/code/MessengerBroker/Dockerfile new file mode 100644 index 0000000..3377d36 --- /dev/null +++ b/code/MessengerBroker/Dockerfile @@ -0,0 +1,29 @@ +# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +# This stage is used when running from VS in fast mode (Default for Debug configuration) +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 + + +# This stage is used to build the service project +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["MessengerBroker/MessengerBroker.csproj", "MessengerBroker/"] +RUN dotnet restore "./MessengerBroker/MessengerBroker.csproj" +COPY . . +WORKDIR "/src/MessengerBroker" +RUN dotnet build "./MessengerBroker.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# This stage is used to publish the service project to be copied to the final stage +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./MessengerBroker.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "MessengerBroker.dll"] \ No newline at end of file diff --git a/code/MessengerBroker/Extensions/EntityExtensions.cs b/code/MessengerBroker/Extensions/EntityExtensions.cs new file mode 100644 index 0000000..c01be5b --- /dev/null +++ b/code/MessengerBroker/Extensions/EntityExtensions.cs @@ -0,0 +1,22 @@ +using System.Linq.Expressions; + +namespace MessengerBroker +{ + public static class EntityExtensions + { + public static IQueryable GetUsers(this IQueryable source, Guid[] guids) + { + return source.Where(x => guids.Any(g => g == x.Id)); + } + + public static IQueryable GetUsersExcept(this IQueryable source, IEnumerable guids) + { + return source.Where(x => !guids.Any(g => g == x.Id)); + } + + public static bool GetUserExists(this IQueryable source, Guid id) + { + return source.Any(x=> x.Id == id); + } + } +} diff --git a/code/MessengerBroker/Handlers/AuthHandler.cs b/code/MessengerBroker/Handlers/AuthHandler.cs new file mode 100644 index 0000000..86777a4 --- /dev/null +++ b/code/MessengerBroker/Handlers/AuthHandler.cs @@ -0,0 +1,28 @@ +namespace MessengerBroker.Handlers +{ + public class AuthHandler + { + private readonly Settings settings; + + public AuthHandler(Settings settings) + { + this.settings = settings; + } + + public Guid? Auth(HttpContext context) + { + var authHeader = context.Request.Headers["Authorization"].ToString(); + + if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ")) + { + var token = authHeader.Substring("Bearer ".Length).Trim(); + if (Guid.TryParse(token, out Guid brokerId) && this.settings.Slaves.Any(x => x.BrokerId == brokerId)) + { + return brokerId; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/code/MessengerBroker/Handlers/DataHandler.cs b/code/MessengerBroker/Handlers/DataHandler.cs new file mode 100644 index 0000000..f3e9a1b --- /dev/null +++ b/code/MessengerBroker/Handlers/DataHandler.cs @@ -0,0 +1,81 @@ +using MessengerApi.Db; +using MessengerBroker.Db; +using Microsoft.EntityFrameworkCore; + +namespace MessengerBroker.Handlers +{ + public class DataHandler + { + private readonly Settings _settings; + + public Task> GetLocalUsersAndRoutes() + { + var foreignUserIds = (Guid[])null; + using(var broCtx = new BrokerDbContext(this._settings.MessengerBrokerDbConnectionString)) + { + foreignUserIds = broCtx.Users.Select(x => x.Id).ToArray(); + } + + using (var apiCtx = new MessengerDbContext(this._settings.MessengerApiDbConnectionString)) + { + var localUsers = apiCtx.Users + .Where(x => !foreignUserIds.Any(f => f == x.Id)) + .ToArray(); + var localRoutes = apiCtx.UserRoutes + .Include(x => x.From) + .Include(x => x.To) + .Where(x => localUsers.Any(l => l.Id == x.From.Id) && localUsers.Any(l => l.Id == x.To.Id)) + .ToArray(); + + return Task.FromResult(new Tuple( + localUsers, + localRoutes)); + } + } + + private Task GetForeignUsers(Guid brokerId) + { + var foreignUserIds = (Guid[])null; + using (var broCtx = new BrokerDbContext(this._settings.MessengerBrokerDbConnectionString)) + { + foreignUserIds = broCtx.Users.Where(x=>x.BrokerId == brokerId).Select(x => x.Id).ToArray(); + } + + using (var apiCtx = new MessengerDbContext(this._settings.MessengerApiDbConnectionString)) + { + var localUsers = apiCtx.Users + .Where(x => !foreignUserIds.Any(f => f == x.Id)) + .ToArray(); + + return Task.FromResult(localUsers); + } + } + + public async Task GetMessages(Guid brokerId, DateTime sinceUtc) + { + var userIds = (Guid[])null; + + if(brokerId == this._settings.BrokerId) + { + // Our messages. + var users = await this.GetLocalUsersAndRoutes(); + userIds = users.Item1.Select(x => x.Id).ToArray(); + } + else + { + var users = await this.GetForeignUsers(brokerId); + userIds = users.Select(x => x.Id).ToArray(); + } + + using (var apiCtx = new MessengerDbContext(this._settings.MessengerApiDbConnectionString)) + { + var messages = apiCtx.Messages + .Include(x => x.From).Include(x => x.To) + .Where(x => x.CreatedUtc >= sinceUtc && userIds.Contains(x.From.Id)) + .ToArray(); + + return messages; + } + } + } +} diff --git a/code/MessengerBroker/Handlers/MasterHandler.cs b/code/MessengerBroker/Handlers/MasterHandler.cs new file mode 100644 index 0000000..97dd523 --- /dev/null +++ b/code/MessengerBroker/Handlers/MasterHandler.cs @@ -0,0 +1,196 @@ +using MessengerApi.Db; +using MessengerBroker.Db; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; + +namespace MessengerBroker.Handlers +{ + public class MasterHandler + { + private readonly Settings settings; + + private readonly HttpClient httpClient = new HttpClient(); + + public async Task BeginSyncingWithMaster(Settings.MasterServer masterServer, CancellationToken ct = default) + { + while (!ct.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(1)); + + var usersRequest = new HttpRequestMessage(HttpMethod.Get, $"{masterServer.BrokerApiUrl}/users"); + usersRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", this.settings.BrokerId.ToString()); + + var usersResponseMessage = await this.httpClient.SendAsync(usersRequest, ct); + + if (!usersResponseMessage.IsSuccessStatusCode) + { + continue; + } + + var usersResponse = JsonSerializer.Deserialize(await usersResponseMessage.Content.ReadAsStringAsync()); + + using (var broCtx = new BrokerDbContext(this.settings.MessengerBrokerDbConnectionString)) + using (var apiCtx = new MessengerDbContext(this.settings.MessengerApiDbConnectionString)) + { + var updatedUsers = usersResponse.Users.Where(x => broCtx.Users.GetUserExists(x.Id)).ToList(); + updatedUsers.ForEach(async x => await this.UpdateUser(x, apiCtx)); + + var deletedUsers = broCtx.Users.GetUsersExcept(usersResponse.Users.Select(x => x.Id)).ToList(); + deletedUsers.ForEach(async x => await this.RemoveUser(x.Id, broCtx, apiCtx)); + + var addedUsers = usersResponse.Users.Where(x => !broCtx.Users.GetUserExists(x.Id)).ToList(); + addedUsers.ForEach(async x => await this.AddUser(x, masterServer.BrokerId, broCtx, apiCtx)); + + foreach (var route in usersResponse.UserRoutes + .Where(r => apiCtx.UserRoutes + .Include(x => x.From) + .Include(x => x.To) + .Any(apir => apir.Id == r.Id && (apir.From.Id != r.FromId || apir.To.Id != r.ToId)))) + { + var existing = apiCtx.UserRoutes.Include(x => x.From).Include(x => x.To).Single(x => x.Id == route.Id); + existing.From = apiCtx.Users.Single(x => x.Id == route.FromId); + existing.To = apiCtx.Users.Single(x => x.Id == route.ToId); + } + + foreach (var deletedRoute in apiCtx.UserRoutes + .Where(x => !usersResponse.UserRoutes.Any(r => r.Id == x.Id))) + { + apiCtx.UserRoutes.Remove(deletedRoute); + } + + foreach (var addedRoute in usersResponse.UserRoutes + .Where(r => !apiCtx.UserRoutes.Any(x => x.Id == r.Id))) + { + apiCtx.UserRoutes.Add(new MessengerApi.Db.Entities.UserRoute + { + Id = addedRoute.Id, + From = apiCtx.Users.Single(x => x.Id == addedRoute.FromId), + To = apiCtx.Users.Single(x => x.Id == addedRoute.ToId) + }); + } + + broCtx.SaveChanges(); + apiCtx.SaveChanges(); + } + + var messagesRequest = new HttpRequestMessage(HttpMethod.Get, $"{masterServer.BrokerApiUrl}/messages"); + messagesRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", this.settings.BrokerId.ToString()); + + var messagesResponseMessage = await this.httpClient.SendAsync(messagesRequest, ct); + + if (!messagesResponseMessage.IsSuccessStatusCode) + { + continue; + } + + var messagesResponse = JsonSerializer.Deserialize(await messagesResponseMessage.Content.ReadAsStringAsync()); + + using (var broCtx = new BrokerDbContext(this.settings.MessengerBrokerDbConnectionString)) + using (var apiCtx = new MessengerDbContext(this.settings.MessengerApiDbConnectionString)) + { + var addedMessages = messagesResponse.Messages + .Where(m => broCtx.Messages.Any(x => x.BrokerId == masterServer.BrokerId && x.Id == m.Id) == false) + .ToList(); + + addedMessages.ForEach(x => + { + broCtx.Messages.Add(new Db.Model.Message + { + BrokerId = masterServer.BrokerId, + Id = x.Id + }); + + apiCtx.Messages.Add(new MessengerApi.Db.Entities.Message + { + Id = x.Id, + CreatedUtc = x.CreatedUtc, + From = x.From != null ? (apiCtx.Users.SingleOrDefault(u => u.Id == x.From.Value)) ?? null : null, + To = x.To != null ? (apiCtx.Users.SingleOrDefault(u => u.Id == x.To.Value)) ?? null : null, + IsAcknowledged = x.IsAcknowledged, + IsDelivered = x.IsDelivered, + Payload = x.Payload, + PayloadId = x.PayloadId, + PayloadLifespanInSeconds = x.PayloadLifespanInSeconds, + PayloadTimestamp = x.PayloadTimestamp, + PayloadType = x.PayloadType, + }); + }); + + var existingMessages = messagesResponse.Messages + .Except(addedMessages) + .ToList(); + + existingMessages.ForEach(x => + { + var existing = apiCtx.Messages.SingleOrDefault(a => a.Id == x.Id); + + if(existing != null && (existing.IsDelivered != x.IsDelivered || existing.IsAcknowledged != x.IsAcknowledged)) + { + existing.IsDelivered = x.IsDelivered; + existing.IsAcknowledged = x.IsAcknowledged; + } + }); + + broCtx.SaveChanges(); + apiCtx.SaveChanges(); + } + } + } + + private async Task RemoveUser(Guid id, BrokerDbContext broCtx, MessengerDbContext apiCtx) + { + var broUser = await broCtx.Users.SingleOrDefaultAsync(x => x.Id == id); + + if (broUser != null) + { + broCtx.Users.Remove(broUser); + } + + var apiUser = await apiCtx.Users.SingleOrDefaultAsync(x => x.Id == id); + + if (apiUser != null) + { + apiCtx.Users.Remove(apiUser); + } + } + + private async Task AddUser(Model.Http.Users.UsersResponse.User user, Guid brokerId, BrokerDbContext broCtx, MessengerDbContext apiCtx) + { + if (broCtx.Users.GetUserExists(user.Id) == false) + { + broCtx.Users.Add(new Db.Model.User + { + Id = user.Id, + BrokerId = brokerId + }); + } + + if (apiCtx.Users.Any(x => x.Id == user.Id)) + { + await this.UpdateUser(user, apiCtx); + } + else + { + await apiCtx.Users.AddAsync(new MessengerApi.Db.Entities.User + { + Id = user.Id, + ApiKey = user.ApiKey, + CanReceive = user.CanReceive, + CanSend = user.CanSend, + IsEnabled = user.IsEnabled, + Name = user.Name + }); + } + } + + private async Task UpdateUser(Model.Http.Users.UsersResponse.User user, MessengerDbContext apiCtx) + { + var apiUser = await apiCtx.Users.SingleAsync(x => x.Id == user.Id); + apiUser.ApiKey = user.ApiKey; + apiUser.CanReceive = user.CanReceive; + apiUser.CanSend = user.CanSend; + apiUser.IsEnabled = user.IsEnabled; + apiUser.Name = user.Name; + } + } +} \ No newline at end of file diff --git a/code/MessengerBroker/MessengerBroker.csproj b/code/MessengerBroker/MessengerBroker.csproj new file mode 100644 index 0000000..d5a8a4e --- /dev/null +++ b/code/MessengerBroker/MessengerBroker.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + disable + enable + Linux + + + + + + + + + + + + diff --git a/code/MessengerBroker/MessengerBroker.http b/code/MessengerBroker/MessengerBroker.http new file mode 100644 index 0000000..2562d21 --- /dev/null +++ b/code/MessengerBroker/MessengerBroker.http @@ -0,0 +1,6 @@ +@MessengerBroker_HostAddress = http://localhost:5048 + +GET {{MessengerBroker_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/code/MessengerBroker/Model/Http/Sync.cs b/code/MessengerBroker/Model/Http/Sync.cs new file mode 100644 index 0000000..4c4bd8d --- /dev/null +++ b/code/MessengerBroker/Model/Http/Sync.cs @@ -0,0 +1,42 @@ +namespace MessengerBroker.Model.Http +{ + public class Sync + { + public class SyncRequest + { + public Guid BrokerId { get; set; } + + public DateTime SinceUtc { get; set; } + } + + public class SyncResponse + { + public Message[] Messages { get; set; } + + public class Message + { + public Guid Id { get; set; } + + public DateTime CreatedUtc { get; set; } + + public Guid? From { get; set; } + + public Guid? To { get; set; } + + public bool IsDelivered { get; set; } + + public bool IsAcknowledged { get; set; } + + public string PayloadId { get; set; } + + public string PayloadType { get; set; } + + public string Payload { get; set; } + + public DateTime? PayloadTimestamp { get; set; } + + public int? PayloadLifespanInSeconds { get; set; } + } + } + } +} \ No newline at end of file diff --git a/code/MessengerBroker/Model/Http/Users.cs b/code/MessengerBroker/Model/Http/Users.cs new file mode 100644 index 0000000..495df81 --- /dev/null +++ b/code/MessengerBroker/Model/Http/Users.cs @@ -0,0 +1,36 @@ +namespace MessengerBroker.Model.Http +{ + public class Users + { + public class UsersResponse + { + public User[] Users { get; set; } + + public UserRoute[] UserRoutes { get; set; } + + public class User + { + public Guid Id { get; set; } + + public Guid ApiKey { get; set; } + + public string Name { get; set; } + + public bool IsEnabled { get; set; } + + public bool CanSend { get; set; } + + public bool CanReceive { get; set; } + } + + public class UserRoute + { + public Guid Id { get; set; } + + public Guid FromId { get; set; } + + public Guid ToId { get; set; } + } + } + } +} diff --git a/code/MessengerBroker/Program.cs b/code/MessengerBroker/Program.cs new file mode 100644 index 0000000..c471fdf --- /dev/null +++ b/code/MessengerBroker/Program.cs @@ -0,0 +1,102 @@ +using MessengerBroker.Handlers; +using MessengerBroker.Model.Http; +using System.Runtime.CompilerServices; + +namespace MessengerBroker +{ + public class Program + { + public static void Main(string[] args) + { + var settings = (Settings)null; + var builder = WebApplication.CreateBuilder(args); + var app = builder.Build(); + + app.MapGet("/users", async (HttpContext httpContext) => + { + var authHandler = app.Services.GetService(); + var brokerId = authHandler.Auth(httpContext); + + if(brokerId == null) + { + return Results.Unauthorized(); + } + + var dataHandler = app.Services.GetService(); + var usersAndRoutes = await dataHandler.GetLocalUsersAndRoutes(); + + var response = new Users.UsersResponse + { + Users = usersAndRoutes.Item1.Select(x => new Users.UsersResponse.User + { + Id = x.Id, + Name = x.Name, + ApiKey = x.ApiKey, + CanReceive = x.CanReceive, + CanSend = x.CanSend, + IsEnabled = x.IsEnabled, + }).ToArray(), + UserRoutes = usersAndRoutes.Item2.Select(x => new Users.UsersResponse.UserRoute + { + Id = x.Id, + FromId = x.From.Id, + ToId = x.To.Id + }).ToArray() + }; + + return Results.Json(response); + }); + + app.MapGet("/sync", async (HttpContext httpContext, [AsParameters] Sync.SyncRequest request) => + { + var authHandler = app.Services.GetService(); + var brokerId = authHandler.Auth(httpContext); + + if(brokerId == null) + { + return Results.Unauthorized(); + } + else if(request.BrokerId != brokerId && request.BrokerId != settings.BrokerId) + { + return Results.Unauthorized(); + } + + var dataHandler = app.Services.GetService(); + var messages = await dataHandler.GetMessages(request.BrokerId, request.SinceUtc); + + var response = new Sync.SyncResponse + { + Messages = messages.Select(x => new Sync.SyncResponse.Message + { + Id = x.Id, + CreatedUtc = x.CreatedUtc, + From = x.From.Id, + To = x.To.Id, + IsAcknowledged = x.IsAcknowledged, + IsDelivered = x.IsDelivered, + Payload = x.Payload, + PayloadId = x.PayloadId, + PayloadLifespanInSeconds = x.PayloadLifespanInSeconds, + PayloadTimestamp = x.PayloadTimestamp, + PayloadType = x.PayloadType + }).ToArray() + }; + + return Results.Json(response); + }); + + _ = Task.Run(async () => + { + var cts = new CancellationTokenSource(); + var handler = app.Services.GetService(); + + foreach(var master in settings.Masters) + { + _ = handler.BeginSyncingWithMaster(master, cts.Token); + } + }); + + app.Run(); + } + } +} diff --git a/code/MessengerBroker/Properties/launchSettings.json b/code/MessengerBroker/Properties/launchSettings.json new file mode 100644 index 0000000..e8ad275 --- /dev/null +++ b/code/MessengerBroker/Properties/launchSettings.json @@ -0,0 +1,22 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5048" + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": { + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": false + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/code/MessengerBroker/Settings.cs b/code/MessengerBroker/Settings.cs new file mode 100644 index 0000000..9efdb05 --- /dev/null +++ b/code/MessengerBroker/Settings.cs @@ -0,0 +1,36 @@ +namespace MessengerBroker +{ + public class Settings + { + /// + /// Connection string to Messenger API DB. + /// + public string MessengerApiDbConnectionString { get; set; } + + public string MessengerBrokerDbConnectionString { get; set; } + + public Guid BrokerId { get; set; } + + public MasterServer[] Masters { get; set; } + + public SlaveServer[] Slaves { get; set; } + + /// + /// A server that we are a slave to. If this server goes down, their users will alternate to us and we have to provide service during outage. We pull data from this server. + /// + public class MasterServer + { + public string BrokerApiUrl { get; set; } + + public Guid BrokerId { set; get; } + } + + /// + /// A server that slaves to us in case of our own outage. They pull from us. + /// + public class SlaveServer + { + public Guid BrokerId { get; set; } + } + } +} diff --git a/code/MessengerBroker/appsettings.Development.json b/code/MessengerBroker/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/code/MessengerBroker/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/code/MessengerBroker/appsettings.json b/code/MessengerBroker/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/code/MessengerBroker/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/sub/messengerapi b/sub/messengerapi new file mode 160000 index 0000000..1f3952b --- /dev/null +++ b/sub/messengerapi @@ -0,0 +1 @@ +Subproject commit 1f3952b14364cf553e0c881b7e6992d81ae8ba9b