fix: some cleanup and fixing runtime bugs, refs NOISSUE

This commit is contained in:
simon 2025-05-31 13:41:03 +02:00
parent dbd83ebb6a
commit 0d2ec3712e
23 changed files with 269 additions and 128 deletions

View File

@ -9,7 +9,7 @@ RUN apt update && apt install libldap-2.5-0 -y
# Restore as distinct layers # Restore as distinct layers
RUN dotnet restore ./song_of_the_day/song_of_the_day.csproj RUN dotnet restore ./song_of_the_day/song_of_the_day.csproj
# Build and publish a release # Build and publish a release
RUN dotnet publish ./song_of_the_day/song_of_the_day.csproj -o out RUN dotnet publish ./song_of_the_day/song_of_the_day.csproj -o out /p:EnvironmentName=Production
# Build runtime image # Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:9.0 FROM mcr.microsoft.com/dotnet/aspnet:9.0

View File

@ -2,7 +2,7 @@
.PHONY: issetup .PHONY: issetup
issetup: issetup:
@[ -f .git/hooks/commit-msg ] || [ $SKIP_MAKE_SETUP_CHECK = "true" ] || (echo "You must run 'make setup' first to initialize the repo!" && exit 1) @[ -f .git/hooks/commit-msg ] || [ ${SKIP_MAKE_SETUP_CHECK} = "true" ] || (echo "You must run 'make setup' first to initialize the repo!" && exit 1)
.PHONY: setup .PHONY: setup
setup: setup:

View File

@ -2,3 +2,5 @@
# CS8981: The type name only contains lower-cased ascii characters. Such names may become reserved for the language. # CS8981: The type name only contains lower-cased ascii characters. Such names may become reserved for the language.
dotnet_diagnostic.CS8981.severity = none dotnet_diagnostic.CS8981.severity = none
dotnet_diagnostic.CS8602.severity = none
dotnet_diagnostic.CS8604.severity = none

46
song_of_the_day/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,46 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "C#: SongOfTheDay Debug",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "publish Debug",
"program": "${workspaceFolder}/bin/Debug/net9.0/publish/song_of_the_day.dll",
"cwd": "${workspaceFolder}/bin/Debug/net9.0/publish/",
"args": [
"/p:EnvironmentName=Development"
],
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"Logging__LogLevel__Microsoft": "Information"
}
},
{
"name": "C#: SongOfTheDay Production",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "publish Release",
"program": "${workspaceFolder}/bin/Release/net9.0/publish/song_of_the_day.dll",
"cwd": "${workspaceFolder}/bin/Release/net9.0/publish/",
"args": [
"/p:EnvironmentName=Production"
],
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Production",
"Logging__LogLevel__Microsoft": "Information"
}
}
]
}

47
song_of_the_day/.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,47 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "publish Debug",
"command": "dotnet",
"type": "shell",
"args": [
"publish",
// Ask dotnet build to generate full paths for file names.
"/property:GenerateFullPaths=true",
// Do not generate summary otherwise it leads to duplicate errors in Problems panel
"/consoleloggerparameters:NoSummary",
"/p:EnvironmentName=Development",
"-c",
"Debug"
],
"group": "build",
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
},
{
"label": "publish Release",
"command": "dotnet",
"type": "shell",
"args": [
"publish",
// Ask dotnet build to generate full paths for file names.
"/property:GenerateFullPaths=true",
// Do not generate summary otherwise it leads to duplicate errors in Problems panel
"/consoleloggerparameters:NoSummary",
"/p:EnvironmentName=Production",
"-c",
"Release"
],
"group": "build",
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
}
]
}

View File

@ -1,15 +1,16 @@
public class LdapAuthenticationService : IAuthenticationService public class LdapAuthenticationService : IAuthenticationService
{ {
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private LdapIntegration _ldapIntegration;
public LdapAuthenticationService(IConfiguration configuration) public LdapAuthenticationService(IConfiguration configuration, LdapIntegration ldapIntegration)
{ {
_configuration = configuration; _configuration = configuration;
_ldapIntegration = ldapIntegration;
} }
public bool Authenticate(string username, string password) public bool Authenticate(string username, string password)
{ {
var ldapInstance = LdapIntegration.Instance; return _ldapIntegration == null ? false : _ldapIntegration.TestLogin(username, password);
return ldapInstance == null ? false : ldapInstance.TestLogin(username, password);
} }
} }

View File

@ -2,11 +2,13 @@ public class PhoneClaimCodeProviderService
{ {
private Dictionary<string, string> _phoneClaimCodes; private Dictionary<string, string> _phoneClaimCodes;
private Dictionary<string, string> _phoneClaimNumbers; private Dictionary<string, string> _phoneClaimNumbers;
private SignalIntegration _signalIntegration;
public PhoneClaimCodeProviderService() public PhoneClaimCodeProviderService(SignalIntegration signalIntegration)
{ {
_phoneClaimCodes = new Dictionary<string, string>(); _phoneClaimCodes = new Dictionary<string, string>();
_phoneClaimNumbers = new Dictionary<string, string>(); _phoneClaimNumbers = new Dictionary<string, string>();
_signalIntegration = signalIntegration;
} }
private static Random random = new Random(); private static Random random = new Random();
@ -32,7 +34,10 @@ public class PhoneClaimCodeProviderService
_phoneClaimNumbers[username] = phoneNumber; _phoneClaimNumbers[username] = phoneNumber;
} }
await SignalIntegration.Instance.SendMessageToUserAsync("Your phone number validation code is: " + generatedCode, phoneNumber); if (_signalIntegration != null)
{
await _signalIntegration.SendMessageToUserAsync("Your phone number validation code is: " + generatedCode, phoneNumber);
}
} }
public string ValidateClaimCodeForUser(string code, string username) public string ValidateClaimCodeForUser(string code, string username)

View File

@ -6,8 +6,6 @@ using System.Linq;
public class LdapIntegration public class LdapIntegration
{ {
public static LdapIntegration? Instance;
private readonly string[] attributesToQuery = new string[] private readonly string[] attributesToQuery = new string[]
{ {
"uid", "uid",
@ -16,12 +14,13 @@ public class LdapIntegration
"mail" "mail"
}; };
public LdapIntegration(string uri, int port, string adminBind, string adminPass) public LdapIntegration(ILogger<LdapIntegration> logger)
{ {
this.Uri = uri; this.Uri = AppConfiguration.Instance.LDAPConfig.LDAPserver;
this.Port = port; this.Port = AppConfiguration.Instance.LDAPConfig.Port;
this.AdminBind = adminBind; this.AdminBind = AppConfiguration.Instance.LDAPConfig.Username;
this.AdminPass = adminPass; this.AdminPass = AppConfiguration.Instance.LDAPConfig.Password;
this.logger = logger;
} }
private string Uri { get; set; } private string Uri { get; set; }
@ -32,6 +31,8 @@ public class LdapIntegration
private string AdminPass { get; set; } private string AdminPass { get; set; }
private ILogger<LdapIntegration> logger { get; set; }
public bool TestLogin(string username, string password) public bool TestLogin(string username, string password)
{ {
try try

View File

@ -1,29 +1,28 @@
using System.Collections;
using System.ComponentModel;
using song_of_the_day; using song_of_the_day;
public class SignalIntegration public class SignalIntegration
{ {
public static SignalIntegration? Instance; private readonly ILogger<SignalIntegration> logger;
private readonly ILogger logger; public SignalIntegration(ILogger<SignalIntegration> logger)
public SignalIntegration(string uri, int port, string phoneNumber)
{ {
using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information));
this.logger = factory.CreateLogger("SignalIntegration"); var uri = AppConfiguration.Instance.SignalAPIEndpointUri;
var port = int.Parse(AppConfiguration.Instance.SignalAPIEndpointPort);
var phoneNumber = AppConfiguration.Instance.HostPhoneNumber;
this.logger = logger;
var http = new HttpClient() var http = new HttpClient()
{ {
BaseAddress = new Uri(uri + ":" + port) BaseAddress = new Uri(uri + ":" + port)
}; };
apiClient = new song_of_the_day.swaggerClient(http); apiClient = new swaggerClient(http);
apiClient.BaseUrl = ""; apiClient.BaseUrl = "";
this.phoneNumber = phoneNumber; this.phoneNumber = phoneNumber;
} }
private song_of_the_day.swaggerClient apiClient; private swaggerClient apiClient;
private string phoneNumber; private string phoneNumber;
@ -83,6 +82,11 @@ public class SignalIntegration
public async Task IntroduceUserAsync(User user) public async Task IntroduceUserAsync(User user)
{ {
if (user == null || user.SignalMemberId == null)
{
logger.LogWarning("Attempt to introduce unknown user was aborted.");
return;
}
await this.SendMessageToUserAsync("Hi, my name is Proggy and I am your friendly neighborhood *Song of the Day* bot!", user.SignalMemberId); 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("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); 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);

View File

@ -3,8 +3,8 @@
{ {
using (var dci = DataContext.Instance) using (var dci = DataContext.Instance)
{ {
var user = dci.Users.Where(u => u.LdapUserName == User.Identity.Name); var user = dci.Users?.Where(u => u.LdapUserName == User.Identity.Name);
return user.Any(); return user == null ? false : user.Any();
} }
} }
} }
@ -18,7 +18,7 @@
<script type="importmap"></script> <script type="importmap"></script>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/sotd.styles.css" asp-append-version="true" /> <link rel="stylesheet" href="~/song_of_the_day.styles.css" asp-append-version="true" />
</head> </head>
<body> <body>
<header> <header>
@ -37,13 +37,15 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/SuggestionHelpers">Suggestion Helpers</a> <a class="nav-link text-dark" asp-area="" asp-page="/SuggestionHelpers">Suggestion Helpers</a>
</li> </li>
@if (this.User.Identity.IsAuthenticated && !DoesUserHaveClaimedPhoneNumber()) @if (this.User != null && this.User.Identity != null &&
this.User.Identity.IsAuthenticated && !DoesUserHaveClaimedPhoneNumber())
{ {
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/UnclaimedPhoneNumbers">Unclaimed Phone Numbers</a> <a class="nav-link text-dark" asp-area="" asp-page="/UnclaimedPhoneNumbers">Unclaimed Phone Numbers</a>
</li> </li>
} }
@if (this.User.Identity.IsAuthenticated && DoesUserHaveClaimedPhoneNumber()) @if (this.User != null && this.User.Identity != null &&
this.User.Identity.IsAuthenticated && DoesUserHaveClaimedPhoneNumber())
{ {
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/SubmitSongs">Submit Songs</a> <a class="nav-link text-dark" asp-area="" asp-page="/SubmitSongs">Submit Songs</a>

View File

@ -1,7 +1,7 @@
@using Microsoft.AspNetCore.Authentication @using Microsoft.AspNetCore.Authentication
<div class="loginform"> <div class="loginform">
@if (!this.User.Identity.IsAuthenticated) @if (this.User == null || this.User.Identity == null || !this.User.Identity.IsAuthenticated)
{ {
<form method="post" action="Auth/Login"> <form method="post" action="Auth/Login">
<div> <div>

View File

@ -17,20 +17,26 @@ public class SubmitSongsModel : PageModel
{ {
_logger = logger; _logger = logger;
this.songResolver = songResolver; this.songResolver = songResolver;
_submitUrl = string.Empty;
SongData = new Song();
} }
[BindProperty] [BindProperty]
public bool IsValidUrl { get; set; } = true; public bool IsValidUrl { get; set; } = true;
[BindProperty] [BindProperty]
public string SubmitUrl { public string SubmitUrl
get { {
get
{
return _submitUrl; return _submitUrl;
} }
set { set
{
_submitUrl = value.ToString(); _submitUrl = value.ToString();
Uri? newValue = default; Uri? newValue = default;
try { try
{
newValue = new Uri(_submitUrl); newValue = new Uri(_submitUrl);
} }
catch (UriFormatException) catch (UriFormatException)
@ -41,7 +47,7 @@ public class SubmitSongsModel : PageModel
IsValidUrl = true; IsValidUrl = true;
if(this.songResolver.CanValidate(newValue)) if (this.songResolver.CanValidate(newValue))
{ {
this.SongData = this.songResolver.ResolveSongAsync(newValue).Result; this.SongData = this.songResolver.ResolveSongAsync(newValue).Result;
} }
@ -49,9 +55,13 @@ public class SubmitSongsModel : PageModel
} }
[BindProperty] [BindProperty]
public bool CanSubmit { get { public bool CanSubmit
return !string.IsNullOrEmpty(SongData?.Artist) && ! string.IsNullOrEmpty(SongData?.Name); {
} } get
{
return !string.IsNullOrEmpty(SongData?.Artist) && !string.IsNullOrEmpty(SongData?.Name);
}
}
[BindProperty] [BindProperty]
public Song SongData { get; set; } public Song SongData { get; set; }
@ -66,6 +76,6 @@ public class SubmitSongsModel : PageModel
{ {
var songUrl = Request.Query["SubmitUrl"]; var songUrl = Request.Query["SubmitUrl"];
this.SubmitUrl = songUrl.ToString(); this.SubmitUrl = songUrl.ToString();
return Partial("_SongPartial", SongData);; return Partial("_SongPartial", SongData); ;
} }
} }

View File

@ -29,7 +29,7 @@ public class SuggestionHelpersModel : PageModel
{ {
using (var dci = DataContext.Instance) using (var dci = DataContext.Instance)
{ {
this.SuggestionHelpers = dci.SuggestionHelpers.ToList(); this.SuggestionHelpers = dci.SuggestionHelpers == null ? new List<SuggestionHelper>() : dci.SuggestionHelpers.ToList();
} }
} }
@ -42,9 +42,9 @@ public class SuggestionHelpersModel : PageModel
Title = this.NewSuggestionTitle, Title = this.NewSuggestionTitle,
Description = this.NewSuggestionDescription Description = this.NewSuggestionDescription
}; };
dci.SuggestionHelpers.Add(newHelper); dci.SuggestionHelpers?.Add(newHelper);
dci.SaveChanges(); dci.SaveChanges();
this.SuggestionHelpers = dci.SuggestionHelpers.ToList(); this.SuggestionHelpers = dci.SuggestionHelpers == null ? new List<SuggestionHelper>() : dci.SuggestionHelpers.ToList();
} }
this.NewSuggestionDescription = ""; this.NewSuggestionDescription = "";
this.NewSuggestionTitle = ""; this.NewSuggestionTitle = "";

View File

@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.VisualBasic; using Microsoft.VisualBasic;
using SpotifyAPI.Web;
namespace sotd.Pages; namespace sotd.Pages;
@ -11,6 +12,7 @@ public class UnclaimedPhoneNumbersModel : PageModel
public UnclaimedPhoneNumbersModel(ILogger<UserModel> logger) public UnclaimedPhoneNumbersModel(ILogger<UserModel> logger)
{ {
_logger = logger; _logger = logger;
UnclaimedUsers = new List<User>();
} }
public int userId { get; set; } public int userId { get; set; }
@ -22,7 +24,7 @@ public class UnclaimedPhoneNumbersModel : PageModel
{ {
using (var dci = DataContext.Instance) using (var dci = DataContext.Instance)
{ {
this.UnclaimedUsers = dci.Users.Where(u => string.IsNullOrEmpty(u.LdapUserName)).ToList(); this.UnclaimedUsers = dci.Users == null ? new List<User>(): dci.Users.Where(u => string.IsNullOrEmpty(u.LdapUserName)).ToList();
} }
} }

View File

@ -25,9 +25,9 @@ public class UserModel : PageModel
{ {
using (var dci = DataContext.Instance) using (var dci = DataContext.Instance)
{ {
var user = dci.Users.Find(userIndex); var user = dci.Users?.Find(userIndex);
this.UserName = user.Name; this.UserName = user == null ? string.Empty : (user.Name ?? string.Empty);
this.UserNickName = user.NickName; this.UserNickName = user == null ? string.Empty : (user.NickName ?? string.Empty);
this.userId = userIndex; this.userId = userIndex;
} }
} }
@ -36,10 +36,13 @@ public class UserModel : PageModel
{ {
using (var dci = DataContext.Instance) using (var dci = DataContext.Instance)
{ {
var user = dci.Users.Find(userIndex); var user = dci.Users?.Find(userIndex);
if (user != null)
{
user.NickName = this.UserNickName; user.NickName = this.UserNickName;
dci.SaveChanges(); dci.SaveChanges();
this.UserName = user.Name; this.UserName = user.Name ?? string.Empty;
}
} }
} }
} }

View File

@ -1,31 +1,37 @@
 
using Scalar.AspNetCore; using Scalar.AspNetCore;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using System.DirectoryServices.Protocols;
using System.Runtime.CompilerServices;
SignalIntegration.Instance = new SignalIntegration(AppConfiguration.Instance.SignalAPIEndpointUri,
int.Parse(AppConfiguration.Instance.SignalAPIEndpointPort),
AppConfiguration.Instance.HostPhoneNumber);
LdapIntegration.Instance = new LdapIntegration(AppConfiguration.Instance.LDAPConfig.LDAPserver,
AppConfiguration.Instance.LDAPConfig.Port,
AppConfiguration.Instance.LDAPConfig.Username,
AppConfiguration.Instance.LDAPConfig.Password);
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information));
var logger = factory.CreateLogger("SongResolver"); // Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddOpenApi();
builder.Services.AddLogging((ILoggingBuilder b) => b.ClearProviders().AddConsole());
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Auth/Login";
});
builder.Services.AddSingleton<LdapAuthenticationService>();
builder.Services.AddSingleton<PhoneClaimCodeProviderService>();
builder.Services.AddSingleton<SignalIntegration>();
builder.Services.AddSingleton<LdapIntegration>();
builder.Services.AddSingleton<SongResolver>();
var app = builder.Build();
var logger = app.Logger;
logger.LogTrace("Setting up user check timer"); logger.LogTrace("Setting up user check timer");
var userCheckTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false); var userCheckTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
userCheckTimer.OnOccurence += async (s, ea) => userCheckTimer.OnOccurence += async (s, ea) =>
{ {
var memberList = await SignalIntegration.Instance.GetMemberListAsync(); var signalIntegration = app.Services.GetService<SignalIntegration>();
var memberList = await signalIntegration.GetMemberListAsync();
var dci = DataContext.Instance; var dci = DataContext.Instance;
var needsSaving = false; var needsSaving = false;
foreach (var memberId in memberList) foreach (var memberId in memberList)
@ -33,7 +39,7 @@ userCheckTimer.OnOccurence += async (s, ea) =>
var foundUser = dci.Users?.Where(u => u.SignalMemberId == memberId).SingleOrDefault(); var foundUser = dci.Users?.Where(u => u.SignalMemberId == memberId).SingleOrDefault();
if (foundUser == null) if (foundUser == null)
{ {
var newUserContact = await SignalIntegration.Instance.GetContactAsync(memberId); var newUserContact = await signalIntegration.GetContactAsync(memberId);
logger.LogDebug("New user:"); logger.LogDebug("New user:");
logger.LogDebug($" Name: {newUserContact.Name}"); logger.LogDebug($" Name: {newUserContact.Name}");
logger.LogDebug($" MemberId: {memberId}"); logger.LogDebug($" MemberId: {memberId}");
@ -74,7 +80,8 @@ userIntroTimer.OnOccurence += async (s, ea) =>
bool needsSaving = false; bool needsSaving = false;
foreach (var user in introUsers) foreach (var user in introUsers)
{ {
await SignalIntegration.Instance.IntroduceUserAsync(user); var signalIntegration = app.Services.GetService<SignalIntegration>();
await signalIntegration.IntroduceUserAsync(user);
user.IsIntroduced = true; user.IsIntroduced = true;
needsSaving = true; needsSaving = true;
} }
@ -137,12 +144,14 @@ pickOfTheDayTimer.OnOccurence += async (s, ea) =>
if (luckyUser.SignalMemberId is string signalId) if (luckyUser.SignalMemberId is string signalId)
{ {
await dci.SongSuggestions.AddAsync(newSongSuggestion); await dci.SongSuggestions.AddAsync(newSongSuggestion);
luckyUser.WasChosenForSuggestionThisRound = true;
await dci.SaveChangesAsync(); await dci.SaveChangesAsync();
await SignalIntegration.Instance.SendMessageToGroupAsync($"Today's chosen person to share a song is: **{userName}**"); var signalIntegration = app.Services.GetService<SignalIntegration>();
await SignalIntegration.Instance.SendMessageToGroupAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*"); await signalIntegration.SendMessageToGroupAsync($"Today's chosen person to share a song is: **{userName}**");
await SignalIntegration.Instance.SendMessageToUserAsync($"Congratulations, you have been chosen to share a song today!", signalId); await signalIntegration.SendMessageToGroupAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*");
await SignalIntegration.Instance.SendMessageToUserAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*", signalId); await signalIntegration.SendMessageToUserAsync($"Congratulations, you have been chosen to share a song today!", signalId);
await 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); await signalIntegration.SendMessageToUserAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*", signalId);
await signalIntegration.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);
} }
await dci.DisposeAsync(); await dci.DisposeAsync();
}; };
@ -151,10 +160,11 @@ var startUserAssociationProcess = async (User userToAssociate) =>
{ {
if (userToAssociate.SignalMemberId is string signalId) if (userToAssociate.SignalMemberId is string signalId)
{ {
await SignalIntegration.Instance.SendMessageToUserAsync($"Hi, I see you are not associated with any website user yet.", signalId); var signalIntegration = app.Services.GetService<SignalIntegration>();
await SignalIntegration.Instance.SendMessageToUserAsync($"If you haven't yet, please navigate to https://users.disi.dev to create a new account.", signalId); await signalIntegration.SendMessageToUserAsync($"Hi, I see you are not associated with any website user yet.", signalId);
await SignalIntegration.Instance.SendMessageToUserAsync($"Once you have done so, go to https://sotd.disi.dev, login, navigate to \"Unclaimed Phone Numbers\" and click on the \"Claim\" button to start the claim process.", signalId); await signalIntegration.SendMessageToUserAsync($"If you haven't yet, please navigate to https://users.disi.dev to create a new account.", signalId);
await SignalIntegration.Instance.SendMessageToUserAsync($"With a future update you will be required to submit songs via your user account - at that point you will be skipped during the selection process if you have not yet claimed your phone number!", signalId); await signalIntegration.SendMessageToUserAsync($"Once you have done so, go to https://sotd.disi.dev, login, navigate to \"Unclaimed Phone Numbers\" and click on the \"Claim\" button to start the claim process.", signalId);
await signalIntegration.SendMessageToUserAsync($"With a future update you will be required to submit songs via your user account - at that point you will be skipped during the selection process if you have not yet claimed your phone number!", signalId);
} }
}; };
@ -187,30 +197,20 @@ ldapAssociationTimer.OnOccurence += async (s, ea) =>
await dci.DisposeAsync(); await dci.DisposeAsync();
}; };
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddOpenApi();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Auth/Login";
});
builder.Services.AddSingleton<LdapAuthenticationService>();
builder.Services.AddSingleton<PhoneClaimCodeProviderService>();
builder.Services.AddSingleton<SongResolver>();
var app = builder.Build();
// only start interaction timers in production builds // only start interaction timers in production builds
// for local/development testing we want those disabled // for local/development testing we want those disabled
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {
logger.LogTrace("Starting timer for scheduled processes.");
userCheckTimer.Start(); userCheckTimer.Start();
userIntroTimer.Start(); userIntroTimer.Start();
pickOfTheDayTimer.Start(); pickOfTheDayTimer.Start();
ldapAssociationTimer.Start(); ldapAssociationTimer.Start();
} }
else
{
logger.LogTrace("This is a debug build - scheduled processes are disabled.");
}
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())

View File

@ -4,17 +4,17 @@ public class SongResolver
{ {
private readonly IEnumerable<ISongValidator> _songValidators; private readonly IEnumerable<ISongValidator> _songValidators;
private readonly ILogger logger; private readonly ILogger<SongResolver> logger;
public SongResolver() public SongResolver(ILogger<SongResolver> logger)
{ {
using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Information)); this.logger = logger;
this.logger = factory.CreateLogger("SongResolver");
this._songValidators = new List<ISongValidator>(); this._songValidators = new List<ISongValidator>();
foreach (Type mytype in System.Reflection.Assembly.GetExecutingAssembly().GetTypes() foreach (Type mytype in System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
.Where(mytype => mytype.GetInterfaces().Contains(typeof(ISongValidator)) && !(mytype.Name.EndsWith("Base")))) { .Where(mytype => mytype.GetInterfaces().Contains(typeof(ISongValidator)) && !(mytype.Name.EndsWith("Base"))))
{
if (Activator.CreateInstance(mytype) is ISongValidator validator) if (Activator.CreateInstance(mytype) is ISongValidator validator)
{ {
logger.LogDebug("Registering song validator: {ValidatorType}", mytype.Name); logger.LogDebug("Registering song validator: {ValidatorType}", mytype.Name);
@ -32,7 +32,8 @@ public class SongResolver
if (!await validator.CanExtractSongMetadataAsync(songUri)) if (!await validator.CanExtractSongMetadataAsync(songUri))
{ {
this.logger.LogWarning("Cannot extract metadata for song URI: {SongUri}", songUri); this.logger.LogWarning("Cannot extract metadata for song URI: {SongUri}", songUri);
return new Song { return new Song
{
Artist = "Unknown Artist", Artist = "Unknown Artist",
Name = "Unknown Title", Name = "Unknown Title",
Url = songUri.ToString(), Url = songUri.ToString(),
@ -43,7 +44,8 @@ public class SongResolver
} }
} }
return new Song { return new Song
{
Artist = "Unknown Artist", Artist = "Unknown Artist",
Name = "Unknown Title", Name = "Unknown Title",
Url = songUri.ToString(), Url = songUri.ToString(),

View File

@ -14,9 +14,9 @@ public class SpotifyValidator : UriBasedSongValidatorBase
spotifyApiClient = new SpotifyApiClient(); spotifyApiClient = new SpotifyApiClient();
} }
public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri) public override Task<bool> CanExtractSongMetadataAsync(Uri songUri)
{ {
return this.CanValidateUri(songUri); return Task.FromResult(this.CanValidateUri(songUri));
} }
public override async Task<Song> ValidateAsync(Uri songUri) public override async Task<Song> ValidateAsync(Uri songUri)

View File

@ -5,9 +5,9 @@ public class YoutubeMusicValidator : UriBasedSongValidatorBase
{ {
public override string UriValidatorRegex => @"^(https?://)?(music\.youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})"; public override string UriValidatorRegex => @"^(https?://)?(music\.youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})";
public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri) public override Task<bool> CanExtractSongMetadataAsync(Uri songUri)
{ {
return this.CanValidateUri(songUri); return Task.FromResult(this.CanValidateUri(songUri));
} }
public override async Task<Song> ValidateAsync(Uri songUri) public override async Task<Song> ValidateAsync(Uri songUri)
@ -15,12 +15,12 @@ public class YoutubeMusicValidator : UriBasedSongValidatorBase
var title = string.Empty; var title = string.Empty;
var artist = string.Empty; var artist = string.Empty;
using(HttpClient httpClient = new HttpClient()) using (HttpClient httpClient = new HttpClient())
{ {
var response = await httpClient.GetAsync(songUri); var response = await httpClient.GetAsync(songUri);
var config = Configuration.Default.WithDefaultLoader(); var config = Configuration.Default.WithDefaultLoader();
var context = BrowsingContext.New(config); var context = BrowsingContext.New(config);
using(var document = await context.OpenAsync(async req => req.Content(await response.Content.ReadAsStringAsync()))) using (var document = await context.OpenAsync(async req => req.Content(await response.Content.ReadAsStringAsync())))
{ {
// document.getElementsByTagName("ytmusic-player-queue-item")[0].getElementsByClassName("song-title")[0].innerHTML // document.getElementsByTagName("ytmusic-player-queue-item")[0].getElementsByClassName("song-title")[0].innerHTML
title = document.QuerySelector(".ytmusic-player-queue-item")?.QuerySelector(".song-title")?.InnerHtml; title = document.QuerySelector(".ytmusic-player-queue-item")?.QuerySelector(".song-title")?.InnerHtml;
@ -29,6 +29,8 @@ public class YoutubeMusicValidator : UriBasedSongValidatorBase
} }
} }
#pragma warning disable CS8604 // Possible null reference argument.
#pragma warning disable CS8604 // Possible null reference argument.
var song = new Song var song = new Song
{ {
Name = title, Name = title,
@ -37,6 +39,8 @@ public class YoutubeMusicValidator : UriBasedSongValidatorBase
Provider = SongProvider.YouTube, Provider = SongProvider.YouTube,
SpotifyId = this.LookupSpotifyId(title, artist) SpotifyId = this.LookupSpotifyId(title, artist)
}; };
#pragma warning restore CS8604 // Possible null reference argument.
#pragma warning restore CS8604 // Possible null reference argument.
return song; return song;
} }

View File

@ -8,14 +8,16 @@ public class YoutubeValidator : UriBasedSongValidatorBase
public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri) public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
{ {
using(HttpClient httpClient = new HttpClient()) using (HttpClient httpClient = new HttpClient())
{ {
var response = await httpClient.GetAsync(songUri); var response = await httpClient.GetAsync(songUri);
var config = Configuration.Default.WithDefaultLoader(); var config = Configuration.Default.WithDefaultLoader();
var context = BrowsingContext.New(config); var context = BrowsingContext.New(config);
using(var document = await context.OpenAsync(async req => req.Content(await response.Content.ReadAsStringAsync()))) using (var document = await context.OpenAsync(async req => req.Content(await response.Content.ReadAsStringAsync())))
{ {
#pragma warning disable CS8602 // Dereference of a possibly null reference.
var documentContents = (document.ChildNodes[1] as HtmlElement).InnerHtml; var documentContents = (document.ChildNodes[1] as HtmlElement).InnerHtml;
#pragma warning restore CS8602 // Dereference of a possibly null reference.
var titleElement = document.QuerySelectorAll(".yt-video-attribute-view-model__title")[0]; var titleElement = document.QuerySelectorAll(".yt-video-attribute-view-model__title")[0];
var artistParentElement = document.QuerySelectorAll(".yt-video-attribute-view-model__secondary-subtitle")[0]; var artistParentElement = document.QuerySelectorAll(".yt-video-attribute-view-model__secondary-subtitle")[0];
@ -29,18 +31,20 @@ public class YoutubeValidator : UriBasedSongValidatorBase
var title = string.Empty; var title = string.Empty;
var artist = string.Empty; var artist = string.Empty;
using(HttpClient httpClient = new HttpClient()) using (HttpClient httpClient = new HttpClient())
{ {
var response = await httpClient.GetAsync(songUri); var response = await httpClient.GetAsync(songUri);
var config = Configuration.Default.WithDefaultLoader(); var config = Configuration.Default.WithDefaultLoader();
var context = BrowsingContext.New(config); var context = BrowsingContext.New(config);
using(var document = await context.OpenAsync(async req => req.Content(await response.Content.ReadAsStringAsync()))) using (var document = await context.OpenAsync(async req => req.Content(await response.Content.ReadAsStringAsync())))
{ {
title = document.QuerySelectorAll(".yt-video-attribute-view-model__title")[0]?.InnerHtml; title = document.QuerySelectorAll(".yt-video-attribute-view-model__title")[0]?.InnerHtml;
artist = document.QuerySelectorAll(".yt-video-attribute-view-model__secondary-subtitle")[0]?.Children[0]?.InnerHtml; artist = document.QuerySelectorAll(".yt-video-attribute-view-model__secondary-subtitle")[0]?.Children[0]?.InnerHtml;
} }
} }
#pragma warning disable CS8604 // Possible null reference argument.
#pragma warning disable CS8604 // Possible null reference argument.
var song = new Song var song = new Song
{ {
Name = title, Name = title,
@ -49,6 +53,8 @@ public class YoutubeValidator : UriBasedSongValidatorBase
Provider = SongProvider.YouTube, Provider = SongProvider.YouTube,
SpotifyId = this.LookupSpotifyId(title, artist) SpotifyId = this.LookupSpotifyId(title, artist)
}; };
#pragma warning restore CS8604 // Possible null reference argument.
#pragma warning restore CS8604 // Possible null reference argument.
return song; return song;
} }

View File

@ -2,8 +2,11 @@
"DetailedErrors": true, "DetailedErrors": true,
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Trace",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning",
"SongResolver": "Trace",
"SignalIntegration": "Trace",
"SongOfTheDay": "Trace"
} }
} }
} }

View File

@ -1,8 +1,11 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Trace",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning",
"SongResolver": "Information",
"SignalIntegration": "Information",
"SongOfTheDay": "Trace"
} }
}, },
"AllowedHosts": "*" "AllowedHosts": "*"