diff --git a/Alchegos.Core.csproj b/Alchegos.Core.csproj index 17b910f..e620ce3 100644 --- a/Alchegos.Core.csproj +++ b/Alchegos.Core.csproj @@ -3,7 +3,17 @@ net9.0 enable - enable + disable + + + + + + + ..\..\..\..\.nuget\packages\microsoft.extensions.options\9.0.2\lib\net9.0\Microsoft.Extensions.Options.dll + + + diff --git a/Class1.cs b/Class1.cs deleted file mode 100644 index 220e893..0000000 --- a/Class1.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Alchegos.Core; - -public class Class1 -{ -} \ No newline at end of file diff --git a/src/Attributes/AutoRegister.cs b/src/Attributes/AutoRegister.cs new file mode 100644 index 0000000..e762e19 --- /dev/null +++ b/src/Attributes/AutoRegister.cs @@ -0,0 +1,6 @@ +namespace Alchegos.Core.Attributes; +[AttributeUsage(AttributeTargets.Class)] +public class AutoRegister : Attribute +{ + +} \ No newline at end of file diff --git a/src/DataStructures/Exchange.cs b/src/DataStructures/Exchange.cs new file mode 100644 index 0000000..2f606fc --- /dev/null +++ b/src/DataStructures/Exchange.cs @@ -0,0 +1,35 @@ +using System.Threading.Channels; +using RabbitMQ.Client; + +namespace Alchegos.Core.DataStructures; + +public class Exchange +{ + public string Name { get; set; } + public string ExchangeType { get; set; } + public bool Durable { get; set; } + public bool AutoDelete { get; set; } + public IDictionary Arguments { get; set; } + public HashSet Queues { get; set; } = new(); + + public async Task CreateAsync(IChannel channel) + { + await channel.ExchangeDeclareAsync(Name, ExchangeType, Durable, AutoDelete, Arguments); + foreach (MessageQueue queue in Queues) + { + await channel.QueueDeclareAsync( + queue: queue.Name, + durable: queue.Durable, + exclusive: queue.Exclusive, + autoDelete: queue.AutoDelete, + arguments: queue.Arguments + ); + await channel.QueueBindAsync( + queue: queue.Name, + exchange: Name, + routingKey: queue.Name, + arguments: queue.Arguments + ); + } + } +} \ No newline at end of file diff --git a/src/DataStructures/MessageQueue.cs b/src/DataStructures/MessageQueue.cs new file mode 100644 index 0000000..d53d6bf --- /dev/null +++ b/src/DataStructures/MessageQueue.cs @@ -0,0 +1,10 @@ +namespace Alchegos.Core.DataStructures; + +public class MessageQueue +{ + public string Name { get; set; } + public bool Exclusive { get; set; } + public bool AutoDelete { get; set; } + public bool Durable { get; set; } + public IDictionary Arguments { get; set; } +} \ No newline at end of file diff --git a/src/GlobalRegistry.cs b/src/GlobalRegistry.cs new file mode 100644 index 0000000..15d7925 --- /dev/null +++ b/src/GlobalRegistry.cs @@ -0,0 +1,42 @@ +using System.Reflection; +using Alchegos.Core.Attributes; +using Alchegos.Core.DataStructures; + +namespace Alchegos.Core; + +public class GlobalRegistry +{ + private static readonly Lock _lock = new Lock(); + + private GlobalRegistry() + { + + } + + private static readonly Lazy BackingInstance = new(() => new GlobalRegistry()); + public static GlobalRegistry Instance { + get + { + lock (_lock) + { + return BackingInstance.Value; + } + } + } + + public void Start() + { + Assembly assembly = Assembly.GetExecutingAssembly(); + IEnumerable types = assembly + .GetTypes() + .Where(t => t.IsClass && t.GetCustomAttributes(typeof(AutoRegister), false).Length > 0); + foreach (Type t in types) + { + MethodInfo registerMethod = t.GetMethod("Register", BindingFlags.Static | BindingFlags.Public); + registerMethod?.Invoke(null, null); + } + } + + public HashSet Exchanges { get; set; } = new(); + +} \ No newline at end of file diff --git a/src/Registers/ExchangeRegister.cs b/src/Registers/ExchangeRegister.cs new file mode 100644 index 0000000..1887999 --- /dev/null +++ b/src/Registers/ExchangeRegister.cs @@ -0,0 +1,64 @@ +using Alchegos.Core.Attributes; +using Alchegos.Core.DataStructures; +using RabbitMQ.Client; + +namespace Alchegos.Core.Registers; +[AutoRegister] +public static class ExchangeRegister +{ + + private static readonly HashSet WebhookEvents = [ + "create", + "delete", + "fork", + "push", + "issue_assign", + "issue_label", + "issue_milestone", + "issue_comment", + "pull_request", + "pull_request_assign", + "pull_request_label", + "pull_request_milestone", + "pull_request_comment", + "pull_request_review_approved", + "pull_request_review_rejected", + "pull_request_review_comment", + "pull_request_sync", + "pull_request_review_request", + "wiki", + "repository", + "release", + "package", + "status", + "schedule" + ]; + + public static void Register() + { + Exchange giteaExchange = new Exchange + { + Name = "gitea.webhook.exchange", + ExchangeType = ExchangeType.Topic, + AutoDelete = false, + Durable = true, + Queues = new() + }; + + foreach (string webhookEvent in WebhookEvents) + { + giteaExchange.Queues.Add(new MessageQueue + { + Name = webhookEvent, + Exclusive = false, + AutoDelete = false, + Durable = true + }); + } + + GlobalRegistry.Instance.Exchanges = + [ + giteaExchange + ]; + } +} \ No newline at end of file diff --git a/src/Services/RabbitMQ/IRabbitPublisher.cs b/src/Services/RabbitMQ/IRabbitPublisher.cs new file mode 100644 index 0000000..d2cbc96 --- /dev/null +++ b/src/Services/RabbitMQ/IRabbitPublisher.cs @@ -0,0 +1,6 @@ +namespace Alchegos.Core.Services.RabbitMQ; + +public interface IRabbitPublisher +{ + Task PublishAsync(string exchange, string routingKey, string message); +} \ No newline at end of file diff --git a/src/Services/RabbitMQ/IRabbitService.cs b/src/Services/RabbitMQ/IRabbitService.cs new file mode 100644 index 0000000..902b84c --- /dev/null +++ b/src/Services/RabbitMQ/IRabbitService.cs @@ -0,0 +1,8 @@ +using RabbitMQ.Client; + +namespace Alchegos.Core.Services.RabbitMQ; + +public interface IRabbitService +{ + Task GetChannelAsync(); +} diff --git a/src/Services/RabbitMQ/RabbitConnectionOptions.cs b/src/Services/RabbitMQ/RabbitConnectionOptions.cs new file mode 100644 index 0000000..ba1ba01 --- /dev/null +++ b/src/Services/RabbitMQ/RabbitConnectionOptions.cs @@ -0,0 +1,10 @@ +namespace Alchegos.Core.Services.RabbitMQ; + +public class RabbitConnectionOptions +{ + + public string HostName { get; set; } = "localhost"; + public string UserName { get; set; } = "guest"; + public string Password { get; set; } = "guest"; + public int Port { get; set; } = 5672; +} \ No newline at end of file diff --git a/src/Services/RabbitMQ/RabbitPublisher.cs b/src/Services/RabbitMQ/RabbitPublisher.cs new file mode 100644 index 0000000..3cac0c8 --- /dev/null +++ b/src/Services/RabbitMQ/RabbitPublisher.cs @@ -0,0 +1,28 @@ +using System.Text; +using RabbitMQ.Client; + +namespace Alchegos.Core.Services.RabbitMQ; + +public class RabbitPublisher(IRabbitService service) : IRabbitPublisher +{ + private IRabbitService RabbitService { get; } = service; + private readonly SemaphoreSlim _lock = new(1, 1); + public async Task PublishAsync(string exchange, string routingKey, string message) + { + ReadOnlyMemory body = Encoding.UTF8.GetBytes(message).AsMemory(); + await _lock.WaitAsync(); + try + { + IChannel channel = await RabbitService.GetChannelAsync(); + await channel.BasicPublishAsync( + exchange: exchange, + routingKey: routingKey, + body: body + ); + } + finally + { + _lock.Release(); + } + } +} \ No newline at end of file diff --git a/src/Services/RabbitMQ/RabbitService.cs b/src/Services/RabbitMQ/RabbitService.cs new file mode 100644 index 0000000..a7ed8fa --- /dev/null +++ b/src/Services/RabbitMQ/RabbitService.cs @@ -0,0 +1,69 @@ +using Alchegos.Core.DataStructures; +using Microsoft.Extensions.Options; +using RabbitMQ.Client; + +namespace Alchegos.Core.Services.RabbitMQ; + +public class RabbitService : IAsyncDisposable, IRabbitService +{ + private readonly SemaphoreSlim _initLock = new (1, 1); + private Lazy LazyInit { get; init; } + + public async Task EnsureInitializedAsync() + { + if (Connection is not null) + return; + await _initLock.WaitAsync(); + try + { + if (Connection is null) + await LazyInit.Value; + } + finally + { + _initLock.Release(); + } + } + + private IConnection Connection { get; set; } + + public async Task GetChannelAsync() + { + await EnsureInitializedAsync(); + return await Connection.CreateChannelAsync(); + } + + public RabbitService(IOptions options) + { + ConnectionFactory = new() + { + HostName = options.Value.HostName, + Port = options.Value.Port, + UserName = options.Value.UserName, + Password = options.Value.Password, + }; + LazyInit = new (InitAsync); + } + + private ConnectionFactory ConnectionFactory { get; set; } + private async Task InitAsync() + { + if (Connection is null) + { + Connection = await ConnectionFactory.CreateConnectionAsync(); + IChannel channel = await Connection.CreateChannelAsync(); + foreach (Exchange exchange in GlobalRegistry.Instance.Exchanges) + await exchange.CreateAsync(channel); + } + } + + public async ValueTask DisposeAsync() + { + if (Connection is not null) + { + await Connection.CloseAsync(); + await Connection.DisposeAsync(); + Connection = null; + } + } +} \ No newline at end of file