feat: initial working version of service refs NOISSUE
Some checks failed
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m36s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
SonarQube Scan / SonarQube Trigger (push) Failing after 4m42s

This commit is contained in:
simon 2025-04-14 22:03:58 +02:00
parent 2bf3258081
commit d04b453e6f
30 changed files with 1146 additions and 72 deletions

View File

@ -1,4 +0,0 @@
FROM mcr.microsoft.com/dotnet/runtime:9.0
COPY ./song_of_the_day/bin/Release/net9.0/ /app
WORKDIR /app
CMD ["song_of_the_day"]

View File

@ -4,8 +4,8 @@
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
<add key="gitea-projects" value="https://git.disi.dev/api/packages/Projects/nuget/index.json" />
<!--add key="gitea-projects" value="https://git.disi.dev/api/packages/Projects/nuget/index.json" />
<add key="gitea-homelab" value="https://git.disi.dev/api/packages/Homelab/nuget/index.json" />
<add key="gitea-artifacts" value="https://git.disi.dev/api/packages/Artifacts/nuget/index.json" />
<add key="gitea-artifacts" value="https://git.disi.dev/api/packages/Artifacts/nuget/index.json" /-->
</packageSources>
</configuration>

View File

@ -0,0 +1,75 @@
public class AppConfiguration
{
public static AppConfiguration Instance = new AppConfiguration();
private AppConfiguration()
{
this.SignalAPIEndpointUri = Environment.GetEnvironmentVariable("SIGNAL_API_URI") ?? "http://192.168.1.108";
this.SignalAPIEndpointPort = Environment.GetEnvironmentVariable("SIGNAL_API_PORT") ?? "8719";
this.HostPhoneNumber = Environment.GetEnvironmentVariable("HOST_PHONE") ?? "+4367762751895";
this.DatabaseUri = Environment.GetEnvironmentVariable("DB_URI") ?? "192.168.1.108";
this.DatabasePort = Environment.GetEnvironmentVariable("DB_PORT") ?? "5477";
this.DatabaseName = Environment.GetEnvironmentVariable("DB_NAME") ?? "sotd";
this.DatabaseUser = Environment.GetEnvironmentVariable("DB_USER") ?? "sotd";
this.DatabasePW = Environment.GetEnvironmentVariable("DB_PASS") ?? "SotdP0stgresP4ss";
this.SignalGroupId = Environment.GetEnvironmentVariable("SIGNAL_GROUP_ID") ?? "group.Wmk1UTVQTnh0Sjd6a0xiOGhnTnMzZlNkc2p2Q3c0SXJiQkU2eDlNU0hyTT0=";
this.WebUIBaseURL = Environment.GetEnvironmentVariable("WEB_BASE_URL") ?? "https://sotd.disi.dev/";
this.UseBotTag = bool.Parse(Environment.GetEnvironmentVariable("USE_BOT_TAG") ?? "true");
}
public string SignalAPIEndpointUri
{
get; private set;
}
public string SignalAPIEndpointPort
{
get; private set;
}
public string DatabaseUri
{
get; private set;
}
public string DatabasePort
{
get; private set;
}
public string DatabaseName
{
get; private set;
}
public string DatabaseUser
{
get; private set;
}
public string DatabasePW
{
get; private set;
}
public string SignalGroupId
{
get; private set;
}
public string HostPhoneNumber
{
get; private set;
}
public string WebUIBaseURL
{
get; private set;
}
public bool UseBotTag
{
get; private set;
}
}

View File

@ -1,13 +1,20 @@
using Microsoft.EntityFrameworkCore;
using sotd.Pages;
public class DataContext : DbContext
{
public static DataContext Instance;
public static DataContext Instance
{
get { return new DataContext(); }
}
public DbSet<User> Users { get; set; }
public DbSet<Song> Songs { get; set; }
public DbSet<SongSuggestion> SongSuggestions { get; set; }
public DbSet<User>? Users { get; set; }
public DbSet<Song>? Songs { get; set; }
public DbSet<SongSuggestion>? SongSuggestions { get; set; }
public DbSet<SuggestionHelper>? SuggestionHelpers { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseNpgsql(@"Host=192.168.1.108:5477;Username=sotd;Password=SotdP0stgresP4ss;Database=sotd");
=> optionsBuilder.UseNpgsql($"Host={AppConfiguration.Instance.DatabaseUri}:{AppConfiguration.Instance.DatabasePort};"
+ $"Username={AppConfiguration.Instance.DatabaseUser};Password={AppConfiguration.Instance.DatabasePW};"
+ $"Database={AppConfiguration.Instance.DatabaseName}");
}

View File

@ -0,0 +1,127 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace song_of_the_day.DataMigrations
{
[DbContext(typeof(DataContext))]
[Migration("20250413192634_AdditionalFields")]
partial class AdditionalFields
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Song", b =>
{
b.Property<int>("SongId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("SongId"));
b.Property<string>("Artist")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.HasKey("SongId");
b.ToTable("Songs");
});
modelBuilder.Entity("SongSuggestion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<int>("SongId")
.HasColumnType("integer");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("SongId");
b.HasIndex("UserId");
b.ToTable("SongSuggestions");
});
modelBuilder.Entity("User", b =>
{
b.Property<int>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("UserId"));
b.Property<bool>("IsIntroduced")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SignalId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("UserName")
.IsRequired()
.HasColumnType("text");
b.HasKey("UserId");
b.ToTable("Users");
});
modelBuilder.Entity("SongSuggestion", b =>
{
b.HasOne("Song", "Song")
.WithMany()
.HasForeignKey("SongId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Song");
b.Navigation("User");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace song_of_the_day.DataMigrations
{
/// <inheritdoc />
public partial class AdditionalFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsIntroduced",
table: "Users",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "UserName",
table: "Users",
type: "text",
nullable: false,
defaultValue: "");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsIntroduced",
table: "Users");
migrationBuilder.DropColumn(
name: "UserName",
table: "Users");
}
}
}

View File

@ -0,0 +1,127 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace song_of_the_day.DataMigrations
{
[DbContext(typeof(DataContext))]
[Migration("20250414161136_Update user properties")]
partial class Updateuserproperties
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Song", b =>
{
b.Property<int>("SongId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("SongId"));
b.Property<string>("Artist")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.HasKey("SongId");
b.ToTable("Songs");
});
modelBuilder.Entity("SongSuggestion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<int>("SongId")
.HasColumnType("integer");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("SongId");
b.HasIndex("UserId");
b.ToTable("SongSuggestions");
});
modelBuilder.Entity("User", b =>
{
b.Property<int>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("UserId"));
b.Property<bool>("IsIntroduced")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("NickName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SignalMemberId")
.IsRequired()
.HasColumnType("text");
b.HasKey("UserId");
b.ToTable("Users");
});
modelBuilder.Entity("SongSuggestion", b =>
{
b.HasOne("Song", "Song")
.WithMany()
.HasForeignKey("SongId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Song");
b.Navigation("User");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace song_of_the_day.DataMigrations
{
/// <inheritdoc />
public partial class Updateuserproperties : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "UserName",
table: "Users",
newName: "SignalMemberId");
migrationBuilder.RenameColumn(
name: "SignalId",
table: "Users",
newName: "NickName");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "SignalMemberId",
table: "Users",
newName: "UserName");
migrationBuilder.RenameColumn(
name: "NickName",
table: "Users",
newName: "SignalId");
}
}
}

View File

@ -0,0 +1,127 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace song_of_the_day.DataMigrations
{
[DbContext(typeof(DataContext))]
[Migration("20250414181019_Add SuggestionHelper entity")]
partial class AddSuggestionHelperentity
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Song", b =>
{
b.Property<int>("SongId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("SongId"));
b.Property<string>("Artist")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.HasKey("SongId");
b.ToTable("Songs");
});
modelBuilder.Entity("SongSuggestion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<int>("SongId")
.HasColumnType("integer");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("SongId");
b.HasIndex("UserId");
b.ToTable("SongSuggestions");
});
modelBuilder.Entity("User", b =>
{
b.Property<int>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("UserId"));
b.Property<bool>("IsIntroduced")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("NickName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SignalMemberId")
.IsRequired()
.HasColumnType("text");
b.HasKey("UserId");
b.ToTable("Users");
});
modelBuilder.Entity("SongSuggestion", b =>
{
b.HasOne("Song", "Song")
.WithMany()
.HasForeignKey("SongId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Song");
b.Navigation("User");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace song_of_the_day.DataMigrations
{
/// <inheritdoc />
public partial class AddSuggestionHelperentity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -0,0 +1,148 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace song_of_the_day.DataMigrations
{
[DbContext(typeof(DataContext))]
[Migration("20250414181911_Now really add SuggestionHelper entity")]
partial class NowreallyaddSuggestionHelperentity
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Song", b =>
{
b.Property<int>("SongId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("SongId"));
b.Property<string>("Artist")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.HasKey("SongId");
b.ToTable("Songs");
});
modelBuilder.Entity("SongSuggestion", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<int>("SongId")
.HasColumnType("integer");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("SongId");
b.HasIndex("UserId");
b.ToTable("SongSuggestions");
});
modelBuilder.Entity("SuggestionHelper", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("SuggestionHelpers");
});
modelBuilder.Entity("User", b =>
{
b.Property<int>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("UserId"));
b.Property<bool>("IsIntroduced")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("NickName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SignalMemberId")
.IsRequired()
.HasColumnType("text");
b.HasKey("UserId");
b.ToTable("Users");
});
modelBuilder.Entity("SongSuggestion", b =>
{
b.HasOne("Song", "Song")
.WithMany()
.HasForeignKey("SongId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Song");
b.Navigation("User");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace song_of_the_day.DataMigrations
{
/// <inheritdoc />
public partial class NowreallyaddSuggestionHelperentity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SuggestionHelpers",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Title = table.Column<string>(type: "text", nullable: false),
Description = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SuggestionHelpers", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SuggestionHelpers");
}
}
}

View File

@ -72,6 +72,27 @@ namespace song_of_the_day.DataMigrations
b.ToTable("SongSuggestions");
});
modelBuilder.Entity("SuggestionHelper", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("SuggestionHelpers");
});
modelBuilder.Entity("User", b =>
{
b.Property<int>("UserId")
@ -80,11 +101,18 @@ namespace song_of_the_day.DataMigrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("UserId"));
b.Property<bool>("IsIntroduced")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SignalId")
b.Property<string>("NickName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SignalMemberId")
.IsRequired()
.HasColumnType("text");

View File

@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore;
public class Song
{
public int SongId { get; set; }
public string Name { get; set; }
public string Artist { get; set; }
public string Url { get; set; }
public string? Name { get; set; }
public string? Artist { get; set; }
public string? Url { get; set; }
}

View File

@ -3,8 +3,8 @@ using Microsoft.EntityFrameworkCore;
public class SongSuggestion
{
public int Id { get; set;}
public User User { get; set; }
public Song Song { get; set; }
public int Id { get; set; }
public User? User { get; set; }
public Song? Song { get; set; }
public DateTime Date { get; set; }
}

View File

@ -0,0 +1,9 @@
using Microsoft.EntityFrameworkCore;
public class SuggestionHelper
{
public int Id { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
}

View File

@ -3,6 +3,8 @@ using Microsoft.EntityFrameworkCore;
public class User
{
public int UserId { get; set; }
public string SignalId { get; set; }
public string Name { get; set; }
public string? SignalMemberId { get; set; }
public string? Name { get; set; }
public string? NickName { get; set; }
public bool IsIntroduced { get; set; }
}

View File

@ -0,0 +1,15 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /App
# Copy everything
COPY . ./
# Restore as distinct layers
RUN dotnet restore
# Build and publish a release
RUN dotnet publish -o out
# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /App
COPY --from=build /App/out .
ENTRYPOINT ["dotnet", "song_of_the_day.dll"]

View File

@ -1,12 +1,17 @@
using System.Collections;
using System.ComponentModel;
using song_of_the_day;
public class SignalIntegration
{
public static SignalIntegration Instance;
public static SignalIntegration? Instance;
public SignalIntegration(string uri, int port, string phoneNumber)
{
var http = new HttpClient() {
BaseAddress = new Uri("http://" + uri + ":" + port)
var http = new HttpClient()
{
BaseAddress = new Uri(uri + ":" + port)
};
apiClient = new song_of_the_day.swaggerClient(http);
apiClient.BaseUrl = "";
@ -17,17 +22,103 @@ public class SignalIntegration
private string phoneNumber;
public async Task ListGroups()
public async Task ListGroupsAsync()
{
try
{
Console.WriteLine("listing groups");
try {
ICollection<song_of_the_day.GroupEntry> groupEntries = await apiClient.GroupsAllAsync(this.phoneNumber);
Console.WriteLine($"{groupEntries.Count} groups");
foreach (var group in groupEntries)
{
Console.WriteLine($"{group.Name} {group.Id}");
}
}
catch (Exception ex)
{
Console.WriteLine("Exception (ListGroupsAsync): " + ex.Message);
}
catch (Exception ex) {
Console.WriteLine("Exception: " + ex.Message);
}
Console.WriteLine("listing groups done");
public async Task SendMessageToGroupAsync(string message)
{
try
{
SendMessageV2 data = new SendMessageV2();
data.Recipients = new List<string>();
data.Recipients.Add(AppConfiguration.Instance.SignalGroupId);
data.Message = message;
data.Text_mode = SendMessageV2Text_mode.Styled;
data.Number = AppConfiguration.Instance.HostPhoneNumber;
var response = await apiClient.Send2Async(data);
}
catch (Exception ex)
{
Console.WriteLine("Exception (SendMessageToGroupAsync): " + ex.Message);
}
}
public async Task SendMessageToUserAsync(string message, string userId)
{
try
{
SendMessageV2 data = new SendMessageV2();
data.Recipients = new List<string>();
data.Recipients.Add(userId);
data.Message = (AppConfiguration.Instance.UseBotTag ? "**[Proggy]**\n" : "") + message;
data.Text_mode = SendMessageV2Text_mode.Styled;
data.Number = AppConfiguration.Instance.HostPhoneNumber;
var response = await apiClient.Send2Async(data);
}
catch (Exception ex)
{
Console.WriteLine("Exception (SendMessageToUserAsync): " + ex.Message);
}
}
public async Task IntroduceUserAsync(User user)
{
await this.SendMessageToUserAsync("Hi, my name is Proggy and I am your friendly neighborhood *Song of the Day* bot!", user.SignalMemberId);
await this.SendMessageToUserAsync("You are receiving this message because you have been invited to a *Song of the Day* community group.", user.SignalMemberId);
await this.SendMessageToUserAsync("In that community group I will pick a person at random each day at 8 AM and encourage them to share a song with the rest of the community.", user.SignalMemberId);
if (AppConfiguration.Instance.UseBotTag)
{
await this.SendMessageToUserAsync("You can always see which messages are sent by me rather than the community host by the **[Proggy]** tag at the beginning of the message", user.SignalMemberId);
}
await this.SendMessageToUserAsync($"Not right now, but eventually you will be able to see more details about your community at {AppConfiguration.Instance.WebUIBaseURL}.", user.SignalMemberId);
await this.SendMessageToUserAsync($"""You can navigate to {AppConfiguration.Instance.WebUIBaseURL + (AppConfiguration.Instance.WebUIBaseURL.EndsWith("/") ? "" : "/")}User/{user.UserId} to set your preferred display name for me to use.""", user.SignalMemberId);
await this.SendMessageToUserAsync($"Now have fun and enjoy being a part of this community!", user.SignalMemberId);
}
public async Task<ICollection<string>> GetMemberListAsync()
{
try
{
var response = await apiClient.Groups2Async(AppConfiguration.Instance.HostPhoneNumber, AppConfiguration.Instance.SignalGroupId);
return response.Members;
}
catch (Exception ex)
{
Console.WriteLine("Exception (GetMemberListAsync): " + ex.Message);
}
return new List<string>();
}
public async Task<ListContactsResponse> GetContactAsync(string memberId)
{
try
{
var allIdentities = await apiClient.ContactsAllAsync(AppConfiguration.Instance.HostPhoneNumber);
var identityCandidates = allIdentities.Where(u => u.Number == memberId || u.Uuid == memberId);
var identity = identityCandidates.SingleOrDefault();
if (identity == null)
{
throw new Exception($"Could not determine identity for memberId '{memberId}'!");
}
return identity;
}
catch (Exception ex)
{
Console.WriteLine("Exception (GetContactAsync): " + ex.Message);
return new ListContactsResponse();
}
}
}

View File

@ -1,7 +1,7 @@
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
ViewData["Title"] = "Song of the Day";
}
<div class="text-center">

View File

@ -1,8 +0,0 @@
@page
@model PrivacyModel
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>

View File

@ -1,19 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace sotd.Pages;
public class PrivacyModel : PageModel
{
private readonly ILogger<PrivacyModel> _logger;
public PrivacyModel(ILogger<PrivacyModel> logger)
{
_logger = logger;
}
public void OnGet()
{
}
}

View File

@ -13,7 +13,7 @@
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-page="/Index">sotd</a>
<a class="navbar-brand" asp-area="" asp-page="/Index">Song of the Day</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
@ -24,7 +24,7 @@
<a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
<a class="nav-link text-dark" asp-area="" asp-page="/SuggestionHelpers">Suggestion Helpers</a>
</li>
</ul>
</div>
@ -39,7 +39,7 @@
<footer class="border-top footer text-muted">
<div class="container">
&copy; 2025 - sotd - <a asp-area="" asp-page="/Privacy">Privacy</a>
&copy; 2025 - Song of the Day
</div>
</footer>

View File

@ -0,0 +1,32 @@
@page
@model SuggestionHelpersModel
@{
ViewData["Title"] = "Suggestion Helpers";
}
<div class="text-left">
<table>
<tr>
<th>Title</th>
<th>Description</th>
</tr>
@foreach (var helper in @Model.SuggestionHelpers)
{
var title = helper.Title; var description = helper.Description;
<tr>
<td>@title</td>
<td>@description</td>
</tr>
}
</table>
<hr />
<form method="post">
<label asp-for="NewSuggestionTitle">Title</label>
<input asp-for="NewSuggestionTitle" />
<br />
<label asp-for="NewSuggestionDescription">Description</label>
<input asp-for="NewSuggestionDescription" />
<br />
<input type="submit" title="Submit" />
</form>
</div>

View File

@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace sotd.Pages;
public class SuggestionHelpersModel : PageModel
{
private readonly ILogger<SuggestionHelpersModel> _logger;
public SuggestionHelpersModel(ILogger<SuggestionHelpersModel> logger)
{
_logger = logger;
this.NewSuggestionDescription = "";
this.NewSuggestionTitle = "";
this.SuggestionHelpers = new List<SuggestionHelper>();
}
[BindProperty]
public ICollection<SuggestionHelper> SuggestionHelpers { get; set; }
[BindProperty]
public string NewSuggestionTitle { get; set; }
[BindProperty]
public string NewSuggestionDescription { get; set; }
public void OnGet()
{
using (var dci = DataContext.Instance)
{
this.SuggestionHelpers = dci.SuggestionHelpers.ToList();
}
}
public void OnPost()
{
using (var dci = DataContext.Instance)
{
var newHelper = new SuggestionHelper()
{
Title = this.NewSuggestionTitle,
Description = this.NewSuggestionDescription
};
dci.SuggestionHelpers.Add(newHelper);
dci.SaveChanges();
this.SuggestionHelpers = dci.SuggestionHelpers.ToList();
}
this.NewSuggestionDescription = "";
this.NewSuggestionTitle = "";
}
}

View File

@ -0,0 +1,17 @@
@page "{userIndex}"
@model UserModel
@{
ViewData["Title"] = "User #" + @Model.userId;
}
<div class="text-left">
<form method="post">
<label asp-for="UserNickName">Preferred Name</label>
<input asp-for="UserNickName" />
<br />
<label asp-for="UserName">Contact Name</label>
<input asp-for="UserName" disabled />
<br />
<input type="submit" title="Submit" />
</form>
</div>

View File

@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace sotd.Pages;
public class UserModel : PageModel
{
private readonly ILogger<UserModel> _logger;
public UserModel(ILogger<UserModel> logger)
{
_logger = logger;
this.UserNickName = "";
this.UserName = "";
}
public int userId { get; set; }
public string UserName { get; set; }
[BindProperty]
public string UserNickName { get; set; }
public void OnGet(int userIndex)
{
using (var dci = DataContext.Instance)
{
var user = dci.Users.Find(userIndex);
this.UserName = user.Name;
this.UserNickName = user.NickName;
this.userId = userIndex;
}
}
public void OnPost(int userIndex)
{
using (var dci = DataContext.Instance)
{
var user = dci.Users.Find(userIndex);
user.NickName = this.UserNickName;
dci.SaveChanges();
this.UserName = user.Name;
}
}
}

View File

@ -1,14 +1,83 @@

using Scalar.AspNetCore;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.EntityFrameworkCore;
DataContext.Instance = new DataContext();
var groupId = "group.Wmk1UTVQTnh0Sjd6a0xiOGhnTnMzZlNkc2p2Q3c0SXJiQkU2eDlNU0hyTT0=";
SignalIntegration.Instance = new SignalIntegration("192.168.1.108", 8719, "+4367762751895");
await SignalIntegration.Instance.ListGroups();
SignalIntegration.Instance = new SignalIntegration(AppConfiguration.Instance.SignalAPIEndpointUri,
int.Parse(AppConfiguration.Instance.SignalAPIEndpointPort),
AppConfiguration.Instance.HostPhoneNumber);
var builder = WebApplication.CreateBuilder(args);
var userCheckTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
userCheckTimer.OnOccurence += async (s, ea) =>
{
var memberList = await SignalIntegration.Instance.GetMemberListAsync();
var dci = DataContext.Instance;
var needsSaving = false;
foreach (var memberId in memberList)
{
var foundUser = dci.Users.Where(u => u.SignalMemberId == memberId).SingleOrDefault();
if (foundUser == null)
{
var newUserContact = await SignalIntegration.Instance.GetContactAsync(memberId);
Console.WriteLine("New user:");
Console.WriteLine($" Name: {newUserContact.Name}");
Console.WriteLine($" MemberId: {memberId}");
User newUser = new User()
{
Name = newUserContact.Name,
SignalMemberId = memberId,
NickName = string.Empty,
IsIntroduced = false
};
needsSaving = true;
}
}
if (needsSaving)
{
await dci.SaveChangesAsync();
}
await dci.DisposeAsync();
};
userCheckTimer.Start();
var userIntroTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
userIntroTimer.OnOccurence += async (s, ea) =>
{
var dci = DataContext.Instance;
var introUsers = dci.Users.Where(u => !u.IsIntroduced);
bool needsSaving = false;
foreach (var user in introUsers)
{
await SignalIntegration.Instance.IntroduceUserAsync(user);
user.IsIntroduced = true;
needsSaving = true;
}
if (needsSaving)
{
await dci.SaveChangesAsync();
}
await dci.DisposeAsync();
};
userIntroTimer.Start();
var pickOfTheDayTimer = new CronTimer("0 8 * * *", "Europe/Vienna", includingSeconds: false);
pickOfTheDayTimer.OnOccurence += async (s, ea) =>
{
var dci = DataContext.Instance;
var luckyUser = await dci.Users.ElementAtAsync((new Random()).Next(await dci.Users.CountAsync()));
var userName = string.IsNullOrEmpty(luckyUser.NickName) ? luckyUser.Name : luckyUser.NickName;
SignalIntegration.Instance.SendMessageToGroupAsync($"Today's chosen person to share a song is: **{userName}**");
SignalIntegration.Instance.SendMessageToUserAsync($"Congratulations, you have been chosen to share a song today!", luckyUser.SignalMemberId);
var suggestion = await dci.SuggestionHelpers.ElementAtAsync((new Random()).Next(await dci.SuggestionHelpers.CountAsync()));
SignalIntegration.Instance.SendMessageToUserAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*", luckyUser.SignalMemberId);
SignalIntegration.Instance.SendMessageToUserAsync($"For now please just share your suggestion with the group - in the future I might ask you to share directly with me or via the website to help me keep track of past suggestions!", luckyUser.SignalMemberId);
};
pickOfTheDayTimer.Start();
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddOpenApi();
@ -35,6 +104,3 @@ app.MapRazorPages()
.WithStaticAssets();
app.Run();
//Console.WriteLine("Size: " + DataContext.Instance.Songs.Count());
//await SignalIntegration.Instance.ListGroups();

View File

@ -16,6 +16,7 @@
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="NSwag.ApiDescription.Client" Version="13.0.5" />
<PackageReference Include="Scalar.AspNetCore" Version="2.1.*" />
<PackageReference Include="CronTimer" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<OpenApiReference Include="swagger.json" SourceUrl="https://bbernhard.github.io/signal-cli-rest-api/src/docs/swagger.json" />