Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
23afbc1009 | |||
220f4d7ffd | |||
0d2ec3712e | |||
![]() |
dbd83ebb6a | ||
![]() |
010316aa70 | ||
![]() |
d416242712 | ||
e9a824c6ef | |||
e0f206bc0c | |||
183309e1ed | |||
a6321324f7 |
@ -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
|
||||||
|
49
HISTORY.md
49
HISTORY.md
@ -4,6 +4,55 @@ Changelog
|
|||||||
|
|
||||||
(unreleased)
|
(unreleased)
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
Fix
|
||||||
|
~~~
|
||||||
|
- Some cleanup and fixing runtime bugs, refs NOISSUE. [Simon
|
||||||
|
Diesenreiter]
|
||||||
|
|
||||||
|
Other
|
||||||
|
~~~~~
|
||||||
|
- Feat: implement song submission support, refs #5. [Simon Diesenreiter]
|
||||||
|
- Feat: basic initial implementation of spotify client link validator
|
||||||
|
and song submission form refs: NOISSUE. [Simon Diesenreiter]
|
||||||
|
|
||||||
|
|
||||||
|
0.3.3 (2025-05-26)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Fix
|
||||||
|
~~~
|
||||||
|
- Save DateTime as UTC, refs NOISSUE. [Simon Diesenreiter]
|
||||||
|
|
||||||
|
Other
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
|
||||||
|
0.3.2 (2025-05-25)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Fix
|
||||||
|
~~~
|
||||||
|
- Exception thrown on LastOrDefault(), refs NOISSUE. [Simon
|
||||||
|
Diesenreiter]
|
||||||
|
|
||||||
|
Other
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
|
||||||
|
0.3.1 (2025-05-24)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Fix
|
||||||
|
~~~
|
||||||
|
- Fix build errors, refs NOISSUE. [Simon Diesenreiter]
|
||||||
|
|
||||||
|
Other
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
|
||||||
|
0.3.0 (2025-05-24)
|
||||||
|
------------------
|
||||||
- Ci: more CI fixes, refs NOISSUE. [Simon Diesenreiter]
|
- Ci: more CI fixes, refs NOISSUE. [Simon Diesenreiter]
|
||||||
- Ci: improve commit message generation script, refs NOISSUE. [Simon
|
- Ci: improve commit message generation script, refs NOISSUE. [Simon
|
||||||
Diesenreiter]
|
Diesenreiter]
|
||||||
|
2
Makefile
2
Makefile
@ -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:
|
||||||
|
6
song_of_the_day/.editorconfig
Normal file
6
song_of_the_day/.editorconfig
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[*.cs]
|
||||||
|
|
||||||
|
# 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.CS8602.severity = none
|
||||||
|
dotnet_diagnostic.CS8604.severity = none
|
46
song_of_the_day/.vscode/launch.json
vendored
Normal file
46
song_of_the_day/.vscode/launch.json
vendored
Normal 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
47
song_of_the_day/.vscode/tasks.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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.TestLogin(username, password);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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();
|
||||||
@ -18,7 +20,7 @@ public class PhoneClaimCodeProviderService
|
|||||||
.Select(s => s[random.Next(s.Length)]).ToArray());
|
.Select(s => s[random.Next(s.Length)]).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void GenerateClaimCodeForUserAndNumber(string username, string phoneNumber)
|
public async void GenerateClaimCodeForUserAndNumber(string username, string phoneNumber)
|
||||||
{
|
{
|
||||||
var generatedCode = string.Empty;
|
var generatedCode = string.Empty;
|
||||||
if (IsCodeGeneratedForUser(username))
|
if (IsCodeGeneratedForUser(username))
|
||||||
@ -32,7 +34,10 @@ public class PhoneClaimCodeProviderService
|
|||||||
_phoneClaimNumbers[username] = phoneNumber;
|
_phoneClaimNumbers[username] = phoneNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
@ -20,6 +20,8 @@ public class AppConfiguration
|
|||||||
var managersGroupName = Environment.GetEnvironmentVariable("LDAP_ADMINGROUP") ?? "admins";
|
var managersGroupName = Environment.GetEnvironmentVariable("LDAP_ADMINGROUP") ?? "admins";
|
||||||
var userGroupName = Environment.GetEnvironmentVariable("LDAP_USERGROUP") ?? "everybody";
|
var userGroupName = Environment.GetEnvironmentVariable("LDAP_USERGROUP") ?? "everybody";
|
||||||
var bindValue = Environment.GetEnvironmentVariable("LDAP_BIND");
|
var bindValue = Environment.GetEnvironmentVariable("LDAP_BIND");
|
||||||
|
this.SpotifyClientId = Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_ID") ?? "0c59b625470b4ad1b70743e0254d17fd";
|
||||||
|
this.SpotifyClientSecret = Environment.GetEnvironmentVariable("SPOTIFY_CLIENT_SECRET") ?? "04daaebd42fc47909c5cbd1f5cf23555";
|
||||||
this.LDAPConfig = new ConfigurationAD()
|
this.LDAPConfig = new ConfigurationAD()
|
||||||
{
|
{
|
||||||
Username = Environment.GetEnvironmentVariable("LDAP_BIND") ?? "cn=admin,dc=disi,dc=dev",
|
Username = Environment.GetEnvironmentVariable("LDAP_BIND") ?? "cn=admin,dc=disi,dc=dev",
|
||||||
@ -83,6 +85,16 @@ public class AppConfiguration
|
|||||||
get; private set;
|
get; private set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string SpotifyClientId
|
||||||
|
{
|
||||||
|
get; private set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SpotifyClientSecret
|
||||||
|
{
|
||||||
|
get; private set;
|
||||||
|
}
|
||||||
|
|
||||||
public bool UseBotTag
|
public bool UseBotTag
|
||||||
{
|
{
|
||||||
get; private set;
|
get; private set;
|
||||||
|
@ -10,7 +10,7 @@ public class AuthController : Controller
|
|||||||
public async Task<IActionResult> Login(string username, string password)
|
public async Task<IActionResult> Login(string username, string password)
|
||||||
{
|
{
|
||||||
var ldapService = HttpContext.RequestServices.GetService<LdapAuthenticationService>();
|
var ldapService = HttpContext.RequestServices.GetService<LdapAuthenticationService>();
|
||||||
if (ldapService.Authenticate(username, password))
|
if (ldapService != null && ldapService.Authenticate(username, password))
|
||||||
{
|
{
|
||||||
var claims = new[] { new Claim(ClaimTypes.Name, username) };
|
var claims = new[] { new Claim(ClaimTypes.Name, username) };
|
||||||
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
170
song_of_the_day/Data/Migrations/20250601144913_some more model updates.Designer.cs
generated
Normal file
170
song_of_the_day/Data/Migrations/20250601144913_some more model updates.Designer.cs
generated
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
// <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("20250601144913_some more model updates")]
|
||||||
|
partial class somemoremodelupdates
|
||||||
|
{
|
||||||
|
/// <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")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int?>("Provider")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("SpotifyId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.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<bool>("HasUsedSuggestion")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int?>("SongId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("SuggestionHelperId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("UserHasSubmitted")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int?>("UserId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SongId");
|
||||||
|
|
||||||
|
b.HasIndex("SuggestionHelperId");
|
||||||
|
|
||||||
|
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")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.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>("AssociationInProgress")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsIntroduced")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("LdapUserName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("NickName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("SignalMemberId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("WasChosenForSuggestionThisRound")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.HasKey("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SongSuggestion", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Song", "Song")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SongId");
|
||||||
|
|
||||||
|
b.HasOne("SuggestionHelper", "SuggestionHelper")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SuggestionHelperId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId");
|
||||||
|
|
||||||
|
b.Navigation("Song");
|
||||||
|
|
||||||
|
b.Navigation("SuggestionHelper");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace song_of_the_day.DataMigrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class somemoremodelupdates : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Provider",
|
||||||
|
table: "Songs",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "SpotifyId",
|
||||||
|
table: "Songs",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Provider",
|
||||||
|
table: "Songs");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SpotifyId",
|
||||||
|
table: "Songs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -35,6 +35,12 @@ namespace song_of_the_day.DataMigrations
|
|||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int?>("Provider")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("SpotifyId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("Url")
|
b.Property<string>("Url")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
@ -6,4 +6,6 @@ public class Song
|
|||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public string? Artist { get; set; }
|
public string? Artist { get; set; }
|
||||||
public string? Url { get; set; }
|
public string? Url { get; set; }
|
||||||
|
public SongProvider? Provider { get; set; }
|
||||||
|
public string? SpotifyId { get; set; }
|
||||||
}
|
}
|
11
song_of_the_day/Data/SongProvider.cs
Normal file
11
song_of_the_day/Data/SongProvider.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
public enum SongProvider
|
||||||
|
{
|
||||||
|
Spotify,
|
||||||
|
YouTube,
|
||||||
|
YoutubeMusic,
|
||||||
|
SoundCloud,
|
||||||
|
Bandcamp,
|
||||||
|
PlainHttp,
|
||||||
|
NavidromeSharedLink,
|
||||||
|
Other
|
||||||
|
}
|
13
song_of_the_day/GlobalSuppressions.cs
Normal file
13
song_of_the_day/GlobalSuppressions.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// This file is used by Code Analysis to maintain SuppressMessage
|
||||||
|
// attributes that are applied to this project.
|
||||||
|
// Project-level suppressions either have no target or are given
|
||||||
|
// a specific target and scoped to a namespace, type, member, etc.
|
||||||
|
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>", Scope = "member", Target = "~P:sotd.Pages.UnclaimedPhoneNumbersModel.userId")]
|
||||||
|
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>", Scope = "member", Target = "~P:sotd.Pages.UserModel.userId")]
|
||||||
|
[assembly: SuppressMessage("Compiler", "CS8981:The type name only contains lower-cased ascii characters. Such names may become reserved for the language.", Justification = "<Pending>", Scope = "type", Target = "~T:song_of_the_day.DataMigrations.additionaldataforsongsubmissions")]
|
||||||
|
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>", Scope = "type", Target = "~T:song_of_the_day.DataMigrations.additionaldataforsongsubmissions")]
|
||||||
|
[assembly: SuppressMessage("Compiler", "CS8981:The type name only contains lower-cased ascii characters. Such names may become reserved for the language.", Justification = "<Pending>", Scope = "type", Target = "~T:song_of_the_day.DataMigrations.keeptrackofusersoickedforsubmission")]
|
||||||
|
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "<Pending>", Scope = "type", Target = "~T:song_of_the_day.DataMigrations.keeptrackofusersoickedforsubmission")]
|
@ -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
|
||||||
|
@ -1,40 +1,61 @@
|
|||||||
|
|
||||||
using System.Collections;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using song_of_the_day;
|
using song_of_the_day;
|
||||||
|
|
||||||
|
public class LinkPreviewAttachment
|
||||||
|
{
|
||||||
|
public string Url { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string Base64Image { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class SignalIntegration
|
public class SignalIntegration
|
||||||
{
|
{
|
||||||
public static SignalIntegration? Instance;
|
private readonly ILogger<SignalIntegration> logger;
|
||||||
|
|
||||||
public SignalIntegration(string uri, int port, string phoneNumber)
|
public SignalIntegration(ILogger<SignalIntegration> logger)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
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),
|
||||||
|
Timeout = TimeSpan.FromSeconds(180),
|
||||||
};
|
};
|
||||||
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;
|
||||||
|
|
||||||
|
public async Task GetMessagesAsync()
|
||||||
|
{
|
||||||
|
var messages = await apiClient.ReceiveAsync(this.phoneNumber, "120", "true", "true", "50", "false");
|
||||||
|
logger.LogInformation($"Received {messages.Count} Signal messages.");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ListGroupsAsync()
|
public async Task ListGroupsAsync()
|
||||||
{
|
{
|
||||||
|
logger.LogDebug("Listing all groups for phone number: {PhoneNumber}", this.phoneNumber);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ICollection<song_of_the_day.GroupEntry> groupEntries = await apiClient.GroupsAllAsync(this.phoneNumber);
|
ICollection<song_of_the_day.GroupEntry> groupEntries = await apiClient.GroupsAllAsync(this.phoneNumber);
|
||||||
foreach (var group in groupEntries)
|
foreach (var group in groupEntries)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"{group.Name} {group.Id}");
|
logger.LogDebug($" {group.Name} {group.Id}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Exception (ListGroupsAsync): " + ex.Message);
|
logger.LogError("Exception (ListGroupsAsync): " + ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,14 +66,37 @@ public class SignalIntegration
|
|||||||
SendMessageV2 data = new SendMessageV2();
|
SendMessageV2 data = new SendMessageV2();
|
||||||
data.Recipients = new List<string>();
|
data.Recipients = new List<string>();
|
||||||
data.Recipients.Add(AppConfiguration.Instance.SignalGroupId);
|
data.Recipients.Add(AppConfiguration.Instance.SignalGroupId);
|
||||||
data.Message = message;
|
data.Message = (AppConfiguration.Instance.UseBotTag ? "**[Proggy]**\n" : "") + message;
|
||||||
data.Text_mode = SendMessageV2Text_mode.Styled;
|
data.Text_mode = SendMessageV2Text_mode.Styled;
|
||||||
data.Number = AppConfiguration.Instance.HostPhoneNumber;
|
data.Number = AppConfiguration.Instance.HostPhoneNumber;
|
||||||
var response = await apiClient.Send2Async(data);
|
var response = await apiClient.Send2Async(data);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Exception (SendMessageToGroupAsync): " + ex.Message);
|
logger.LogError("Exception (SendMessageToGroupAsync): " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendMessageToGroupAsync(string message, LinkPreviewAttachment previewData)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SendMessageV2 data = new SendMessageV2();
|
||||||
|
data.Recipients = new List<string>();
|
||||||
|
data.Recipients.Add(AppConfiguration.Instance.SignalGroupId);
|
||||||
|
data.Message = (AppConfiguration.Instance.UseBotTag ? "**[Proggy]**\n" : "") + message;
|
||||||
|
data.Text_mode = SendMessageV2Text_mode.Styled;
|
||||||
|
data.Number = AppConfiguration.Instance.HostPhoneNumber;
|
||||||
|
data.Link_preview = new LinkPreviewType();
|
||||||
|
data.Link_preview.Url = previewData.Url;
|
||||||
|
data.Link_preview.Title = previewData.Title;
|
||||||
|
data.Link_preview.Description = previewData.Description;
|
||||||
|
data.Link_preview.Base64_thumbnail = previewData.Base64Image;
|
||||||
|
var response = await apiClient.Send2Async(data);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError("Exception (SendMessageToGroupAsync): " + ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,12 +114,40 @@ public class SignalIntegration
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Exception (SendMessageToUserAsync): " + ex.Message);
|
logger.LogError("Exception (SendMessageToUserAsync): " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendMessageToUserAsync(string message, LinkPreviewAttachment previewData, 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;
|
||||||
|
data.Link_preview = new LinkPreviewType();
|
||||||
|
data.Link_preview.Url = previewData.Url;
|
||||||
|
data.Link_preview.Title = previewData.Title;
|
||||||
|
data.Link_preview.Description = previewData.Description;
|
||||||
|
data.Link_preview.Base64_thumbnail = previewData.Base64Image;
|
||||||
|
var response = await apiClient.Send2Async(data);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError("Exception (SendMessageToUserAsync): " + ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
@ -97,7 +169,7 @@ public class SignalIntegration
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Exception (GetMemberListAsync): " + ex.Message);
|
logger.LogError("Exception (GetMemberListAsync): " + ex.Message);
|
||||||
}
|
}
|
||||||
return new List<string>();
|
return new List<string>();
|
||||||
}
|
}
|
||||||
@ -117,7 +189,7 @@ public class SignalIntegration
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Exception (GetContactAsync): " + ex.Message);
|
logger.LogError("Exception (GetContactAsync): " + ex.Message);
|
||||||
return new ListContactsResponse();
|
return new ListContactsResponse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<h1 class="display-4">Welcome</h1>
|
<h1 class="display-4">Submission History</h1>
|
||||||
<p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Song</th>
|
||||||
|
<th>Submitter</th>
|
||||||
|
<th>Details</th>
|
||||||
|
</tr>
|
||||||
|
@foreach(var songSuggestion in Model.SongSuggestions)
|
||||||
|
{
|
||||||
|
@if(songSuggestion != null && songSuggestion.Song != null && songSuggestion.User != null && songSuggestion.UserHasSubmitted)
|
||||||
|
{
|
||||||
|
var displayName = string.IsNullOrEmpty(songSuggestion?.User?.NickName)
|
||||||
|
? songSuggestion?.User.Name
|
||||||
|
: songSuggestion.User.NickName;
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>@songSuggestion?.Date.ToString("dd. MM. yyyy")</td>
|
||||||
|
<td><a href="@songSuggestion?.Song.Url" target="_blank">@string.Format("{0} - {1}", songSuggestion?.Song.Name, songSuggestion?.Song.Artist)</a></td>
|
||||||
|
<td>@displayName</td>
|
||||||
|
<td><a href="/SongSubmission/@songSuggestion?.Id">View</a></td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace sotd.Pages;
|
namespace sotd.Pages;
|
||||||
|
|
||||||
@ -12,8 +14,16 @@ public class IndexModel : PageModel
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnGet()
|
[BindProperty]
|
||||||
{
|
public List<SongSuggestion> SongSuggestions { get; set; } = new List<SongSuggestion>();
|
||||||
|
|
||||||
|
public async Task OnGet()
|
||||||
|
{
|
||||||
|
using var dci = DataContext.Instance;
|
||||||
|
this.SongSuggestions = dci.SongSuggestions.OrderByDescending(s => s.Date)
|
||||||
|
.Take(50)
|
||||||
|
.Include(s => s.Song)
|
||||||
|
.Include(s => s.User)
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
16
song_of_the_day/Pages/Shared/SongPartialModel.cs
Normal file
16
song_of_the_day/Pages/Shared/SongPartialModel.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
public class SongPartialModel
|
||||||
|
{
|
||||||
|
public Song InnerSong { get; set; }
|
||||||
|
|
||||||
|
public string? Artist => InnerSong.Artist;
|
||||||
|
|
||||||
|
public string? Name => InnerSong.Name;
|
||||||
|
|
||||||
|
public string? SpotifyId => InnerSong.SpotifyId;
|
||||||
|
|
||||||
|
public string? Url => InnerSong.Url;
|
||||||
|
|
||||||
|
public SongProvider? Provider => InnerSong.Provider;
|
||||||
|
|
||||||
|
public bool IsPageReadonly { get; set; }
|
||||||
|
}
|
2
song_of_the_day/Pages/Shared/UpdateInputText.razor
Normal file
2
song_of_the_day/Pages/Shared/UpdateInputText.razor
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@inherits Microsoft.AspNetCore.Components.Forms.InputText
|
||||||
|
<input @attributes="@AdditionalAttributes" class="@CssClass" @bind="@CurrentValueAsString" @bind:event="oninput" />
|
@ -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,12 +37,20 @@
|
|||||||
<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 != null && this.User.Identity != null &&
|
||||||
|
this.User.Identity.IsAuthenticated && DoesUserHaveClaimedPhoneNumber())
|
||||||
|
{
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-dark" href="/SongSubmission/">Song Submissions</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,4 +45,4 @@ button.accept-policy {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
line-height: 60px;
|
line-height: 60px;
|
||||||
}
|
}
|
@ -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>
|
||||||
|
8
song_of_the_day/Pages/Shared/_SongPartial.cshtml
Normal file
8
song_of_the_day/Pages/Shared/_SongPartial.cshtml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
@model SongPartialModel
|
||||||
|
<label asp-for="Name">Name:</label>
|
||||||
|
<input asp-for="Name" oninput="UpdateSongSuggestions()" disabled="@Model.IsPageReadonly" />
|
||||||
|
<label asp-for="Artist">Artist:</label>
|
||||||
|
<input asp-for="Artist" oninput="UpdateSongSuggestions()" disabled="@Model.IsPageReadonly" />
|
||||||
|
<label asp-for="SpotifyId">Spotify ID:</label>
|
||||||
|
<input asp-for="SpotifyId" readonly="@Model.Provider.Equals(SongProvider.Spotify)" disabled="@Model.IsPageReadonly" />
|
||||||
|
<input asp-for="Provider" hidden />
|
@ -0,0 +1,38 @@
|
|||||||
|
@model List<SpotifyAPI.Web.FullTrack>
|
||||||
|
<div class="spotifySongSelector">
|
||||||
|
@foreach(var track in Model)
|
||||||
|
{
|
||||||
|
<div class="songSelectorButton" onclick="clickHandler(this)">
|
||||||
|
<div class="songName">@track.Name</div>
|
||||||
|
<div class="artist">@track.Artists[0].Name</div>
|
||||||
|
<div class="album">@track.Album.Name</div>
|
||||||
|
<div class="id" hidden>@track.Id</div>
|
||||||
|
<div class="albumCover">
|
||||||
|
<img src="@track.Album.Images[0].Url" width="50px" height="50px"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function clickHandler(t) {
|
||||||
|
// remove previous selection
|
||||||
|
$('.songSelectorButton').removeClass("selected");
|
||||||
|
t.classList.add("selected");
|
||||||
|
var idElement = t.getElementsByClassName("id")[0]
|
||||||
|
console.log(idElement);
|
||||||
|
SetSpotifyId(idElement.textContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeSelection() {
|
||||||
|
var currentSpotifyId = GetSpotifyId();
|
||||||
|
$('.songSelectorButton').each(function(i, e) {
|
||||||
|
if(e.getElementsByClassName("id")[0].textContent == currentSpotifyId)
|
||||||
|
{
|
||||||
|
e.classList.add("selected");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeSelection();
|
||||||
|
</script>
|
@ -0,0 +1,16 @@
|
|||||||
|
.songSelectorButton {
|
||||||
|
background-color: #e3e6e7;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songSelectorButton:nth-child(2n) {
|
||||||
|
background-color: #cacbce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songSelectorButton.selected {
|
||||||
|
background-color: #a0c3e5;
|
||||||
|
border-radius: 2px;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: #285786;
|
||||||
|
}
|
158
song_of_the_day/Pages/SongSubmission.cshtml
Normal file
158
song_of_the_day/Pages/SongSubmission.cshtml
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
@page "{submissionIndex:int?}"
|
||||||
|
@model SongSubmissionModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = this.Model.SubmissionIndex == null ? "Song Submissions" : "New Song Submission";
|
||||||
|
}
|
||||||
|
|
||||||
|
@if(Model.SubmissionIndex == null)
|
||||||
|
{
|
||||||
|
<div class="text-left">
|
||||||
|
<h1>Your Submission History:</h1>
|
||||||
|
<table id="submissionTable">
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Suggestion</th>
|
||||||
|
<th>Song</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
@foreach(var submission in Model.UserSongSubmissions)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="submissionDate">
|
||||||
|
@submission.Date.ToString("dd. MM. yyyy")
|
||||||
|
</td>
|
||||||
|
<td class="submissionSuggestion">
|
||||||
|
@if(submission.Song == null || submission.HasUsedSuggestion)
|
||||||
|
{
|
||||||
|
@submission.SuggestionHelper.Title
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="submissionSong">
|
||||||
|
@if(submission.Song != null)
|
||||||
|
{
|
||||||
|
@string.Format("{0} - {1}", submission.Song.Name, submission.Song.Artist);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div style="font-style: italic;">No submission yet!</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="=viewLink">
|
||||||
|
@{
|
||||||
|
var buttonClass = submission.Song == null ? "submitButton" : "viewButton";
|
||||||
|
var buttonText = submission.Song == null ? "Submit Song" : "View";
|
||||||
|
}
|
||||||
|
<button class=@buttonClass onclick="redirectTo(@submission.Id)">@buttonText</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="text-left">
|
||||||
|
<div class="suggestionHelper">
|
||||||
|
<div class="title">Today's suggestionHelper is: <b>@Model.SuggestionHelper.Title</b></div>
|
||||||
|
<div class="description" style="font-style: italic;">@Model.SuggestionHelper.Description</div>
|
||||||
|
</div>
|
||||||
|
<form method="post">
|
||||||
|
<label asp-for="SubmitUrl" >Song Url:</label>
|
||||||
|
<input asp-for="SubmitUrl" oninput="Update(this)" disabled="@Model.IsPageReadonly" />
|
||||||
|
<label asp-for="HasUsedSuggestion">Submission has used suggestion.</label>
|
||||||
|
<input asp-for="HasUsedSuggestion" checked disabled="@Model.IsPageReadonly" />
|
||||||
|
@{
|
||||||
|
var songPartialModel = new SongPartialModel() {
|
||||||
|
InnerSong = Model.SongData,
|
||||||
|
IsPageReadonly = Model.IsPageReadonly
|
||||||
|
};
|
||||||
|
}
|
||||||
|
<div id="songdata">
|
||||||
|
<partial name="_SongPartial" model="@songPartialModel" />
|
||||||
|
</div>
|
||||||
|
@if(!Model.IsPageReadonly)
|
||||||
|
{
|
||||||
|
<div id="suggestionPicker">
|
||||||
|
<partial name="_SpotifySongSuggestionsPartial" model="@Model.SpotifySuggestions" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<input type="submit" id="songsubmit" title="Submit" value="Submit" disabled="@Model.CanSubmit" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@if(Model.SubmissionIndex == null)
|
||||||
|
{
|
||||||
|
// inject relevant scripts
|
||||||
|
<script>
|
||||||
|
function redirectTo(id) {
|
||||||
|
window.location.href = '/SongSubmission/' + id;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<script>
|
||||||
|
var skipUpdate = false;
|
||||||
|
|
||||||
|
function ReevaluateSubmit(callback) {
|
||||||
|
var canSubmit = $('#songdata #Name').val().length > 0 && $('#songdata #Artist').val().length > 0;
|
||||||
|
if(canSubmit)
|
||||||
|
{
|
||||||
|
$('#songsubmit').removeAttr("disabled");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$('#songsubmit').attr("disabled", "disabled");
|
||||||
|
}
|
||||||
|
if(callback)
|
||||||
|
{
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update(t) {
|
||||||
|
var songDataUrl = '?handler=Update&&SubmitUrl=' + $(t).val();
|
||||||
|
$('#songdata').load(songDataUrl, null, function() {
|
||||||
|
ReevaluateSubmit(function() {
|
||||||
|
UpdateSongSuggestions(t);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateSongSuggestions() {
|
||||||
|
if(skipUpdate)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
skipUpdate = true;
|
||||||
|
var trackName = $('#songdata #Name').val();
|
||||||
|
var artistName = $('#songdata #Artist').val();
|
||||||
|
var submitUrl = $('#SubmitUrl').val();
|
||||||
|
var provider = $('#songdata #Provider').val();
|
||||||
|
var spotifyId = $('#songdata #SpotifyId').val();
|
||||||
|
var suggestionDataUrl = '?handler=SpotifySuggestions&song=' + encodeURIComponent(trackName)
|
||||||
|
+ '&artist=' + encodeURIComponent(artistName)
|
||||||
|
+ '&submitUrl=' + encodeURI(submitUrl)
|
||||||
|
+ '&provider=' + encodeURI(provider)
|
||||||
|
+ '&spotifyId=' + encodeURI(spotifyId);
|
||||||
|
$('#suggestionPicker').load(suggestionDataUrl, null, function() {
|
||||||
|
ReevaluateSubmit();
|
||||||
|
skipUpdate = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function SetSpotifyId(id) {
|
||||||
|
$('#SpotifyId').val(id);
|
||||||
|
ReevaluateSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function GetSpotifyId() {
|
||||||
|
return $('#SpotifyId').val();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('songdata').oninput = ReevaluateSubmit;
|
||||||
|
ReevaluateSubmit();
|
||||||
|
</script>
|
||||||
|
}
|
237
song_of_the_day/Pages/SongSubmission.cshtml.cs
Normal file
237
song_of_the_day/Pages/SongSubmission.cshtml.cs
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.VisualBasic;
|
||||||
|
|
||||||
|
namespace sotd.Pages;
|
||||||
|
|
||||||
|
public class SongSubmissionModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly ILogger<UserModel> _logger;
|
||||||
|
|
||||||
|
private SongResolver songResolver;
|
||||||
|
|
||||||
|
private string _submitUrl;
|
||||||
|
|
||||||
|
private SpotifyApiClient spotifyApiClient;
|
||||||
|
|
||||||
|
private SignalIntegration signalIntegration;
|
||||||
|
|
||||||
|
public SongSubmissionModel(ILogger<UserModel> logger, SongResolver songResolver, SpotifyApiClient spotifyApiClient, SignalIntegration signalIntegration)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
this.spotifyApiClient = spotifyApiClient;
|
||||||
|
this.songResolver = songResolver;
|
||||||
|
this.signalIntegration = signalIntegration;
|
||||||
|
_submitUrl = string.Empty;
|
||||||
|
SongData = new Song();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int? SubmissionIndex { get; set; }
|
||||||
|
|
||||||
|
[BindProperty]
|
||||||
|
public bool IsValidUrl { get; set; } = true;
|
||||||
|
|
||||||
|
[BindProperty]
|
||||||
|
public bool IsPageReadonly { get; set; } = false;
|
||||||
|
|
||||||
|
[BindProperty]
|
||||||
|
public SuggestionHelper SuggestionHelper { get; set; } = null;
|
||||||
|
|
||||||
|
[BindProperty]
|
||||||
|
public List<SongSuggestion> UserSongSubmissions { get; set; } = [];
|
||||||
|
|
||||||
|
[BindProperty(SupportsGet = true)]
|
||||||
|
public string SubmitUrl
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _submitUrl;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_submitUrl = value == null ? string.Empty : value.ToString();
|
||||||
|
Uri? newValue = default;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
newValue = new Uri(_submitUrl);
|
||||||
|
}
|
||||||
|
catch (UriFormatException)
|
||||||
|
{
|
||||||
|
IsValidUrl = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsValidUrl = true;
|
||||||
|
|
||||||
|
if (this.songResolver.CanValidate(newValue))
|
||||||
|
{
|
||||||
|
this.SongData = this.songResolver.ResolveSongAsync(newValue).Result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public List<SpotifyAPI.Web.FullTrack> SpotifySuggestions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var suggestionList = new List<SpotifyAPI.Web.FullTrack>();
|
||||||
|
if (!string.IsNullOrEmpty(this.SongData.Name) && !string.IsNullOrEmpty(this.SongData.Artist))
|
||||||
|
{
|
||||||
|
suggestionList.AddRange(spotifyApiClient.GetTrackCandidatesAsync(this.SongData.Name, this.SongData.Artist).Result);
|
||||||
|
}
|
||||||
|
return suggestionList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult OnGetSpotifySuggestions(string song, string artist, string submitUrl, SongProvider provider, string spotifyId)
|
||||||
|
{
|
||||||
|
this.SongData.Name = song;
|
||||||
|
this.SongData.Artist = artist;
|
||||||
|
this.SongData.Url = submitUrl;
|
||||||
|
this.SongData.Provider = provider;
|
||||||
|
this.SongData.SpotifyId = spotifyId;
|
||||||
|
return Partial("_SpotifySongSuggestionsPartial", this.SpotifySuggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
[BindProperty]
|
||||||
|
public string CanSubmit
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var disableCondition = !(string.IsNullOrEmpty(SongData?.Artist) && string.IsNullOrEmpty(SongData?.Name));
|
||||||
|
return disableCondition ? "" : "disabled";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[BindProperty(SupportsGet = true)]
|
||||||
|
public Song SongData { get; set; }
|
||||||
|
|
||||||
|
[BindProperty(SupportsGet = true)]
|
||||||
|
public bool HasUsedSuggestion { get; set; } = true;
|
||||||
|
|
||||||
|
private async Task UpdateGroup(SongSuggestion suggestion)
|
||||||
|
{
|
||||||
|
var isItToday = suggestion.Date.Date == DateTime.Today;
|
||||||
|
var dateString = isItToday ? "submission for today" : "submission from " + suggestion.Date.ToString("dd. MM. yyyy");
|
||||||
|
var displayName = string.IsNullOrEmpty(suggestion.User.NickName) ? suggestion.User.Name : suggestion.User.NickName;
|
||||||
|
var imageBuilder = await songResolver.GetPreviewImageAsync(suggestion.Song.SpotifyId);
|
||||||
|
var previewData = new LinkPreviewAttachment()
|
||||||
|
{
|
||||||
|
Title = suggestion.Song.Name,
|
||||||
|
Description = suggestion.Song.Artist,
|
||||||
|
Url = suggestion.Song.Url,
|
||||||
|
Base64Image = imageBuilder.ToString(),
|
||||||
|
};
|
||||||
|
await signalIntegration.SendMessageToGroupAsync($"**{displayName}**'s " + dateString + $" is: \n\n {suggestion.Song.Url}", previewData);
|
||||||
|
if (suggestion.HasUsedSuggestion)
|
||||||
|
{
|
||||||
|
await signalIntegration.SendMessageToGroupAsync($"The suggestion used for this pick was: \n\n **{suggestion.SuggestionHelper.Title}**'s \n\n {suggestion.SuggestionHelper.Description}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPost(int? submissionIndex)
|
||||||
|
{
|
||||||
|
if (submissionIndex == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Attempt to submit song without submissionId");
|
||||||
|
}
|
||||||
|
// Todo implement save submission
|
||||||
|
using var dci = DataContext.Instance;
|
||||||
|
var suggestion = dci.SongSuggestions
|
||||||
|
.Include(s => s.User)
|
||||||
|
.Include(s => s.SuggestionHelper)
|
||||||
|
.Where(s => s.Id == submissionIndex)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (suggestion == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Attempt to submit song with invalid submissionId");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.SongData.Artist = Request.Form["Artist"];
|
||||||
|
this.SongData.Url = Request.Form["SubmitUrl"];
|
||||||
|
this.SongData.Name = Request.Form["Name"];
|
||||||
|
this.SongData.SpotifyId = Request.Form["SpotifyId"];
|
||||||
|
this.SongData.Provider = Enum.Parse<SongProvider>(Request.Form["Provider"]);
|
||||||
|
|
||||||
|
// Let's check if we already have this song in the DB
|
||||||
|
var songToUse = dci.Songs.Where(s => s.SpotifyId == this.SongData.SpotifyId).FirstOrDefault();
|
||||||
|
if (songToUse == null)
|
||||||
|
{
|
||||||
|
// Song was not in DB yet, creating a new one
|
||||||
|
var newSong = new Song()
|
||||||
|
{
|
||||||
|
SpotifyId = this.SongData.SpotifyId,
|
||||||
|
Artist = this.SongData.Artist,
|
||||||
|
Name = this.SongData.Name,
|
||||||
|
Url = this.SongData.Url,
|
||||||
|
Provider = this.SongData.Provider,
|
||||||
|
};
|
||||||
|
var dbRecord = dci.Songs.Add(newSong);
|
||||||
|
var newId = await dci.SaveChangesAsync();
|
||||||
|
songToUse = dbRecord.Entity;
|
||||||
|
}
|
||||||
|
suggestion.Song = songToUse;
|
||||||
|
suggestion.UserHasSubmitted = true;
|
||||||
|
suggestion.HasUsedSuggestion = this.HasUsedSuggestion;
|
||||||
|
dci.Update(suggestion);
|
||||||
|
await dci.SaveChangesAsync();
|
||||||
|
// load overview page again after submitting
|
||||||
|
|
||||||
|
await UpdateGroup(suggestion);
|
||||||
|
|
||||||
|
return RedirectToPage("SongSubmission");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnGet(int? submissionIndex)
|
||||||
|
{
|
||||||
|
this.SubmissionIndex = submissionIndex;
|
||||||
|
|
||||||
|
// we want to show overview, so we need to fetch user submission list
|
||||||
|
if (submissionIndex == null)
|
||||||
|
{
|
||||||
|
using var dci = DataContext.Instance;
|
||||||
|
var currentUserName = this.User.Identity.Name;
|
||||||
|
this.UserSongSubmissions = dci.SongSuggestions
|
||||||
|
.Include(s => s.SuggestionHelper)
|
||||||
|
.Include(s => s.Song)
|
||||||
|
.Where(s => s.User.LdapUserName.Equals(currentUserName))
|
||||||
|
.OrderByDescending(s => s.Date)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using var dci = DataContext.Instance;
|
||||||
|
var songSuggestion = dci.SongSuggestions
|
||||||
|
.Include(s => s.SuggestionHelper)
|
||||||
|
.Include(s => s.Song)
|
||||||
|
.Where(s => s.Id.Equals(submissionIndex.Value))
|
||||||
|
.First();
|
||||||
|
this.SuggestionHelper = songSuggestion.SuggestionHelper;
|
||||||
|
this.SongData = songSuggestion.Song == null ? new Song() : songSuggestion.Song;
|
||||||
|
this.SubmitUrl = this.SongData.Url;
|
||||||
|
if (!string.IsNullOrEmpty(this.SongData.Name) && !string.IsNullOrEmpty(this.SongData.Artist))
|
||||||
|
{
|
||||||
|
this.IsPageReadonly = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult OnGetUpdate()
|
||||||
|
{
|
||||||
|
var songUrl = Request.Query["SubmitUrl"];
|
||||||
|
this.SubmitUrl = songUrl.ToString();
|
||||||
|
var songPartialModel = new SongPartialModel()
|
||||||
|
{
|
||||||
|
InnerSong = SongData,
|
||||||
|
IsPageReadonly = false
|
||||||
|
};
|
||||||
|
return Partial("_SongPartial", songPartialModel); ;
|
||||||
|
}
|
||||||
|
}
|
@ -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 = "";
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
user.NickName = this.UserNickName;
|
if (user != null)
|
||||||
dci.SaveChanges();
|
{
|
||||||
this.UserName = user.Name;
|
user.NickName = this.UserNickName;
|
||||||
|
dci.SaveChanges();
|
||||||
|
this.UserName = user.Name ?? string.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,38 @@
|
|||||||
|
|
||||||
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);
|
||||||
|
|
||||||
var rand = new Random();
|
|
||||||
var num = rand.NextInt64();
|
|
||||||
var mod = num % AppConfiguration.Instance.AverageDaysBetweenRequests;
|
|
||||||
|
|
||||||
Console.WriteLine("Setting up user check timer");
|
// 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<SpotifyApiClient>();
|
||||||
|
builder.Services.AddSingleton<SongResolver>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
var logger = app.Logger;
|
||||||
|
|
||||||
|
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)
|
||||||
@ -34,10 +40,10 @@ 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);
|
||||||
Console.WriteLine("New user:");
|
logger.LogDebug("New user:");
|
||||||
Console.WriteLine($" Name: {newUserContact.Name}");
|
logger.LogDebug($" Name: {newUserContact.Name}");
|
||||||
Console.WriteLine($" MemberId: {memberId}");
|
logger.LogDebug($" MemberId: {memberId}");
|
||||||
User newUser = new User()
|
User newUser = new User()
|
||||||
{
|
{
|
||||||
Name = newUserContact.Name,
|
Name = newUserContact.Name,
|
||||||
@ -59,9 +65,8 @@ userCheckTimer.OnOccurence += async (s, ea) =>
|
|||||||
}
|
}
|
||||||
await dci.DisposeAsync();
|
await dci.DisposeAsync();
|
||||||
};
|
};
|
||||||
userCheckTimer.Start();
|
|
||||||
|
|
||||||
Console.WriteLine("Setting up user intro timer");
|
logger.LogTrace("Setting up user intro timer");
|
||||||
var userIntroTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
|
var userIntroTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
|
||||||
userIntroTimer.OnOccurence += async (s, ea) =>
|
userIntroTimer.OnOccurence += async (s, ea) =>
|
||||||
{
|
{
|
||||||
@ -76,7 +81,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;
|
||||||
}
|
}
|
||||||
@ -87,43 +93,42 @@ userIntroTimer.OnOccurence += async (s, ea) =>
|
|||||||
}
|
}
|
||||||
await dci.DisposeAsync();
|
await dci.DisposeAsync();
|
||||||
};
|
};
|
||||||
userIntroTimer.Start();
|
|
||||||
|
|
||||||
|
|
||||||
Console.WriteLine("Setting up pick of the day timer");
|
logger.LogTrace("Setting up pick of the day timer");
|
||||||
var pickOfTheDayTimer = new CronTimer("0 8 * * *", "Europe/Vienna", includingSeconds: false);
|
var pickOfTheDayTimer = new CronTimer("0 8 * * *", "Europe/Vienna", includingSeconds: false);
|
||||||
pickOfTheDayTimer.OnOccurence += async (s, ea) =>
|
pickOfTheDayTimer.OnOccurence += async (s, ea) =>
|
||||||
{
|
{
|
||||||
var dci = DataContext.Instance;
|
var dci = DataContext.Instance;
|
||||||
|
|
||||||
var lastSong = dci.SongSuggestions?.LastOrDefault();
|
var lastSong = dci.SongSuggestions?.OrderBy(s => s.Id).LastOrDefault();
|
||||||
|
|
||||||
if (lastSong != null && lastSong.Date >= DateTime.Today.Subtract(TimeSpan.FromDays(AppConfiguration.Instance.DaysBetweenRequests)))
|
if (lastSong != null && lastSong.Date > DateTime.Today.Subtract(TimeSpan.FromDays(AppConfiguration.Instance.DaysBetweenRequests)))
|
||||||
{
|
{
|
||||||
Console.WriteLine("Skipping pick of the day today!");
|
logger.LogWarning("Skipping pick of the day today!");
|
||||||
await dci.DisposeAsync();
|
await dci.DisposeAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dci.Users == null || dci.SuggestionHelpers == null || dci.SongSuggestions == null)
|
if (dci.Users == null || dci.SuggestionHelpers == null || dci.SongSuggestions == null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Unable to properly initialize DB context!");
|
logger.LogError("Unable to properly initialize DB context!");
|
||||||
await dci.DisposeAsync();
|
await dci.DisposeAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var potentialUsers = dci.Users.Where(u => !u.WasChosenForSuggestionThisRound);
|
var potentialUsers = dci.Users.Where(u => !u.WasChosenForSuggestionThisRound);
|
||||||
if (!potentialUsers.Any())
|
if (!potentialUsers.Any())
|
||||||
{
|
{
|
||||||
Console.WriteLine("Resetting suggestion count on users before resuming");
|
logger.LogTrace("Resetting suggestion count on users before resuming");
|
||||||
await dci.Users.ForEachAsync(u => u.WasChosenForSuggestionThisRound = false);
|
await dci.Users.ForEachAsync(u => u.WasChosenForSuggestionThisRound = false);
|
||||||
await dci.SaveChangesAsync();
|
await dci.SaveChangesAsync();
|
||||||
potentialUsers = dci.Users.Where(u => !u.WasChosenForSuggestionThisRound);
|
potentialUsers = dci.Users.Where(u => !u.WasChosenForSuggestionThisRound);
|
||||||
}
|
}
|
||||||
Console.WriteLine("Today's pool of pickable users is: " + string.Join(", ", potentialUsers.Select(u => u.Name)));
|
logger.LogDebug("Today's pool of pickable users is: " + string.Join(", ", potentialUsers.Select(u => u.Name)));
|
||||||
var luckyUser = potentialUsers.ElementAt((new Random()).Next(potentialUsers.Count()));
|
var luckyUser = potentialUsers.ElementAt((new Random()).Next(potentialUsers.Count()));
|
||||||
if (luckyUser == null)
|
if (luckyUser == null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Unable to determine today's lucky user!");
|
logger.LogError("Unable to determine today's lucky user!");
|
||||||
await dci.DisposeAsync();
|
await dci.DisposeAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -135,41 +140,43 @@ pickOfTheDayTimer.OnOccurence += async (s, ea) =>
|
|||||||
SuggestionHelper = suggestion,
|
SuggestionHelper = suggestion,
|
||||||
UserHasSubmitted = false,
|
UserHasSubmitted = false,
|
||||||
HasUsedSuggestion = false,
|
HasUsedSuggestion = false,
|
||||||
Date = DateTime.Today
|
Date = DateTime.Today.ToUniversalTime()
|
||||||
};
|
};
|
||||||
if (luckyUser.SignalMemberId is string signalId)
|
if (luckyUser.SignalMemberId is string signalId)
|
||||||
{
|
{
|
||||||
await dci.SongSuggestions.AddAsync(newSongSuggestion);
|
var result = await dci.SongSuggestions.AddAsync(newSongSuggestion);
|
||||||
|
newSongSuggestion = result.Entity;
|
||||||
|
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.SendMessageToUserAsync($"Congratulations, you have been chosen to share a song today!", signalId);
|
||||||
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($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*", 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($"Please navigate to https://sord.disi.dev/SongSubmission/{newSongSuggestion.Id} to submit your choice!", luckyUser.SignalMemberId);
|
||||||
}
|
}
|
||||||
await dci.DisposeAsync();
|
await dci.DisposeAsync();
|
||||||
};
|
};
|
||||||
pickOfTheDayTimer.Start();
|
|
||||||
|
|
||||||
var startUserAssociationProcess = async (User userToAssociate) =>
|
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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Console.WriteLine("Setting up LdapAssociation timer");
|
logger.LogTrace("Setting up LdapAssociation timer");
|
||||||
var ldapAssociationTimer = new CronTimer("*/10 * * * *", "Europe/Vienna", includingSeconds: false);
|
var ldapAssociationTimer = new CronTimer("*/10 * * * *", "Europe/Vienna", includingSeconds: false);
|
||||||
ldapAssociationTimer.OnOccurence += async (s, ea) =>
|
ldapAssociationTimer.OnOccurence += async (s, ea) =>
|
||||||
{
|
{
|
||||||
var dci = DataContext.Instance;
|
var dci = DataContext.Instance;
|
||||||
if (dci.Users == null)
|
if (dci.Users == null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Unable to properly initialize DB context!");
|
logger.LogError("Unable to properly initialize DB context!");
|
||||||
await dci.DisposeAsync();
|
await dci.DisposeAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -190,27 +197,31 @@ ldapAssociationTimer.OnOccurence += async (s, ea) =>
|
|||||||
}
|
}
|
||||||
await dci.DisposeAsync();
|
await dci.DisposeAsync();
|
||||||
};
|
};
|
||||||
ldapAssociationTimer.Start();
|
|
||||||
|
|
||||||
var searchResults = LdapIntegration.Instance.SearchInAD(
|
logger.LogTrace("Setting up MessageSync timer");
|
||||||
AppConfiguration.Instance.LDAPConfig.LDAPQueryBase,
|
var messageSyncTimer = new CronTimer("*/10 * * * *", "Europe/Vienna", includingSeconds: false);
|
||||||
$"(memberOf={AppConfiguration.Instance.LDAPConfig.CrewGroup})",
|
messageSyncTimer.OnOccurence += async (s, ea) =>
|
||||||
SearchScope.Subtree
|
{
|
||||||
);
|
var signalIntegration = app.Services.GetService<SignalIntegration>();
|
||||||
|
await signalIntegration.GetMessagesAsync();
|
||||||
|
};
|
||||||
|
// disabled for now, is still buggy
|
||||||
|
// messageSyncTimer.Start();
|
||||||
|
|
||||||
// Add services to the container.
|
// only start interaction timers in production builds
|
||||||
builder.Services.AddRazorPages();
|
// for local/development testing we want those disabled
|
||||||
builder.Services.AddOpenApi();
|
if (!app.Environment.IsDevelopment())
|
||||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
{
|
||||||
.AddCookie(options =>
|
logger.LogTrace("Starting timer for scheduled processes.");
|
||||||
{
|
userCheckTimer.Start();
|
||||||
options.LoginPath = "/Auth/Login";
|
userIntroTimer.Start();
|
||||||
});
|
pickOfTheDayTimer.Start();
|
||||||
|
ldapAssociationTimer.Start();
|
||||||
builder.Services.AddSingleton<LdapAuthenticationService>();
|
}
|
||||||
builder.Services.AddSingleton<PhoneClaimCodeProviderService>();
|
else
|
||||||
|
{
|
||||||
var app = builder.Build();
|
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())
|
||||||
|
23
song_of_the_day/SongValidators/Base64UrlImageBuilder.cs
Normal file
23
song_of_the_day/SongValidators/Base64UrlImageBuilder.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
public class Base64UrlImageBuilder
|
||||||
|
{
|
||||||
|
public string ContentType { set; get; }
|
||||||
|
|
||||||
|
public string Url
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var httpClient = new HttpClient();
|
||||||
|
var response = (httpClient.GetAsync(new Uri($"{value}"))).Result;
|
||||||
|
var bytes = (response.Content.ReadAsByteArrayAsync()).Result;
|
||||||
|
FileContents = Convert.ToBase64String(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FileContents { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
//return $"data:{ContentType};base64,{FileContents}";
|
||||||
|
return $"{FileContents}";
|
||||||
|
}
|
||||||
|
}
|
10
song_of_the_day/SongValidators/ISongValidator.cs
Normal file
10
song_of_the_day/SongValidators/ISongValidator.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
public interface ISongValidator
|
||||||
|
{
|
||||||
|
Task<Song> ValidateAsync(Uri songUri);
|
||||||
|
|
||||||
|
bool CanValidateUri(Uri songUri);
|
||||||
|
|
||||||
|
Task<bool> CanExtractSongMetadataAsync(Uri songUri);
|
||||||
|
|
||||||
|
SongProvider GetSongProvider();
|
||||||
|
}
|
84
song_of_the_day/SongValidators/SongResolver.cs
Normal file
84
song_of_the_day/SongValidators/SongResolver.cs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
|
public class SongResolver
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<ISongValidator> _songValidators;
|
||||||
|
|
||||||
|
private readonly ILogger<SongResolver> logger;
|
||||||
|
|
||||||
|
private SpotifyApiClient spotifyApiClient;
|
||||||
|
|
||||||
|
public SongResolver(ILogger<SongResolver> logger, ILoggerFactory loggerFactory, SpotifyApiClient spotifyApiClient)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
|
||||||
|
this._songValidators = new List<ISongValidator>();
|
||||||
|
this.spotifyApiClient = spotifyApiClient;
|
||||||
|
|
||||||
|
foreach (Type mytype in System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
|
||||||
|
.Where(mytype => { return (mytype.GetInterfaces().Contains(typeof(ISongValidator)) && !(mytype.Name.Split("`")[0].EndsWith("Base"))); }))
|
||||||
|
{
|
||||||
|
var typedLogger = loggerFactory.CreateLogger(mytype);
|
||||||
|
if (Activator.CreateInstance(mytype, typedLogger, spotifyApiClient) is ISongValidator validator)
|
||||||
|
{
|
||||||
|
logger.LogDebug("Registering song validator: {ValidatorType}", mytype.Name);
|
||||||
|
this._songValidators = this._songValidators.Append(validator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Song> ResolveSongAsync(Uri songUri)
|
||||||
|
{
|
||||||
|
foreach (var validator in _songValidators)
|
||||||
|
{
|
||||||
|
if (validator.CanValidateUri(songUri))
|
||||||
|
{
|
||||||
|
if (!await validator.CanExtractSongMetadataAsync(songUri))
|
||||||
|
{
|
||||||
|
this.logger.LogWarning("Cannot extract metadata for song URI: {SongUri}", songUri);
|
||||||
|
return new Song
|
||||||
|
{
|
||||||
|
Artist = "Unknown Artist",
|
||||||
|
Name = "Unknown Title",
|
||||||
|
Url = songUri.ToString(),
|
||||||
|
Provider = SongProvider.PlainHttp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return await validator.ValidateAsync(songUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Song
|
||||||
|
{
|
||||||
|
Artist = "Unknown Artist",
|
||||||
|
Name = "Unknown Title",
|
||||||
|
Url = songUri.ToString(),
|
||||||
|
Provider = SongProvider.PlainHttp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanValidate(Uri songUri)
|
||||||
|
{
|
||||||
|
foreach (var validator in _songValidators)
|
||||||
|
{
|
||||||
|
if (validator.CanValidateUri(songUri))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Base64UrlImageBuilder> GetPreviewImageAsync(string spotifyId)
|
||||||
|
{
|
||||||
|
var track = await spotifyApiClient.GetTrackByIdAsync(spotifyId);
|
||||||
|
var url = track.Album.Images.FirstOrDefault().Url;
|
||||||
|
return new Base64UrlImageBuilder()
|
||||||
|
{
|
||||||
|
Url = url,
|
||||||
|
ContentType = "image/jpeg"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
30
song_of_the_day/SongValidators/SongValidatorBase.cs
Normal file
30
song_of_the_day/SongValidators/SongValidatorBase.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
public abstract class SongValidatorBase : ISongValidator
|
||||||
|
{
|
||||||
|
public abstract Task<Song> ValidateAsync(Uri songUri);
|
||||||
|
|
||||||
|
public abstract Task<bool> CanExtractSongMetadataAsync(Uri songUri);
|
||||||
|
|
||||||
|
public abstract bool CanValidateUri(Uri songUri);
|
||||||
|
|
||||||
|
public abstract SongProvider GetSongProvider();
|
||||||
|
|
||||||
|
protected SpotifyApiClient _spotifyApiClient;
|
||||||
|
|
||||||
|
protected ILogger _logger;
|
||||||
|
|
||||||
|
public SongValidatorBase(ILogger logger, SpotifyApiClient spotifyApiClient)
|
||||||
|
{
|
||||||
|
_spotifyApiClient = spotifyApiClient;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task<string> LookupSpotifyIdAsync(string songName, string songArtist)
|
||||||
|
{
|
||||||
|
var candidates = await _spotifyApiClient.GetTrackCandidatesAsync(songName, songArtist);
|
||||||
|
return candidates.Any() ? candidates[0].Id : "";
|
||||||
|
}
|
||||||
|
}
|
41
song_of_the_day/SongValidators/SpotifyValidator.cs
Normal file
41
song_of_the_day/SongValidators/SpotifyValidator.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using AngleSharp;
|
||||||
|
using AngleSharp.Dom;
|
||||||
|
using AngleSharp.Html.Dom;
|
||||||
|
|
||||||
|
public class SpotifyValidator : UriBasedSongValidatorBase
|
||||||
|
{
|
||||||
|
public override string UriValidatorRegex => @"^(https?://)?open.spotify.com/track/([a-zA-Z0-9_-]{22})(\?si=[a-zA-Z0-9_-]+)?$";
|
||||||
|
|
||||||
|
public SpotifyValidator(ILogger _logger, SpotifyApiClient spotifyApiClient) : base(_logger, spotifyApiClient)
|
||||||
|
{}
|
||||||
|
|
||||||
|
public override Task<bool> CanExtractSongMetadataAsync(Uri songUri)
|
||||||
|
{
|
||||||
|
return Task.FromResult(this.CanValidateUri(songUri));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override SongProvider GetSongProvider()
|
||||||
|
{
|
||||||
|
return SongProvider.Spotify;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<Song> ValidateAsync(Uri songUri)
|
||||||
|
{
|
||||||
|
var regexp = new Regex(UriValidatorRegex, RegexOptions.IgnoreCase);
|
||||||
|
var trackIdMatch = regexp.Match(songUri.ToString()).Groups[2].Value;
|
||||||
|
|
||||||
|
var track = await _spotifyApiClient.GetTrackByIdAsync(trackIdMatch);
|
||||||
|
|
||||||
|
var song = new Song
|
||||||
|
{
|
||||||
|
Name = track.Name,
|
||||||
|
Artist = track.Artists.FirstOrDefault()?.Name ?? "Unknown Artist",
|
||||||
|
Url = songUri.ToString(),
|
||||||
|
Provider = SongProvider.Spotify,
|
||||||
|
SpotifyId = trackIdMatch
|
||||||
|
};
|
||||||
|
|
||||||
|
return song;
|
||||||
|
}
|
||||||
|
}
|
20
song_of_the_day/SongValidators/UriBasedSongValidatorBase.cs
Normal file
20
song_of_the_day/SongValidators/UriBasedSongValidatorBase.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
public abstract class UriBasedSongValidatorBase : SongValidatorBase
|
||||||
|
{
|
||||||
|
public abstract string UriValidatorRegex { get; }
|
||||||
|
|
||||||
|
public UriBasedSongValidatorBase(ILogger logger, SpotifyApiClient spotifyApiClient) : base(logger, spotifyApiClient)
|
||||||
|
{}
|
||||||
|
|
||||||
|
public Match GetUriMatch(Uri songUri)
|
||||||
|
{
|
||||||
|
var regexp = new Regex(UriValidatorRegex, RegexOptions.IgnoreCase);
|
||||||
|
return regexp.Match(songUri.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanValidateUri(Uri songUri)
|
||||||
|
{
|
||||||
|
return GetUriMatch(songUri).Success;
|
||||||
|
}
|
||||||
|
}
|
48
song_of_the_day/SongValidators/YoutubeMusicValidator.cs
Normal file
48
song_of_the_day/SongValidators/YoutubeMusicValidator.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
using AngleSharp;
|
||||||
|
using AngleSharp.Dom;
|
||||||
|
using YouTubeMusicAPI.Client;
|
||||||
|
|
||||||
|
public class YoutubeMusicValidator : UriBasedSongValidatorBase
|
||||||
|
{
|
||||||
|
public override string UriValidatorRegex => @"^(https?://)?(music\.youtube\.com/playlist\?list=)([a-zA-Z0-9_-]+)";
|
||||||
|
|
||||||
|
private YouTubeMusicClient youtubeClient;
|
||||||
|
|
||||||
|
public YoutubeMusicValidator(ILogger logger, SpotifyApiClient spotifyApiClient) : base(logger, spotifyApiClient)
|
||||||
|
{
|
||||||
|
youtubeClient = new YouTubeMusicClient(logger, "AT", null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override SongProvider GetSongProvider()
|
||||||
|
{
|
||||||
|
return SongProvider.YoutubeMusic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<bool> CanExtractSongMetadataAsync(Uri songUri)
|
||||||
|
{
|
||||||
|
return Task.FromResult(this.CanValidateUri(songUri));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<Song> ValidateAsync(Uri songUri)
|
||||||
|
{
|
||||||
|
var match = this.GetUriMatch(songUri);
|
||||||
|
var playlistId = match.Groups[3].Value;
|
||||||
|
|
||||||
|
var playlistResult = await youtubeClient.GetCommunityPlaylistInfoAsync(playlistId);
|
||||||
|
var songData = playlistResult.Songs[0];
|
||||||
|
|
||||||
|
var title = songData.Name;
|
||||||
|
var artist = songData.Artists[0].Name;
|
||||||
|
|
||||||
|
var song = new Song
|
||||||
|
{
|
||||||
|
Name = title,
|
||||||
|
Artist = artist,
|
||||||
|
Url = songUri.ToString(),
|
||||||
|
Provider = SongProvider.YoutubeMusic,
|
||||||
|
SpotifyId = await this.LookupSpotifyIdAsync(title, artist)
|
||||||
|
};
|
||||||
|
|
||||||
|
return song;
|
||||||
|
}
|
||||||
|
}
|
48
song_of_the_day/SongValidators/YoutubeValidator.cs
Normal file
48
song_of_the_day/SongValidators/YoutubeValidator.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
using AngleSharp;
|
||||||
|
using AngleSharp.Dom;
|
||||||
|
using AngleSharp.Html.Dom;
|
||||||
|
using YouTubeMusicAPI.Client;
|
||||||
|
|
||||||
|
public class YoutubeValidator : UriBasedSongValidatorBase
|
||||||
|
{
|
||||||
|
private YouTubeMusicClient youtubeClient;
|
||||||
|
|
||||||
|
public YoutubeValidator(ILogger logger, SpotifyApiClient spotifyApiClient) : base(logger, spotifyApiClient)
|
||||||
|
{
|
||||||
|
youtubeClient = new("AT");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string UriValidatorRegex => @"^(https?://)?(www\.)?(youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})";
|
||||||
|
|
||||||
|
public override async Task<bool> CanExtractSongMetadataAsync(Uri songUri)
|
||||||
|
{
|
||||||
|
return this.CanValidateUri(songUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override SongProvider GetSongProvider()
|
||||||
|
{
|
||||||
|
return SongProvider.YouTube;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<Song> ValidateAsync(Uri songUri)
|
||||||
|
{
|
||||||
|
var match = this.GetUriMatch(songUri);
|
||||||
|
var songId = match.Groups[4].Value;
|
||||||
|
|
||||||
|
var songData = await youtubeClient.GetSongVideoInfoAsync(songId);
|
||||||
|
|
||||||
|
var title = songData.Name;
|
||||||
|
var artist = songData.Artists[0].Name;
|
||||||
|
|
||||||
|
var song = new Song
|
||||||
|
{
|
||||||
|
Name = title,
|
||||||
|
Artist = artist,
|
||||||
|
Url = songUri.ToString(),
|
||||||
|
Provider = SongProvider.YouTube,
|
||||||
|
SpotifyId = await this.LookupSpotifyIdAsync(title, artist)
|
||||||
|
};
|
||||||
|
|
||||||
|
return song;
|
||||||
|
}
|
||||||
|
}
|
44
song_of_the_day/SpotifyIntegration/SpotifyClient.cs
Normal file
44
song_of_the_day/SpotifyIntegration/SpotifyClient.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
using SpotifyAPI.Web;
|
||||||
|
|
||||||
|
public class SpotifyApiClient
|
||||||
|
{
|
||||||
|
private SpotifyClient _spotifyClient;
|
||||||
|
|
||||||
|
public SpotifyApiClient()
|
||||||
|
{
|
||||||
|
var config = SpotifyClientConfig.CreateDefault()
|
||||||
|
.WithAuthenticator(new ClientCredentialsAuthenticator(
|
||||||
|
AppConfiguration.Instance.SpotifyClientId,
|
||||||
|
AppConfiguration.Instance.SpotifyClientSecret));
|
||||||
|
|
||||||
|
_spotifyClient = new SpotifyClient(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<FullTrack>> GetTrackCandidatesAsync(string trackName, string artistName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var searchResponse = await _spotifyClient.Search.Item(new SearchRequest(SearchRequest.Types.Track, $"{trackName} {artistName}")
|
||||||
|
{
|
||||||
|
Limit = 5
|
||||||
|
});
|
||||||
|
return searchResponse.Tracks.Items ?? new List<FullTrack>();
|
||||||
|
}
|
||||||
|
catch (APIException ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Error fetching tracks by query: \"{trackName} {artistName}\": {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<FullTrack> GetTrackByIdAsync(string trackId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _spotifyClient.Tracks.Get(trackId);
|
||||||
|
}
|
||||||
|
catch (APIException ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Error fetching track by ID: {trackId}: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1 @@
|
|||||||
0.3.0
|
0.4.0
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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": "*"
|
||||||
|
@ -6,16 +6,19 @@
|
|||||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AngleSharp" Version="1.3.0" />
|
||||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
<PackageReference Include="NSwag.ApiDescription.Client" Version="13.0.5" />
|
<PackageReference Include="NSwag.ApiDescription.Client" Version="13.0.5" />
|
||||||
<PackageReference Include="Scalar.AspNetCore" Version="2.1.*" />
|
<PackageReference Include="Scalar.AspNetCore" Version="2.1.*" />
|
||||||
|
<PackageReference Include="SpotifyAPI.Web" Version="7.2.1" />
|
||||||
|
<PackageReference Include="YouTubeMusicAPI" Version="2.2.8" />
|
||||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="*" />
|
<PackageReference Include="System.DirectoryServices.Protocols" Version="*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -2423,6 +2423,9 @@
|
|||||||
"edit_timestamp": {
|
"edit_timestamp": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"link_preview": {
|
||||||
|
"$ref": "#/definitions/data.LinkPreviewType"
|
||||||
|
},
|
||||||
"mentions": {
|
"mentions": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@ -2817,6 +2820,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"data.LinkPreviewType": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"base64_thumbnail": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"data.MessageMention": {
|
"data.MessageMention": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user