7 Commits
v2.0 ... v2.0.1

Author SHA1 Message Date
a50bd4cf65 Removed compose/docker steps as Portainer CE doesn't allow automating deployments.
All checks were successful
Build and Push Docker Image / docker (push) Successful in 31s
2025-07-06 15:47:16 +02:00
238202c45b Redeploy bug fix.
All checks were successful
Build and Push Docker Image / build (push) Successful in 53s
Build and Push Docker Image / docker (push) Successful in 8s
2025-07-06 10:34:53 +02:00
f49705b70f Fixed a bug and added redeploy step.
Some checks failed
Build and Push Docker Image / build (push) Successful in 53s
Build and Push Docker Image / docker (push) Failing after 36s
2025-07-06 10:30:31 +02:00
a44912ac87 payloadLifespan/payloadLifetime and other name variants unified to Time To Live naming.
All checks were successful
Build and Push Docker Image / build (push) Successful in 55s
Build and Push Docker Image / docker (push) Successful in 36s
2025-07-05 17:07:53 +02:00
85c462a614 Updated generic resolution of Guid from config.
All checks were successful
Build and Push Docker Image / build (push) Successful in 50s
Build and Push Docker Image / docker (push) Successful in 35s
2025-07-05 09:05:02 +02:00
3c7418974a Misc.
All checks were successful
Build and Push Docker Image / build (push) Successful in 52s
Build and Push Docker Image / docker (push) Successful in 34s
2025-07-05 08:53:27 +02:00
3a2005cad9 Assets for logo.
All checks were successful
Build and Push Docker Image / build (push) Successful in 51s
Build and Push Docker Image / docker (push) Successful in 9s
2025-07-05 01:18:58 +02:00
37 changed files with 518 additions and 79 deletions

View File

@ -1,26 +0,0 @@
name: Build and Push Docker Image
on:
push:
jobs:
build:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/dotnet/sdk:9.0
steps:
- name: Install Node.js and dependencies
run: |
apt-get update
apt-get install -y curl gnupg
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
apt-get install -y nodejs git
- name: Checkout
uses: actions/checkout@v3
- name: Restore dependencies
run: dotnet restore ./code/MessengerApi/MessengerApi.csproj
- name: Build project
run: dotnet build ./code/MessengerApi/MessengerApi.csproj -c Release

View File

@ -3,9 +3,10 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.6" /> <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.6" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageVersion Include="portaloggy" Version="1.0.2" /> <PackageVersion Include="portaloggy" Version="1.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" /> <PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.1.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="7.1.0" />
</ItemGroup> </ItemGroup>

View File

@ -49,9 +49,9 @@ Additional tunables, with their sustainable default values:
- `QUERY_RATE_PER_MINUTE: 100` - `QUERY_RATE_PER_MINUTE: 100`
- Sets maximum allowed client query rate per minute for all endpoints. Anonymous users share same limit pool. - Sets maximum allowed client query rate per minute for all endpoints. Anonymous users share same limit pool.
- If send rate is exceeded, client receives a `HTTP 429` with explanation. - If send rate is exceeded, client receives a `HTTP 429` with explanation.
- `DEFAULT_MESSAGE_LIFETIME_IN_MINUTES: 1` - `DEFAULT_MESSAGE_TIME_TO_LIVE_IN_SECONDS: 60`
- Message will wait for delivery for set amount of time. After the time passes, a call to `/receive` will not consider it for delivery anymore. - Message will wait for delivery for set amount of time. After the time passes, a call to `/receive` will not consider it for delivery anymore.
- Override this in message content by setting _optional_ `lifespanInSeconds` value inside the request. - Override this in message content by setting _optional_ `timeToLiveInSeconds` value inside the request.
- There will be no indication to the sender or to client that there was a missed message. Once it's gone, it's gone. - There will be no indication to the sender or to client that there was a missed message. Once it's gone, it's gone.
- `HOUSEKEEPING_ENABLED: true` - `HOUSEKEEPING_ENABLED: true`
- Turns on housekeeping job that periodically removes stale, delivered and/or acknowledged messages. You can tune this further, see below. By default, it only removes messages that are 2 hours old, regardless of their delivery or acknowledgement state. - Turns on housekeeping job that periodically removes stale, delivered and/or acknowledged messages. You can tune this further, see below. By default, it only removes messages that are 2 hours old, regardless of their delivery or acknowledgement state.
@ -106,7 +106,7 @@ Request:
"payloadType": "STATUS", "payloadType": "STATUS",
"payload": "{\n \"system\": \"OK\",\n}", "payload": "{\n \"system\": \"OK\",\n}",
"toUserId": "46b882b7-4b96-4fa2-ba1b-4955a9500c36", "toUserId": "46b882b7-4b96-4fa2-ba1b-4955a9500c36",
"lifespanInSeconds": "3600" "timeToLiveInSeconds": "3600"
} }
Response: Response:

View File

@ -1,2 +1,2 @@
mobileapp;true;f696442b-e8dc-4074-b34f-94bcece8e74b;aab8f7e9-ad13-4bf8-bb2e-0cd93d81adc0;remote mobileapp;true;f696442b-e8dc-4074-b34f-94bcece8e74b;aab8f7e9-ad13-4bf8-bb2e-0cd93d81adc0;remote
remote;true;15d97720-f5b7-47aa-9c1a-71f98b0b9248;8f73f683-7cb3-40df-998e-6e604aef0e53 remote;true;15d97720-f5b7-47aa-9c1a-71f98b0b9248;8f73f683-7cb3-40df-998e-6e604aef0e53;mobileapp

BIN
assets/messengerapi.128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
assets/messengerapi.128.psd Normal file

Binary file not shown.

BIN
assets/messengerapi.256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
assets/messengerapi.256.psd Normal file

Binary file not shown.

View File

@ -4,6 +4,7 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -31,7 +31,7 @@ namespace MessengerApi.Configuration.Model
/// <summary> /// <summary>
/// Message lifetime unless set differently in message body. /// Message lifetime unless set differently in message body.
/// </summary> /// </summary>
public int DefaultMessageLifetimeInMinutes { get; set; } public int DefaultMessageTimeToLiveInSeconds { get; set; }
/// <summary> /// <summary>
/// If true, messages are periodically wiped to free up space. /// If true, messages are periodically wiped to free up space.
@ -72,7 +72,7 @@ namespace MessengerApi.Configuration.Model
this.Origins = origins ?? []; this.Origins = origins ?? [];
this.Proxies = []; this.Proxies = [];
this.RateLimitPerMinute = 120; this.RateLimitPerMinute = 120;
this.DefaultMessageLifetimeInMinutes = 1; this.DefaultMessageTimeToLiveInSeconds = 60;
this.HousekeepingEnabled = true; this.HousekeepingEnabled = true;
this.HousekeepingMessageAgeInMinutes = 120; this.HousekeepingMessageAgeInMinutes = 120;
this.HousekeepingMessageState = HousekeepingMessageStates.None; this.HousekeepingMessageState = HousekeepingMessageStates.None;
@ -85,7 +85,7 @@ namespace MessengerApi.Configuration.Model
{ {
Populate<string>(config, Env.PROXIES, x => this.Proxies = ProxiesParser.Parse(x)); Populate<string>(config, Env.PROXIES, x => this.Proxies = ProxiesParser.Parse(x));
Populate<int>(config, Env.QUERY_RATE_PER_MINUTE, x => this.RateLimitPerMinute = x); Populate<int>(config, Env.QUERY_RATE_PER_MINUTE, x => this.RateLimitPerMinute = x);
Populate<int>(config, Env.DEFAULT_MESSAGE_LIFETIME_IN_MINUTES, x => this.DefaultMessageLifetimeInMinutes = x); Populate<int>(config, Env.DEFAULT_MESSAGE_TIME_TO_LIVE_IN_SECONDS, x => this.DefaultMessageTimeToLiveInSeconds = x);
Populate<bool>(config, Env.HOUSEKEEPING_ENABLED, x => this.HousekeepingEnabled = x); Populate<bool>(config, Env.HOUSEKEEPING_ENABLED, x => this.HousekeepingEnabled = x);
Populate<int>(config, Env.HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES, x => this.HousekeepingMessageAgeInMinutes = x); Populate<int>(config, Env.HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES, x => this.HousekeepingMessageAgeInMinutes = x);
Populate<string>(config, Env.HOUSEKEEPING_MESSAGE_STATE, x => this.HousekeepingMessageState = HousekeepingMessageStateParser.Parse(x)); Populate<string>(config, Env.HOUSEKEEPING_MESSAGE_STATE, x => this.HousekeepingMessageState = HousekeepingMessageStateParser.Parse(x));

View File

@ -6,6 +6,11 @@ namespace MessengerApi.Configuration.Parsers
{ {
public static PersistenceTypes Parse(string value) public static PersistenceTypes Parse(string value)
{ {
if (string.IsNullOrWhiteSpace(value))
{
return PersistenceTypes.Sql;
}
return (PersistenceTypes)Enum.Parse(typeof(PersistenceTypes), value, true); return (PersistenceTypes)Enum.Parse(typeof(PersistenceTypes), value, true);
} }
} }

View File

@ -10,7 +10,7 @@
public const string CORS_ORIGINS = nameof(CORS_ORIGINS); public const string CORS_ORIGINS = nameof(CORS_ORIGINS);
public const string PROXIES = nameof(PROXIES); public const string PROXIES = nameof(PROXIES);
public const string QUERY_RATE_PER_MINUTE = nameof(QUERY_RATE_PER_MINUTE); public const string QUERY_RATE_PER_MINUTE = nameof(QUERY_RATE_PER_MINUTE);
public const string DEFAULT_MESSAGE_LIFETIME_IN_MINUTES = nameof(DEFAULT_MESSAGE_LIFETIME_IN_MINUTES); public const string DEFAULT_MESSAGE_TIME_TO_LIVE_IN_SECONDS = nameof(DEFAULT_MESSAGE_TIME_TO_LIVE_IN_SECONDS);
public const string HOUSEKEEPING_ENABLED = nameof(HOUSEKEEPING_ENABLED); public const string HOUSEKEEPING_ENABLED = nameof(HOUSEKEEPING_ENABLED);
public const string HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES = nameof(HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES); public const string HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES = nameof(HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES);
public const string HOUSEKEEPING_MESSAGE_STATE = nameof(HOUSEKEEPING_MESSAGE_STATE); public const string HOUSEKEEPING_MESSAGE_STATE = nameof(HOUSEKEEPING_MESSAGE_STATE);

View File

@ -9,6 +9,12 @@
public T GetValue<T>(string key) public T GetValue<T>(string key)
{ {
if (typeof(T).Equals(typeof(Guid)))
{
var guid = Guid.Parse(System.Environment.GetEnvironmentVariable(key));
return (T)Convert.ChangeType(guid, typeof(T));
}
return (T)Convert.ChangeType(System.Environment.GetEnvironmentVariable(key), typeof(T)); return (T)Convert.ChangeType(System.Environment.GetEnvironmentVariable(key), typeof(T));
} }
} }

View File

@ -20,6 +20,6 @@ namespace MessengerApi.Db.Entities
public string Payload { get; set; } public string Payload { get; set; }
public int? PayloadLifespanInSeconds { get; set; } public int? TimeToLiveInSeconds { get; set; }
} }
} }

View File

@ -4,6 +4,7 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -8,10 +8,15 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -3,11 +3,12 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>disable</Nullable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,123 @@
// <auto-generated />
using System;
using MessengerApi.Db.Npg;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace MessengerApi.Db.Npg.Migrations
{
[DbContext(typeof(MessengerNpgDbContext))]
[Migration("20250705145537_RenameTtlColumn")]
partial class RenameTtlColumn
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.6")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("MessengerApi.Db.Entities.Message", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("FromId")
.HasColumnType("uuid");
b.Property<bool>("IsAcknowledged")
.HasColumnType("boolean");
b.Property<bool>("IsDelivered")
.HasColumnType("boolean");
b.Property<string>("Payload")
.HasColumnType("text");
b.Property<string>("PayloadType")
.HasColumnType("text");
b.Property<int>("TimeToLiveInSeconds")
.HasColumnType("integer");
b.Property<Guid>("ToId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("ToId", "IsDelivered");
b.ToTable("Messages");
});
modelBuilder.Entity("MessengerApi.Db.Entities.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ApiKey")
.HasColumnType("uuid");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("FromId")
.HasColumnType("uuid");
b.Property<Guid?>("ToId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("FromId");
b.HasIndex("ToId");
b.ToTable("UserRoutes");
});
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
{
b.HasOne("MessengerApi.Db.Entities.User", "From")
.WithMany()
.HasForeignKey("FromId");
b.HasOne("MessengerApi.Db.Entities.User", "To")
.WithMany()
.HasForeignKey("ToId");
b.Navigation("From");
b.Navigation("To");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MessengerApi.Db.Npg.Migrations
{
/// <inheritdoc />
public partial class RenameTtlColumn : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "PayloadLifespanInSeconds",
table: "Messages",
newName: "TimeToLiveInSeconds");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "TimeToLiveInSeconds",
table: "Messages",
newName: "PayloadLifespanInSeconds");
}
}
}

View File

@ -43,12 +43,12 @@ namespace MessengerApi.Db.Npg.Migrations
b.Property<string>("Payload") b.Property<string>("Payload")
.HasColumnType("text"); .HasColumnType("text");
b.Property<int>("PayloadLifespanInSeconds")
.HasColumnType("integer");
b.Property<string>("PayloadType") b.Property<string>("PayloadType")
.HasColumnType("text"); .HasColumnType("text");
b.Property<int>("TimeToLiveInSeconds")
.HasColumnType("integer");
b.Property<Guid>("ToId") b.Property<Guid>("ToId")
.HasColumnType("uuid"); .HasColumnType("uuid");

View File

@ -13,6 +13,10 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -4,10 +4,11 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,125 @@
// <auto-generated />
using System;
using MessengerApi.Db.Sql;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace MessengerApi.Db.Sql.Migrations
{
[DbContext(typeof(MessengerSqlDbContext))]
[Migration("20250705145221_ShortenPayloadLifespanColumnName")]
partial class ShortenPayloadLifespanColumnName
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.6")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("MessengerApi.Db.Entities.Message", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("datetime2");
b.Property<Guid>("FromId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsAcknowledged")
.HasColumnType("bit");
b.Property<bool>("IsDelivered")
.HasColumnType("bit");
b.Property<int>("LifespanInSeconds")
.HasColumnType("int");
b.Property<string>("Payload")
.HasColumnType("nvarchar(max)");
b.Property<string>("PayloadType")
.HasColumnType("nvarchar(max)");
b.Property<Guid>("ToId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("ToId", "IsDelivered");
SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("ToId", "IsDelivered"), false);
b.ToTable("Messages");
});
modelBuilder.Entity("MessengerApi.Db.Entities.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid>("ApiKey")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsEnabled")
.HasColumnType("bit");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("FromId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("ToId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("FromId");
b.HasIndex("ToId");
b.ToTable("UserRoutes");
});
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
{
b.HasOne("MessengerApi.Db.Entities.User", "From")
.WithMany()
.HasForeignKey("FromId");
b.HasOne("MessengerApi.Db.Entities.User", "To")
.WithMany()
.HasForeignKey("ToId");
b.Navigation("From");
b.Navigation("To");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MessengerApi.Db.Sql.Migrations
{
/// <inheritdoc />
public partial class ShortenPayloadLifespanColumnName : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "PayloadLifespanInSeconds",
table: "Messages",
newName: "LifespanInSeconds");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "LifespanInSeconds",
table: "Messages",
newName: "PayloadLifespanInSeconds");
}
}
}

View File

@ -0,0 +1,125 @@
// <auto-generated />
using System;
using MessengerApi.Db.Sql;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace MessengerApi.Db.Sql.Migrations
{
[DbContext(typeof(MessengerSqlDbContext))]
[Migration("20250705145452_RenameTtlColumnName")]
partial class RenameTtlColumnName
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.6")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("MessengerApi.Db.Entities.Message", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedUtc")
.HasColumnType("datetime2");
b.Property<Guid>("FromId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsAcknowledged")
.HasColumnType("bit");
b.Property<bool>("IsDelivered")
.HasColumnType("bit");
b.Property<string>("Payload")
.HasColumnType("nvarchar(max)");
b.Property<string>("PayloadType")
.HasColumnType("nvarchar(max)");
b.Property<int>("TimeToLiveInSeconds")
.HasColumnType("int");
b.Property<Guid>("ToId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("ToId", "IsDelivered");
SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("ToId", "IsDelivered"), false);
b.ToTable("Messages");
});
modelBuilder.Entity("MessengerApi.Db.Entities.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid>("ApiKey")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsEnabled")
.HasColumnType("bit");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("FromId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("ToId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("FromId");
b.HasIndex("ToId");
b.ToTable("UserRoutes");
});
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
{
b.HasOne("MessengerApi.Db.Entities.User", "From")
.WithMany()
.HasForeignKey("FromId");
b.HasOne("MessengerApi.Db.Entities.User", "To")
.WithMany()
.HasForeignKey("ToId");
b.Navigation("From");
b.Navigation("To");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MessengerApi.Db.Sql.Migrations
{
/// <inheritdoc />
public partial class RenameTtlColumnName : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "LifespanInSeconds",
table: "Messages",
newName: "TimeToLiveInSeconds");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "TimeToLiveInSeconds",
table: "Messages",
newName: "LifespanInSeconds");
}
}
}

View File

@ -43,12 +43,12 @@ namespace MessengerApi.Db.Sql.Migrations
b.Property<string>("Payload") b.Property<string>("Payload")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<int>("PayloadLifespanInSeconds")
.HasColumnType("int");
b.Property<string>("PayloadType") b.Property<string>("PayloadType")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<int>("TimeToLiveInSeconds")
.HasColumnType("int");
b.Property<Guid>("ToId") b.Property<Guid>("ToId")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");

View File

@ -19,7 +19,7 @@ namespace MessengerApi.Db
modelBuilder.Entity<User>().HasKey(e => e.Id); modelBuilder.Entity<User>().HasKey(e => e.Id);
modelBuilder.Entity<Message>().HasKey(e => e.Id); modelBuilder.Entity<Message>().HasKey(e => e.Id);
modelBuilder.Entity<Message>().Property(e => e.CreatedUtc).HasConversion<DateTimeAsUtcValueConverter>(); modelBuilder.Entity<Message>().Property(e => e.CreatedUtc).HasConversion<DateTimeAsUtcValueConverter>();
modelBuilder.Entity<Message>().Property(e => e.PayloadLifespanInSeconds).IsRequired(); modelBuilder.Entity<Message>().Property(e => e.TimeToLiveInSeconds).IsRequired();
modelBuilder.Entity<UserRoute>().HasKey(e => e.Id); modelBuilder.Entity<UserRoute>().HasKey(e => e.Id);
} }
} }

View File

@ -16,7 +16,7 @@ namespace MessengerApi.Db.Repositories
return this.db return this.db
.Where(x => x.ToId == user.Id && x.IsDelivered == false) .Where(x => x.ToId == user.Id && x.IsDelivered == false)
.Where(x => x.PayloadLifespanInSeconds == null || x.CreatedUtc.AddSeconds(x.PayloadLifespanInSeconds.Value) >= timestamp) .Where(x => x.TimeToLiveInSeconds == null || x.CreatedUtc.AddSeconds(x.TimeToLiveInSeconds.Value) >= timestamp)
.OrderBy(x => x.CreatedUtc); .OrderBy(x => x.CreatedUtc);
} }
} }

View File

@ -28,7 +28,6 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
..\Directory.Packages.props = ..\Directory.Packages.props ..\Directory.Packages.props = ..\Directory.Packages.props
..\docker-compose.yml = ..\docker-compose.yml
..\Dockerfile = ..\Dockerfile ..\Dockerfile = ..\Dockerfile
..\assets\example-users.config = ..\assets\example-users.config ..\assets\example-users.config = ..\assets\example-users.config
..\NuGet.config = ..\NuGet.config ..\NuGet.config = ..\NuGet.config
@ -36,7 +35,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
..\.gitea\workflows\build.yml = ..\.gitea\workflows\build.yml
..\.gitea\workflows\docker-build-and-push.yml = ..\.gitea\workflows\docker-build-and-push.yml ..\.gitea\workflows\docker-build-and-push.yml = ..\.gitea\workflows\docker-build-and-push.yml
EndProjectSection EndProjectSection
EndProject EndProject

View File

@ -1,8 +0,0 @@
#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.
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
COPY ./publish .
ENTRYPOINT ["dotnet", "MessengerApi.dll"]

View File

@ -1,5 +1,5 @@
using MessengerApi.Configuration.Model; using MessengerApi.Configuration.Model.Persistence;
using MessengerApi.Configuration.Model.Persistence; using MessengerApi.Configuration.Model.Persistence.Base;
using MessengerApi.Contracts.Factories; using MessengerApi.Contracts.Factories;
using MessengerApi.Db; using MessengerApi.Db;
using MessengerApi.Db.Npg; using MessengerApi.Db.Npg;
@ -10,22 +10,22 @@ namespace MessengerApi.Factories
{ {
public class DbContextFactory : IDbContextFactory, IDbContextFactory<MessengerDbContext> public class DbContextFactory : IDbContextFactory, IDbContextFactory<MessengerDbContext>
{ {
private readonly MessengerConfiguration configuration; private readonly PersistenceConfiguration configuration;
public DbContextFactory(MessengerConfiguration configuration) public DbContextFactory(PersistenceConfiguration configuration)
{ {
this.configuration = configuration; this.configuration = configuration;
} }
public MessengerDbContext CreateDbContext() public MessengerDbContext CreateDbContext()
{ {
if (this.configuration.PersistenceConfiguration.PersistenceType == Configuration.Enums.PersistenceTypes.Sql) if (this.configuration.PersistenceType == Configuration.Enums.PersistenceTypes.Sql)
{ {
return new MessengerSqlDbContext((configuration.PersistenceConfiguration as SqlPersistenceConfiguration).ConnectionString); return new MessengerSqlDbContext((configuration as SqlPersistenceConfiguration).ConnectionString);
} }
else if (this.configuration.PersistenceConfiguration.PersistenceType == Configuration.Enums.PersistenceTypes.PostgreSql) else if (this.configuration.PersistenceType == Configuration.Enums.PersistenceTypes.PostgreSql)
{ {
return new MessengerNpgDbContext((configuration.PersistenceConfiguration as NpgPersistenceConfiguration).ConnectionString); return new MessengerNpgDbContext((configuration as NpgPersistenceConfiguration).ConnectionString);
} }
throw new NotImplementedException(); throw new NotImplementedException();

View File

@ -32,7 +32,7 @@ namespace MessengerApi.Handlers.Endpoint
Guid? toUserId, Guid? toUserId,
string payload, string payload,
string payloadType, string payloadType,
int? payloadLifespanInSeconds) int? timeToLiveInSeconds)
{ {
// Authorize. // Authorize.
var targetRecipientId = toUserId.HasValue var targetRecipientId = toUserId.HasValue
@ -50,7 +50,7 @@ namespace MessengerApi.Handlers.Endpoint
ToId = targetRecipientId, ToId = targetRecipientId,
Payload = payload, Payload = payload,
PayloadType = payloadType, PayloadType = payloadType,
PayloadLifespanInSeconds = payloadLifespanInSeconds ?? (this.configuration.DefaultMessageLifetimeInMinutes * 60) TimeToLiveInSeconds = timeToLiveInSeconds ?? (this.configuration.DefaultMessageTimeToLiveInSeconds)
}; };
this.unitOfWork.Messages.Add(message); this.unitOfWork.Messages.Add(message);

View File

@ -8,6 +8,6 @@
public string PayloadType { get; set; } public string PayloadType { get; set; }
public int? PayloadLifetimeInSeconds { get; set; } public int? TimeToLiveInSeconds { get; set; }
} }
} }

View File

@ -1,5 +1,6 @@
using MessengerApi.Configuration.Model; using MessengerApi.Configuration.Model;
using MessengerApi.Configuration.Model.Persistence; using MessengerApi.Configuration.Model.Persistence;
using MessengerApi.Configuration.Model.Persistence.Base;
using MessengerApi.Configuration.Sources.Environment; using MessengerApi.Configuration.Sources.Environment;
using MessengerApi.Contracts.Factories; using MessengerApi.Contracts.Factories;
using MessengerApi.Contracts.Models.Scoped; using MessengerApi.Contracts.Models.Scoped;
@ -41,6 +42,7 @@ namespace MessengerApi.Api
builder.Services.AddMemoryCache(); builder.Services.AddMemoryCache();
builder.Services.AddSingleton<MessengerConfiguration>(configuration); builder.Services.AddSingleton<MessengerConfiguration>(configuration);
builder.Services.AddSingleton<PersistenceConfiguration>(configuration.PersistenceConfiguration);
builder.Services.AddSingleton<ILogger>(new Factories.LoggerFactory(configuration).CreateLogger()); builder.Services.AddSingleton<ILogger>(new Factories.LoggerFactory(configuration).CreateLogger());
builder.Services.AddSingleton<SendEndpointHandler>(); builder.Services.AddSingleton<SendEndpointHandler>();
builder.Services.AddSingleton<HousekeepingHandler>(); builder.Services.AddSingleton<HousekeepingHandler>();
@ -103,7 +105,6 @@ namespace MessengerApi.Api
}); });
var app = builder.Build(); var app = builder.Build();
app.UseDeveloperExceptionPage();
// DB Migrations // DB Migrations
using (var ctx = app.Services.GetRequiredService<IDbContextFactory>().CreateDbContext()) using (var ctx = app.Services.GetRequiredService<IDbContextFactory>().CreateDbContext())
@ -183,7 +184,7 @@ namespace MessengerApi.Api
{ {
try try
{ {
var response = await handler.SendMessage(request.ToUserId, request.Payload, request.PayloadType, request.PayloadLifetimeInSeconds); var response = await handler.SendMessage(request.ToUserId, request.Payload, request.PayloadType, request.TimeToLiveInSeconds);
await unitOfWork.SaveChanges(); await unitOfWork.SaveChanges();
return Results.Json(response.Id); return Results.Json(response.Id);
} }

View File

@ -4,13 +4,12 @@
"commandName": "Project", "commandName": "Project",
"launchBrowser": true, "launchBrowser": true,
"environmentVariables": { "environmentVariables": {
"PERSISTENCE_TYPE": "Sql",
"USERSCONFIG_FILE_PATH": "./../../assets/example-users.config", "USERSCONFIG_FILE_PATH": "./../../assets/example-users.config",
"SQL_CONNECTIONSTRING": "" "SQL_CONNECTIONSTRING": ""
"CORS_ORIGINS": "", "CORS_ORIGINS": "",
"PROXIES": "", "PROXIES": "",
"QUERY_RATE_PER_MINUTE": "100", "QUERY_RATE_PER_MINUTE": "100",
"DEFAULT_MESSAGE_LIFETIME_IN_MINUTES": "60", "DEFAULT_MESSAGE_TIME_TO_LIVE_IN_SECONDS": "60",
"HOUSEKEEPING_ENABLED": "False", "HOUSEKEEPING_ENABLED": "False",
"LOGGING_VERBOSITY": "Trace" "LOGGING_VERBOSITY": "Trace"
}, },

View File

@ -1,7 +0,0 @@
services:
messengerapi:
image: https://gitea.masita.net/mc/messengerapi:latest
container_name: messengerapi
restart: unless-stopped
environment:
- ASPNETCORE_ENVIRONMENT=Production