Compare commits

..

55 Commits

Author SHA1 Message Date
e9a824c6ef release: version 0.3.2 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 30s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m28s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / deploy (push) Successful in 2m48s
SonarQube Scan / SonarQube Trigger (push) Failing after 4m55s
2025-05-25 21:36:50 +02:00
e0f206bc0c fix: exception thrown on LastOrDefault(), refs NOISSUE 2025-05-25 21:36:34 +02:00
183309e1ed release: version 0.3.1 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 21s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m22s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / deploy (push) Successful in 3m13s
SonarQube Scan / SonarQube Trigger (push) Failing after 4m54s
2025-05-24 19:20:17 +02:00
a6321324f7 fix: fix build errors, refs NOISSUE 2025-05-24 19:20:12 +02:00
e400249284 release: version 0.3.0 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 24s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m3s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / deploy (push) Failing after 1m48s
SonarQube Scan / SonarQube Trigger (push) Failing after 4m56s
2025-05-24 18:56:45 +02:00
73c5e40e1d ci: more CI fixes, refs NOISSUE 2025-05-24 18:56:37 +02:00
f728c88853 ci: improve commit message generation script, refs NOISSUE 2025-05-24 18:51:58 +02:00
5fdd6ec1d0 feat: keep track of user submissions, refs #4 2025-05-24 18:51:58 +02:00
fbb6d1a409 feat: save submission history, refs #7 2025-05-24 18:51:50 +02:00
Simon Diesenreiter
d9da54653e release: version 0.2.6 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 1m17s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m20s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / deploy (push) Successful in 3m32s
SonarQube Scan / SonarQube Trigger (push) Failing after 5m7s
2025-05-22 14:28:36 +02:00
Simon Diesenreiter
859e96a706 fix: bugfixes, refs NOISSUE 2025-05-22 14:28:23 +02:00
0135b89f01 reduce number of emitted logs
Some checks failed
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m5s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
SonarQube Scan / SonarQube Trigger (push) Failing after 4m47s
2025-05-21 04:33:24 -07:00
8c1bbc9866 release: version 0.2.5 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 24s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m11s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / deploy (push) Successful in 2m16s
SonarQube Scan / SonarQube Trigger (push) Failing after 4m52s
2025-05-18 11:59:55 +02:00
8100998732 fix: install dependencies in runtime container not only build container, refs NOISSUE 2025-05-18 11:59:52 +02:00
4a77a0d33a release: version 0.2.4 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 35s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m36s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / deploy (push) Successful in 2m37s
SonarQube Scan / SonarQube Trigger (push) Failing after 5m5s
2025-05-18 11:44:06 +02:00
3acd4ad9d9 fix: build errors, refs NOISSUE 2025-05-18 11:44:01 +02:00
33aae65647 release: version 0.2.3 🚀
Some checks failed
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m29s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / Create Release (push) Successful in 1m18s
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build Docker image / deploy (push) Has been cancelled
2025-05-18 11:42:19 +02:00
083038d76c fix: resolve linting errors, refs NOISSUE 2025-05-18 11:42:09 +02:00
055cf79354 release: version 0.2.2 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 18s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m3s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / deploy (push) Failing after 2m0s
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
2025-05-18 11:39:32 +02:00
b4a893d936 fix: also send pick suggestion to the group, refs NOISSUE 2025-05-18 11:39:27 +02:00
31af2e3238 release: version 0.2.1 🚀
Some checks failed
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m12s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / Create Release (push) Successful in 14s
Build Docker image / deploy (push) Successful in 1m44s
SonarQube Scan / SonarQube Trigger (push) Failing after 4m49s
2025-05-17 22:32:12 +02:00
4f4cda622f fix: fix Docker build, refs NOISSUE 2025-05-17 22:32:08 +02:00
49d8c2cb08 release: version 0.2.0 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 32s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m26s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / deploy (push) Failing after 3m18s
SonarQube Scan / SonarQube Trigger (push) Failing after 4m54s
2025-05-17 22:17:38 +02:00
efbbc915e5 feat: add user management, refs NOISSUE 2025-05-17 22:17:09 +02:00
Simon Diesenreiter
6b9c383697 feat(auth): initial auth added part 2, refs NOISSUE
Some checks failed
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m28s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
SonarQube Scan / SonarQube Trigger (push) Failing after 5m32s
2025-04-26 14:04:28 +02:00
Simon Diesenreiter
92828a90e2 feat(auth): initial auth added, refs NOISSUE
Some checks failed
CI / tests_linux (9.0.X, ubuntu-latest) (push) Blocked by required conditions
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
2025-04-26 14:00:19 +02:00
Simon Diesenreiter
9fe4613be7 release: version 0.1.21 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 25s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been cancelled
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build Docker image / deploy (push) Successful in 2m20s
2025-04-15 15:33:04 +02:00
Simon Diesenreiter
a09e27281e fix: fix new user saving refs NOISSUE 2025-04-15 15:33:02 +02:00
Simon Diesenreiter
8417003ea7 release: version 0.1.20 🚀
Some checks failed
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been cancelled
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build Docker image / Create Release (push) Successful in 20s
Build Docker image / deploy (push) Successful in 1m32s
2025-04-15 15:24:51 +02:00
Simon Diesenreiter
8412cc13df fix: remove unnecessary dotnet runtime download in CI job refs NOISSUE 2025-04-15 15:24:47 +02:00
Simon Diesenreiter
7dabcb71f8 release: version 0.1.19 🚀
Some checks failed
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been cancelled
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build Docker image / Create Release (push) Successful in 19s
Build Docker image / deploy (push) Failing after 1m56s
2025-04-15 15:18:40 +02:00
Simon Diesenreiter
001711487c fix: improve Docker build refs NOISSUE 2025-04-15 15:18:37 +02:00
Simon Diesenreiter
bf4bcebfb8 release: version 0.1.18 🚀
Some checks failed
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been cancelled
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build Docker image / Create Release (push) Successful in 12s
Build Docker image / deploy (push) Failing after 1m48s
2025-04-15 15:10:23 +02:00
Simon Diesenreiter
0561eb757c fix: remove broken Crontimer NuGet feed reference refs NOISSUE 2025-04-15 15:10:18 +02:00
Simon Diesenreiter
5510832538 release: version 0.1.17 🚀
Some checks failed
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been cancelled
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build Docker image / Create Release (push) Successful in 17s
Build Docker image / deploy (push) Failing after 1m18s
2025-04-15 15:07:10 +02:00
Simon Diesenreiter
604e8dfe70 fix: remove uinnecessary NuGet login refs NOISSUE 2025-04-15 15:07:03 +02:00
Simon Diesenreiter
2c2a58975e release: version 0.1.16 🚀
Some checks failed
CI / tests_linux (9.0.X, ubuntu-latest) (push) Blocked by required conditions
Build Docker image / Create Release (push) Successful in 20s
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build Docker image / deploy (push) Has been cancelled
2025-04-15 15:06:32 +02:00
Simon Diesenreiter
7572a66ae2 fix: add direct CronTimer reference refs NOISSUE 2025-04-15 15:06:29 +02:00
Simon Diesenreiter
3ed6c4c54f release: version 0.1.15 🚀
Some checks failed
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been cancelled
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build Docker image / Create Release (push) Successful in 10s
Build Docker image / deploy (push) Failing after 52s
2025-04-15 14:56:01 +02:00
Simon Diesenreiter
f2472a3ea2 fix: login to NuGet feed at build refs NOISSUE 2025-04-15 14:55:56 +02:00
Simon Diesenreiter
4d657cf887 release: version 0.1.14 🚀
Some checks failed
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been cancelled
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build Docker image / Create Release (push) Successful in 12s
Build Docker image / deploy (push) Failing after 1m13s
2025-04-15 14:38:19 +02:00
Simon Diesenreiter
1388ce80db fix: fix typo refs NOISSUE 2025-04-15 14:38:16 +02:00
Simon Diesenreiter
d335ddfd0d release: version 0.1.13 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 21s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m2s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build Docker image / deploy (push) Failing after 1m57s
2025-04-15 14:34:56 +02:00
Simon Diesenreiter
68caf0a329 fix: really fix Crontab dependency refs NOISSUE 2025-04-15 14:34:51 +02:00
Simon Diesenreiter
2889661f24 release: version 0.1.12 🚀
Some checks failed
Build Docker image / Create Release (push) Successful in 1m17s
CI / linter (9.0.X, ubuntu-latest) (push) Failing after 1m37s
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been skipped
Build Docker image / deploy (push) Successful in 2m46s
SonarQube Scan / SonarQube Trigger (push) Failing after 4m56s
2025-04-15 13:58:19 +02:00
Simon Diesenreiter
513589a6fc fix: hotfixed CronTab dependencyrefs NOISSUE 2025-04-15 13:58:12 +02:00
Simon Diesenreiter
28616252aa release: version 0.1.11 🚀
Some checks failed
CI / tests_linux (9.0.X, ubuntu-latest) (push) Blocked by required conditions
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Upload Python Package / Create Release (push) Successful in 8s
Upload Python Package / deploy (push) Successful in 1m54s
2025-04-15 13:07:36 +02:00
Simon Diesenreiter
09acbbefea fix: more debug outputs refs NOISSUE 2025-04-15 13:06:39 +02:00
Simon Diesenreiter
9598965a4a release: version 0.1.10 🚀
Some checks failed
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been cancelled
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Upload Python Package / Create Release (push) Successful in 46s
Upload Python Package / deploy (push) Successful in 2m26s
2025-04-15 12:37:15 +02:00
Simon Diesenreiter
2111eb21e3 fix: release version jumbled up refs NOISSUE 2025-04-15 12:37:06 +02:00
Simon Diesenreiter
49ec368b2f release: version 0.1.8 🚀
Some checks failed
CI / tests_linux (9.0.X, ubuntu-latest) (push) Blocked by required conditions
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Upload Python Package / deploy (push) Has been cancelled
Upload Python Package / Create Release (push) Has been cancelled
2025-04-15 12:35:39 +02:00
Simon Diesenreiter
a473eccfda release: version 0.1.8 🚀 2025-04-15 12:35:32 +02:00
Simon Diesenreiter
ce82717d62 fix: additional debug outputs refs NOISSUE 2025-04-15 12:28:34 +02:00
Simon Diesenreiter
3bb39ab17c release: version 0.1.7 🚀
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
CI / tests_linux (9.0.X, ubuntu-latest) (push) Has been cancelled
CI / linter (9.0.X, ubuntu-latest) (push) Has been cancelled
Upload Python Package / Create Release (push) Successful in 22s
Upload Python Package / deploy (push) Successful in 3m28s
2025-04-15 11:59:51 +02:00
Simon Diesenreiter
a66615c6fc fix: more fixes in release logic refs NOISSUE 2025-04-15 11:59:47 +02:00
33 changed files with 1913 additions and 43 deletions

View File

@@ -46,7 +46,7 @@ create_file() {
}
get_commit_range() {
rm $TEMP_FILE_PATH/messages.txt
rm -f $TEMP_FILE_PATH/messages.txt
if [[ $LAST_TAG =~ $PATTERN ]]; then
create_file true
else
@@ -64,16 +64,16 @@ start() {
while read message; do
echo $message
if echo $message | grep -Pq '(feat|style)(\([\w]+\))?!:([a-zA-Z0-9-_!\&\.\%\(\)\=\w\s]+)\s?(,?\s?)((ref(s?):?\s?)(([A-Z0-9]+\-[0-9]+)|(NOISSUE)))'; then
if echo $message | grep -Pq '(feat|style)(\([\w]+\))?!:([a-zA-Z0-9-_!\&\.\%\(\)\=\w\s]+)\s?(,?\s?)((ref(s?):?\s?)(([A-Z0-9]+\-[0-9]+)|(#[0-9]+)|(NOISSUE)))'; then
increment_type="major"
echo "a"
break
elif echo $message | grep -Pq '(feat|style)(\([\w]+\))?:([a-zA-Z0-9-_!\&\.\%\(\)\=\w\s]+)\s?(,?\s?)((ref(s?):?\s?)(([A-Z0-9]+\-[0-9]+)|(NOISSUE)))'; then
elif echo $message | grep -Pq '(feat|style)(\([\w]+\))?:([a-zA-Z0-9-_!\&\.\%\(\)\=\w\s]+)\s?(,?\s?)((ref(s?):?\s?)(([A-Z0-9]+\-[0-9]+)|(#[0-9]+)|(NOISSUE)))'; then
if [ -z "$increment_type" ] || [ "$increment_type" == "patch" ]; then
increment_type="minor"
echo "b"
fi
elif echo $message | grep -Pq '(build|fix|perf|refactor|revert)(\(.+\))?:\s([a-zA-Z0-9-_!\&\.\%\(\)\=\w\s]+)\s?(,?\s?)((ref(s?):?\s?)(([A-Z0-9]+\-[0-9]+)|(NOISSUE)))'; then
elif echo $message | grep -Pq '(build|fix|perf|refactor|revert)(\(.+\))?:\s([a-zA-Z0-9-_!\&\.\%\(\)\=\w\s]+)\s?(,?\s?)((ref(s?):?\s?)(([A-Z0-9]+\-[0-9]+)|(#[0-9]+)|(NOISSUE)))'; then
if [ -z "$increment_type" ]; then
increment_type="patch"
echo "c"
@@ -86,9 +86,9 @@ start() {
echo "New version: $new_version"
gitchangelog | grep -v "[rR]elease:" > HISTORY.md
git add song_of_the_day/VERSION
git add HISTORY.md
echo $new_version > song_of_the_day/VERSION
git add song_of_the_day/VERSION
git commit -m "release: version $new_version 🚀"
echo "creating git tag : $new_version"
git tag $new_version

View File

@@ -1,4 +1,4 @@
name: Upload Python Package
name: Build Docker image
permissions:
contents: write
@@ -38,10 +38,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.X'
- name: Check version match
run: |
REPOSITORY_NAME=$(echo "$GITHUB_REPOSITORY" | awk -F '/' '{print $2}' | tr '-' '_')
@@ -61,5 +57,5 @@ jobs:
run: |
REPOSITORY_OWNER=$(echo "$GITHUB_REPOSITORY" | awk -F '/' '{print $1}' | tr '[:upper:]' '[:lower:]')
REPOSITORY_NAME=$(echo "$GITHUB_REPOSITORY" | awk -F '/' '{print $2}' | tr '-' '_')
docker build -t "git.disi.dev/$REPOSITORY_OWNER/song-of-the-day:$(cat $REPOSITORY_NAME/VERSION)" ./song_of_the_day
docker build -t "git.disi.dev/$REPOSITORY_OWNER/song-of-the-day:$(cat $REPOSITORY_NAME/VERSION)" ./
docker push "git.disi.dev/$REPOSITORY_OWNER/song-of-the-day:$(cat $REPOSITORY_NAME/VERSION)"

View File

@@ -0,0 +1,6 @@
using System;
public class CronTimerEventArgs : EventArgs
{
public DateTime At { get; set; }
}

78
CronTimer/CronTimer.cs Normal file
View File

@@ -0,0 +1,78 @@
using System;
using System.Threading;
using NCrontab;
public class CronTimer
{
public const string UTC = "Etc/UTC";
static readonly TimeSpan InfiniteTimeSpan = TimeSpan.FromMilliseconds(Timeout.Infinite); // net 3.5
readonly CrontabSchedule schedule;
readonly TimeZoneInfo tzi;
readonly string id;
readonly Timer t;
public string tz { get; }
public string Expression { get; }
public event EventHandler<CronTimerEventArgs> OnOccurence;
public DateTime Next { get; private set; }
public CronTimer(string expression, string tz = UTC, bool includingSeconds = false)
{
Expression = expression;
this.tz = tz;
id = TimeZoneConverter.TZConvert.IanaToWindows(tz);
tzi = TimeZoneInfo.FindSystemTimeZoneById(id);
schedule = CrontabSchedule.Parse(expression, new CrontabSchedule.ParseOptions { IncludingSeconds = includingSeconds });
Next = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tzi);
OnOccurence += OnOccurenceScheduleNext;
t = new Timer(s =>
{
var ea = new CronTimerEventArgs
{
At = Next
};
OnOccurence(this, ea);
}, null, InfiniteTimeSpan, InfiniteTimeSpan);
}
void OnOccurenceScheduleNext(object sender, EventArgs e)
{
var delay = CalculateDelay();
//Console.WriteLine($"Next for [{tz} {expression}] in {delay}.");
t.Change(delay, InfiniteTimeSpan);
}
public void Start()
{
var delay = CalculateDelay();
//Console.WriteLine($"Next for [{tz} {expression}] in {delay}.");
t.Change(delay, InfiniteTimeSpan);
}
TimeSpan CalculateDelay()
{
var nowUtc = DateTime.UtcNow;
Next = schedule.GetNextOccurrence(Next);
TimeSpan delay;
if (tz != UTC)
{
var nextUtc = TimeZoneInfo.ConvertTimeToUtc(Next, tzi);
delay = nextUtc - nowUtc;
}
else
{
delay = Next - nowUtc;
}
//Console.WriteLine($"Now: {nowUtc} [utc] {now} [{tz}], Next: {next} [{tz}] {nextUtc} [utc], Delay: {delay}");
if (delay < TimeSpan.Zero) delay = TimeSpan.Zero;
return delay;
}
public void Stop()
{
t.Change(InfiniteTimeSpan, InfiniteTimeSpan);
}
}

View File

@@ -0,0 +1,49 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<LangVersion>latest</LangVersion>
<TargetFrameworks>net461;netstandard2.1</TargetFrameworks>
<PlatformTarget>AnyCPU</PlatformTarget>
</PropertyGroup>
<PropertyGroup>
<!-- AssemblyFileVersionAttribute -->
<FileVersion>2.0.1</FileVersion>
<!-- AssemblyInformationalVersionAttribute -->
<Version>$(FileVersion)</Version>
<!-- AssemblyVersionAttribute -->
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<!-- Nuget -->
<PackageVersion>$(Version)</PackageVersion>
<PackageId>CronTimer</PackageId>
<Company>https://github.com/ramonsmits</Company>
<Authors>ramonsmits</Authors>
<Description>Simple .net Timer that is based on cron expressions with second accuracy to fire timer events to a very specific schedule.</Description>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageReleaseNotes></PackageReleaseNotes>
<PackageProjectUrl>https://github.com/ramonsmits/CronTimer/tree/$(PackageVersion)</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<IncludeSource>True</IncludeSource>
<RepositoryUrl>https://github.com/ramonsmits/CronTimer</RepositoryUrl>
<Copyright>Copyright 2022 (c) Ramon Smits</Copyright>
<PackageTags>cron timer</PackageTags>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ncrontab" Version="3.3.1" />
<PackageReference Include="TimeZoneConverter" Version="6.0.1" />
</ItemGroup>
<!--<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.1'">
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions">
<Version>3.1.4</Version>
</PackageReference>
</ItemGroup>-->
</Project>

36
CronTimer/CronTimer.sln Normal file
View File

@@ -0,0 +1,36 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30717.126
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CronTimer", "CronTimer.csproj", "{FB64C227-8615-4AE1-94E3-F9F9DF192B72}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CCDD0B34-653C-430C-9B17-5129618F8D7D}"
ProjectSection(SolutionItems) = preProject
..\README.md = ..\README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo", "..\Demo\Demo.csproj", "{C2638357-1621-4422-8701-B55BFB37ACCF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FB64C227-8615-4AE1-94E3-F9F9DF192B72}.Release|Any CPU.Build.0 = Release|Any CPU
{C2638357-1621-4422-8701-B55BFB37ACCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C2638357-1621-4422-8701-B55BFB37ACCF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2638357-1621-4422-8701-B55BFB37ACCF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C2638357-1621-4422-8701-B55BFB37ACCF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A639B164-6581-40DF-ADC4-479DAB467CFE}
EndGlobalSection
EndGlobal

View File

@@ -3,13 +3,17 @@ WORKDIR /App
# Copy everything
COPY . ./
RUN apt update && apt install libldap-2.5-0 -y
# Restore as distinct layers
RUN dotnet restore
RUN dotnet restore ./song_of_the_day/song_of_the_day.csproj
# Build and publish a release
RUN dotnet publish -o out
RUN dotnet publish ./song_of_the_day/song_of_the_day.csproj -o out
# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /App
RUN apt update && apt install libldap-2.5-0 -y
COPY --from=build /App/out .
ENTRYPOINT ["dotnet", "song_of_the_day.dll"]

View File

@@ -5,10 +5,275 @@ Changelog
(unreleased)
------------
Fix
~~~
- Exception thrown on LastOrDefault(), refs NOISSUE. [Simon
Diesenreiter]
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: improve commit message generation script, refs NOISSUE. [Simon
Diesenreiter]
- Feat: keep track of user submissions, refs #4. [Simon Diesenreiter]
- Feat: save submission history, refs #7. [Simon Diesenreiter]
0.2.6 (2025-05-22)
------------------
Fix
~~~
- Bugfixes, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
- Reduce number of emitted logs. [simon]
0.2.5 (2025-05-18)
------------------
Fix
~~~
- Install dependencies in runtime container not only build container,
refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.2.4 (2025-05-18)
------------------
Fix
~~~
- Build errors, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.2.3 (2025-05-18)
------------------
Fix
~~~
- Resolve linting errors, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.2.2 (2025-05-18)
------------------
Fix
~~~
- Also send pick suggestion to the group, refs NOISSUE. [Simon
Diesenreiter]
Other
~~~~~
0.2.1 (2025-05-17)
------------------
Fix
~~~
- Fix Docker build, refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.2.0 (2025-05-17)
------------------
- Feat: add user management, refs NOISSUE. [Simon Diesenreiter]
- Feat(auth): initial auth added part 2, refs NOISSUE. [Simon
Diesenreiter]
- Feat(auth): initial auth added, refs NOISSUE. [Simon Diesenreiter]
0.1.21 (2025-04-15)
-------------------
Fix
~~~
- Fix new user saving refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.1.20 (2025-04-15)
-------------------
Fix
~~~
- Remove unnecessary dotnet runtime download in CI job refs NOISSUE.
[Simon Diesenreiter]
Other
~~~~~
0.1.19 (2025-04-15)
-------------------
Fix
~~~
- Improve Docker build refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.1.18 (2025-04-15)
-------------------
Fix
~~~
- Remove broken Crontimer NuGet feed reference refs NOISSUE. [Simon
Diesenreiter]
Other
~~~~~
0.1.17 (2025-04-15)
-------------------
Fix
~~~
- Remove uinnecessary NuGet login refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.1.16 (2025-04-15)
-------------------
Fix
~~~
- Add direct CronTimer reference refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.1.15 (2025-04-15)
-------------------
Fix
~~~
- Login to NuGet feed at build refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.1.14 (2025-04-15)
-------------------
Fix
~~~
- Fix typo refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.1.13 (2025-04-15)
-------------------
Fix
~~~
- Really fix Crontab dependency refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.1.12 (2025-04-15)
-------------------
Fix
~~~
- Hotfixed CronTab dependencyrefs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.1.11 (2025-04-15)
-------------------
Fix
~~~
- More debug outputs refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.1.10 (2025-04-15)
-------------------
Fix
~~~
- Release version jumbled up refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.1.9 (2025-04-15)
------------------
Fix
~~~
- Additional debug outputs refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.1.7 (2025-04-15)
------------------
Fix
~~~
- More fixes in release logic refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.1.6 (2025-04-15)
------------------
Fix
~~~
- Makefile issues on sh refs NOISSUE. [Simon Diesenreiter]
Other
~~~~~
0.1.5 (2025-04-14)
------------------

View File

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

View File

@@ -0,0 +1,17 @@
public class ConfigurationAD
{
public int Port { get; set; } = 389;
public string Zone { get; set; } = string.Empty;
public string Domain { get; set; } = string.Empty;
public string Subdomain { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string LDAPserver { get; set; } = string.Empty;
public string LDAPQueryBase { get; set; } = string.Empty;
public string LDAPUserQueryBase { get; set; } = string.Empty;
public string CrewGroup { get; set; } = string.Empty;
public string ManagerGroup { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,4 @@
public interface IAuthenticationService
{
bool Authenticate(string userName, string password);
}

View File

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

View File

@@ -0,0 +1,58 @@
public class PhoneClaimCodeProviderService
{
private Dictionary<string, string> _phoneClaimCodes;
private Dictionary<string, string> _phoneClaimNumbers;
public PhoneClaimCodeProviderService()
{
_phoneClaimCodes = new Dictionary<string, string>();
_phoneClaimNumbers = new Dictionary<string, string>();
}
private static Random random = new Random();
private static string RandomString(int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[random.Next(s.Length)]).ToArray());
}
public void GenerateClaimCodeForUserAndNumber(string username, string phoneNumber)
{
var generatedCode = string.Empty;
if (IsCodeGeneratedForUser(username))
{
generatedCode = _phoneClaimCodes[username];
}
else
{
generatedCode = RandomString(6);
_phoneClaimCodes[username] = generatedCode;
_phoneClaimNumbers[username] = phoneNumber;
}
SignalIntegration.Instance.SendMessageToUserAsync("Your phone number validation code is: " + generatedCode, phoneNumber);
}
public string ValidateClaimCodeForUser(string code, string username)
{
var result = false;
result = _phoneClaimCodes[username] == code;
if (result)
{
_phoneClaimCodes.Remove(username);
var number = _phoneClaimNumbers[username];
_phoneClaimNumbers.Remove(username);
return number;
}
return string.Empty;
}
public bool IsCodeGeneratedForUser(string username)
{
return _phoneClaimCodes.ContainsKey(username);
}
}

View File

@@ -16,6 +16,21 @@ public class AppConfiguration
this.SignalGroupId = Environment.GetEnvironmentVariable("SIGNAL_GROUP_ID") ?? "group.Wmk1UTVQTnh0Sjd6a0xiOGhnTnMzZlNkc2p2Q3c0SXJiQkU2eDlNU0hyTT0=";
this.WebUIBaseURL = Environment.GetEnvironmentVariable("WEB_BASE_URL") ?? "https://sotd.disi.dev/";
this.UseBotTag = bool.Parse(Environment.GetEnvironmentVariable("USE_BOT_TAG") ?? "true");
this.DaysBetweenRequests = int.Parse(Environment.GetEnvironmentVariable("DAYS_BETWEEN_REQUESTS") ?? "2");
var managersGroupName = Environment.GetEnvironmentVariable("LDAP_ADMINGROUP") ?? "admins";
var userGroupName = Environment.GetEnvironmentVariable("LDAP_USERGROUP") ?? "everybody";
var bindValue = Environment.GetEnvironmentVariable("LDAP_BIND");
this.LDAPConfig = new ConfigurationAD()
{
Username = Environment.GetEnvironmentVariable("LDAP_BIND") ?? "cn=admin,dc=disi,dc=dev",
Password = Environment.GetEnvironmentVariable("LDAP_PASS") ?? "adminPass2022!",
Port = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LDAP_BIND")) ? int.Parse(bindValue ?? "389") : 389,
LDAPserver = Environment.GetEnvironmentVariable("LDAP_URL") ?? "192.168.1.108",
LDAPQueryBase = Environment.GetEnvironmentVariable("LDAP_BASE") ?? "dc=disi,dc=dev",
LDAPUserQueryBase = Environment.GetEnvironmentVariable("LDAP_BASE") ?? "ou=people,dc=disi,dc=dev",
CrewGroup = $"cn={userGroupName},ou=groups,dc=disi,dc=dev",
ManagerGroup = $"cn={managersGroupName},ou=groups,dc=disi,dc=dev"
};
}
public string SignalAPIEndpointUri
@@ -72,4 +87,14 @@ public class AppConfiguration
{
get; private set;
}
public int DaysBetweenRequests
{
get; private set;
}
public ConfigurationAD LDAPConfig
{
get; private set;
}
}

View File

@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
public class AuthController : Controller
{
[HttpPost]
public async Task<IActionResult> Login(string username, string password)
{
var ldapService = HttpContext.RequestServices.GetService<LdapAuthenticationService>();
if (ldapService.Authenticate(username, password))
{
var claims = new[] { new Claim(ClaimTypes.Name, username) };
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(new ClaimsPrincipal(identity));
return RedirectToSamePageIfPossible();
}
ViewBag.Error = "Invalid credentials";
return RedirectToSamePageIfPossible();
}
[HttpPost]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync();
return RedirectToSamePageIfPossible();
}
private IActionResult RedirectToSamePageIfPossible()
{
if (Request.Headers.ContainsKey("Referer"))
{
return Redirect(Request.Headers["Referer"].ToString());
}
return RedirectToPage("/");
}
}

View File

@@ -0,0 +1,142 @@
// <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("20250516161831_UpdateUserModel")]
partial class UpdateUserModel
{
/// <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<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<int?>("SongId")
.HasColumnType("integer");
b.Property<int?>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("SongId");
b.HasIndex("UserId");
b.ToTable("SongSuggestions");
});
modelBuilder.Entity("SuggestionHelper", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Description")
.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.HasKey("UserId");
b.ToTable("Users");
});
modelBuilder.Entity("SongSuggestion", b =>
{
b.HasOne("Song", "Song")
.WithMany()
.HasForeignKey("SongId");
b.HasOne("User", "User")
.WithMany()
.HasForeignKey("UserId");
b.Navigation("Song");
b.Navigation("User");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,265 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace song_of_the_day.DataMigrations
{
/// <inheritdoc />
public partial class UpdateUserModel : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_SongSuggestions_Songs_SongId",
table: "SongSuggestions");
migrationBuilder.DropForeignKey(
name: "FK_SongSuggestions_Users_UserId",
table: "SongSuggestions");
migrationBuilder.AlterColumn<string>(
name: "SignalMemberId",
table: "Users",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "NickName",
table: "Users",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Users",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AddColumn<bool>(
name: "AssociationInProgress",
table: "Users",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "LdapUserName",
table: "Users",
type: "text",
nullable: true);
migrationBuilder.AlterColumn<string>(
name: "Title",
table: "SuggestionHelpers",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Description",
table: "SuggestionHelpers",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<int>(
name: "UserId",
table: "SongSuggestions",
type: "integer",
nullable: true,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AlterColumn<int>(
name: "SongId",
table: "SongSuggestions",
type: "integer",
nullable: true,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AlterColumn<string>(
name: "Url",
table: "Songs",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Songs",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Artist",
table: "Songs",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AddForeignKey(
name: "FK_SongSuggestions_Songs_SongId",
table: "SongSuggestions",
column: "SongId",
principalTable: "Songs",
principalColumn: "SongId");
migrationBuilder.AddForeignKey(
name: "FK_SongSuggestions_Users_UserId",
table: "SongSuggestions",
column: "UserId",
principalTable: "Users",
principalColumn: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_SongSuggestions_Songs_SongId",
table: "SongSuggestions");
migrationBuilder.DropForeignKey(
name: "FK_SongSuggestions_Users_UserId",
table: "SongSuggestions");
migrationBuilder.DropColumn(
name: "AssociationInProgress",
table: "Users");
migrationBuilder.DropColumn(
name: "LdapUserName",
table: "Users");
migrationBuilder.AlterColumn<string>(
name: "SignalMemberId",
table: "Users",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "NickName",
table: "Users",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Users",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Title",
table: "SuggestionHelpers",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Description",
table: "SuggestionHelpers",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "UserId",
table: "SongSuggestions",
type: "integer",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "integer",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "SongId",
table: "SongSuggestions",
type: "integer",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "integer",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Url",
table: "Songs",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Songs",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Artist",
table: "Songs",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AddForeignKey(
name: "FK_SongSuggestions_Songs_SongId",
table: "SongSuggestions",
column: "SongId",
principalTable: "Songs",
principalColumn: "SongId",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_SongSuggestions_Users_UserId",
table: "SongSuggestions",
column: "UserId",
principalTable: "Users",
principalColumn: "UserId",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@@ -0,0 +1,145 @@
// <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("20250524160218_additional data for song submissions")]
partial class additionaldataforsongsubmissions
{
/// <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<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<int?>("SongId")
.HasColumnType("integer");
b.Property<bool>("Submitted")
.HasColumnType("boolean");
b.Property<int?>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("SongId");
b.HasIndex("UserId");
b.ToTable("SongSuggestions");
});
modelBuilder.Entity("SuggestionHelper", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Description")
.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.HasKey("UserId");
b.ToTable("Users");
});
modelBuilder.Entity("SongSuggestion", b =>
{
b.HasOne("Song", "Song")
.WithMany()
.HasForeignKey("SongId");
b.HasOne("User", "User")
.WithMany()
.HasForeignKey("UserId");
b.Navigation("Song");
b.Navigation("User");
});
#pragma warning restore 612, 618
}
}
}

View File

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

View File

@@ -0,0 +1,164 @@
// <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("20250524164159_keep track of users oicked for submission")]
partial class keeptrackofusersoickedforsubmission
{
/// <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<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
}
}
}

View File

@@ -0,0 +1,82 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace song_of_the_day.DataMigrations
{
/// <inheritdoc />
public partial class keeptrackofusersoickedforsubmission : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "Submitted",
table: "SongSuggestions",
newName: "UserHasSubmitted");
migrationBuilder.AddColumn<bool>(
name: "WasChosenForSuggestionThisRound",
table: "Users",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "HasUsedSuggestion",
table: "SongSuggestions",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<int>(
name: "SuggestionHelperId",
table: "SongSuggestions",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex(
name: "IX_SongSuggestions_SuggestionHelperId",
table: "SongSuggestions",
column: "SuggestionHelperId");
migrationBuilder.AddForeignKey(
name: "FK_SongSuggestions_SuggestionHelpers_SuggestionHelperId",
table: "SongSuggestions",
column: "SuggestionHelperId",
principalTable: "SuggestionHelpers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_SongSuggestions_SuggestionHelpers_SuggestionHelperId",
table: "SongSuggestions");
migrationBuilder.DropIndex(
name: "IX_SongSuggestions_SuggestionHelperId",
table: "SongSuggestions");
migrationBuilder.DropColumn(
name: "WasChosenForSuggestionThisRound",
table: "Users");
migrationBuilder.DropColumn(
name: "HasUsedSuggestion",
table: "SongSuggestions");
migrationBuilder.DropColumn(
name: "SuggestionHelperId",
table: "SongSuggestions");
migrationBuilder.RenameColumn(
name: "UserHasSubmitted",
table: "SongSuggestions",
newName: "Submitted");
}
}
}

View File

@@ -30,15 +30,12 @@ namespace song_of_the_day.DataMigrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("SongId"));
b.Property<string>("Artist")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.HasKey("SongId");
@@ -57,16 +54,27 @@ namespace song_of_the_day.DataMigrations
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<int>("SongId")
b.Property<bool>("HasUsedSuggestion")
.HasColumnType("boolean");
b.Property<int?>("SongId")
.HasColumnType("integer");
b.Property<int>("UserId")
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");
@@ -81,11 +89,9 @@ namespace song_of_the_day.DataMigrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
@@ -101,21 +107,27 @@ namespace song_of_the_day.DataMigrations
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")
.IsRequired()
.HasColumnType("text");
b.Property<string>("NickName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SignalMemberId")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("WasChosenForSuggestionThisRound")
.HasColumnType("boolean");
b.HasKey("UserId");
b.ToTable("Users");
@@ -125,18 +137,22 @@ namespace song_of_the_day.DataMigrations
{
b.HasOne("Song", "Song")
.WithMany()
.HasForeignKey("SongId")
.HasForeignKey("SongId");
b.HasOne("SuggestionHelper", "SuggestionHelper")
.WithMany()
.HasForeignKey("SuggestionHelperId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
.HasForeignKey("UserId");
b.Navigation("Song");
b.Navigation("SuggestionHelper");
b.Navigation("User");
});
#pragma warning restore 612, 618

View File

@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
public class SongSuggestion
@@ -7,4 +8,7 @@ public class SongSuggestion
public User? User { get; set; }
public Song? Song { get; set; }
public DateTime Date { get; set; }
public bool UserHasSubmitted { get; set; }
public required SuggestionHelper SuggestionHelper { get; set; }
public bool HasUsedSuggestion { get; set; }
}

View File

@@ -7,4 +7,7 @@ public class User
public string? Name { get; set; }
public string? NickName { get; set; }
public bool IsIntroduced { get; set; }
public bool AssociationInProgress { get; set; }
public string? LdapUserName { get; set; }
public bool WasChosenForSuggestionThisRound { get; set; }
}

View File

@@ -0,0 +1,7 @@
public class LdapUser
{
public string? UserId { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
}

View File

@@ -0,0 +1,125 @@
using System.Collections;
using System.ComponentModel;
using song_of_the_day;
using System.DirectoryServices.Protocols;
using System.Linq;
public class LdapIntegration
{
public static LdapIntegration? Instance;
private readonly string[] attributesToQuery = new string[]
{
"uid",
"givenName",
"sn",
"mail"
};
public LdapIntegration(string uri, int port, string adminBind, string adminPass)
{
this.Uri = uri;
this.Port = port;
this.AdminBind = adminBind;
this.AdminPass = adminPass;
}
private string Uri { get; set; }
private int Port { get; set; }
private string AdminBind { get; set; }
private string AdminPass { get; set; }
public bool TestLogin(string username, string password)
{
try
{
var userList = this.SearchInADAsUser(
AppConfiguration.Instance.LDAPConfig.LDAPQueryBase,
$"(uid={username})",
SearchScope.Subtree,
username,
password);
}
catch (LdapException ex)
{
if (ex.Message.Contains("credential is invalid"))
{
return false;
}
throw;
}
return true;
}
public List<LdapUser> SearchInAD(
string targetOU,
string query,
SearchScope scope
)
{
// search as admin
return this.SearchInADAsUser(targetOU, query, scope, this.AdminBind, this.AdminPass);
}
public List<LdapUser> SearchInADAsUser(
string targetOU,
string query,
SearchScope scope,
string userName,
string userPass
)
{
// on Windows the authentication type is Negotiate, so there is no need to prepend
// AD user login with domain. On other platforms at the moment only
// Basic authentication is supported
var authType = AuthType.Basic;
var user = userName.StartsWith("cn=") || userName.StartsWith("uid=") ? userName : "uid=" + userName + "," + AppConfiguration.Instance.LDAPConfig.LDAPUserQueryBase;
//var connection = new LdapConnection(ldapServer)
var connection = new LdapConnection(
new LdapDirectoryIdentifier(this.Uri, this.Port)
)
{
AuthType = authType,
Credential = new(user, userPass)
};
// the default one is v2 (at least in that version), and it is unknown if v3
// is actually needed, but at least Synology LDAP works only with v3,
// and since our Exchange doesn't complain, let it be v3
connection.SessionOptions.ProtocolVersion = 3;
// this is for connecting via LDAPS (636 port). It should be working,
// according to https://github.com/dotnet/runtime/issues/43890,
// but it doesn't (at least with Synology DSM LDAP), although perhaps
// for a different reason
//connection.SessionOptions.SecureSocketLayer = true;
connection.Bind();
var request = new SearchRequest(targetOU, query, scope, attributesToQuery);
var response = (System.DirectoryServices.Protocols.SearchResponse)connection.SendRequest(request);
var userList = new List<LdapUser>();
foreach (SearchResultEntry result in response.Entries)
{
userList.Add(new LdapUser()
{
UserId = result.Attributes["uid"][0].ToString(),
FirstName = result.Attributes["givenName"][0].ToString(),
LastName = result.Attributes["sn"][0].ToString(),
Email = result.Attributes["mail"][0].ToString(),
});
}
connection.Dispose();
return userList;
}
}

View File

@@ -1,4 +1,15 @@
<!DOCTYPE html>
@{
bool DoesUserHaveClaimedPhoneNumber()
{
using (var dci = DataContext.Instance)
{
var user = dci.Users.Where(u => u.LdapUserName == User.Identity.Name);
return user.Any();
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
@@ -26,9 +37,18 @@
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/SuggestionHelpers">Suggestion Helpers</a>
</li>
@if (this.User.Identity.IsAuthenticated && !DoesUserHaveClaimedPhoneNumber())
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/UnclaimedPhoneNumbers">Unclaimed Phone Numbers</a>
</li>
}
</ul>
</div>
</div>
<div class="container" style="min-height: auto; width: 400px;">
<partial name="_LoginView" />
</div>
</nav>
</header>
<div class="container">

View File

@@ -0,0 +1,36 @@
@using Microsoft.AspNetCore.Authentication
<div class="loginform">
@if (!this.User.Identity.IsAuthenticated)
{
<form method="post" action="Auth/Login">
<div>
<label for="username">Username:</label>
</div>
<div>
<input name="username" type="text" />
</div>
<div>
<label for="password">Password:</label>
</div>
<div>
<input name="password" type="password" />
</div>
<div>
<input name="submit" type="submit" value="Login" />
</div>
</form>
}
else
{
<form method="post" action="Auth/Logout">
<div>
Welcome, @User.Identity.Name!
</div>
<div>
<input name="submit" type="submit" value="Logout" />
</div>
</form>
}
</div>

View File

@@ -0,0 +1,38 @@
@page
@model UnclaimedPhoneNumbersModel
@{
ViewData["Title"] = "Unclaimed Phone Numbers";
var codeService = HttpContext.RequestServices.GetService<PhoneClaimCodeProviderService>();
var codeGenerated = codeService.IsCodeGeneratedForUser(User.Identity.Name);
}
<div class="text-left">
<table>
<tr>
<th>Phone Number</th>
<th>Claim</th>
</tr>
@foreach (var user in @Model.UnclaimedUsers)
{
var phone = user.SignalMemberId; var userId = user.UserId;
<tr>
<td>@phone</td>
<td>
<form method="post">
<input name="userIndex" value="@userId" type="hidden" />
<input type="submit" title="Claim" value="Claim" disabled="@codeGenerated" />
</form>
</td>
</tr>
}
</table>
@if(codeGenerated)
{
<form method="post">
<label for="code">Verification code:</label>
<input type="text" id="code" name="code" />
<input type="submit" title="Verify" value="Verify" asp-page-handler="SubmitCode" />
</form>
}
</div>

View File

@@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.VisualBasic;
namespace sotd.Pages;
public class UnclaimedPhoneNumbersModel : PageModel
{
private readonly ILogger<UserModel> _logger;
public UnclaimedPhoneNumbersModel(ILogger<UserModel> logger)
{
_logger = logger;
}
public int userId { get; set; }
[BindProperty]
public List<User> UnclaimedUsers { get; set; }
public void OnGet()
{
using (var dci = DataContext.Instance)
{
this.UnclaimedUsers = dci.Users.Where(u => string.IsNullOrEmpty(u.LdapUserName)).ToList();
}
}
public void OnPost(int userIndex)
{
using (var dci = DataContext.Instance)
{
var user = dci.Users.Find(userIndex);
var claimCodeService = HttpContext.RequestServices.GetService<PhoneClaimCodeProviderService>();
claimCodeService.GenerateClaimCodeForUserAndNumber(HttpContext.User.Identity.Name, user.SignalMemberId);
this.UnclaimedUsers = dci.Users.Where(u => string.IsNullOrEmpty(u.LdapUserName)).ToList();
}
}
public IActionResult OnPostSubmitCode(string code)
{
var claimCodeService = HttpContext.RequestServices.GetService<PhoneClaimCodeProviderService>();
var validatedNumber = claimCodeService.ValidateClaimCodeForUser(code, HttpContext.User.Identity.Name);
if (!string.IsNullOrEmpty(validatedNumber))
{
using (var dci = DataContext.Instance)
{
var user = dci.Users.Where(u => u.SignalMemberId == validatedNumber).FirstOrDefault();
if (user == default(User))
{
throw new Exception("User with specified phone number not found!");
}
user.LdapUserName = HttpContext.User.Identity.Name;
dci.SaveChanges();
}
}
else
{
throw new Exception("Invalid code provided!");
}
return RedirectToPage("/");
}
}

View File

@@ -2,13 +2,23 @@
using Scalar.AspNetCore;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication;
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);
Console.WriteLine("Setting up user check timer");
var userCheckTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
userCheckTimer.OnOccurence += async (s, ea) =>
{
@@ -17,7 +27,7 @@ userCheckTimer.OnOccurence += async (s, ea) =>
var needsSaving = false;
foreach (var memberId in memberList)
{
var foundUser = dci.Users.Where(u => u.SignalMemberId == memberId).SingleOrDefault();
var foundUser = dci.Users?.Where(u => u.SignalMemberId == memberId).SingleOrDefault();
if (foundUser == null)
{
var newUserContact = await SignalIntegration.Instance.GetContactAsync(memberId);
@@ -29,8 +39,12 @@ userCheckTimer.OnOccurence += async (s, ea) =>
Name = newUserContact.Name,
SignalMemberId = memberId,
NickName = string.Empty,
IsIntroduced = false
IsIntroduced = false,
LdapUserName = string.Empty,
AssociationInProgress = false,
WasChosenForSuggestionThisRound = false,
};
dci.Users?.Add(newUser);
needsSaving = true;
}
}
@@ -43,11 +57,18 @@ userCheckTimer.OnOccurence += async (s, ea) =>
};
userCheckTimer.Start();
Console.WriteLine("Setting up user intro timer");
var userIntroTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeconds: false);
userIntroTimer.OnOccurence += async (s, ea) =>
{
var dci = DataContext.Instance;
var introUsers = dci.Users.Where(u => !u.IsIntroduced);
var introUsers = dci.Users?.Where(u => !u.IsIntroduced);
if (introUsers == null)
{
await dci.DisposeAsync();
return;
}
bool needsSaving = false;
foreach (var user in introUsers)
{
@@ -64,23 +85,126 @@ userIntroTimer.OnOccurence += async (s, ea) =>
};
userIntroTimer.Start();
Console.WriteLine("Setting up pick of the day timer");
var pickOfTheDayTimer = new CronTimer("0 8 * * *", "Europe/Vienna", includingSeconds: false);
pickOfTheDayTimer.OnOccurence += async (s, ea) =>
{
var dci = DataContext.Instance;
var luckyUser = await dci.Users.ElementAtAsync((new Random()).Next(await dci.Users.CountAsync()));
var lastSong = dci.SongSuggestions?.OrderBy(s => s.Id).LastOrDefault();
if (lastSong != null && lastSong.Date >= DateTime.Today.Subtract(TimeSpan.FromDays(AppConfiguration.Instance.DaysBetweenRequests)))
{
Console.WriteLine("Skipping pick of the day today!");
await dci.DisposeAsync();
return;
}
if (dci.Users == null || dci.SuggestionHelpers == null || dci.SongSuggestions == null)
{
Console.WriteLine("Unable to properly initialize DB context!");
await dci.DisposeAsync();
return;
}
var potentialUsers = dci.Users.Where(u => !u.WasChosenForSuggestionThisRound);
if (!potentialUsers.Any())
{
Console.WriteLine("Resetting suggestion count on users before resuming");
await dci.Users.ForEachAsync(u => u.WasChosenForSuggestionThisRound = false);
await dci.SaveChangesAsync();
potentialUsers = dci.Users.Where(u => !u.WasChosenForSuggestionThisRound);
}
Console.WriteLine("Today's pool of pickable users is: " + string.Join(", ", potentialUsers.Select(u => u.Name)));
var luckyUser = potentialUsers.ElementAt((new Random()).Next(potentialUsers.Count()));
if (luckyUser == null)
{
Console.WriteLine("Unable to determine today's lucky user!");
await dci.DisposeAsync();
return;
}
var userName = string.IsNullOrEmpty(luckyUser.NickName) ? luckyUser.Name : luckyUser.NickName;
SignalIntegration.Instance.SendMessageToGroupAsync($"Today's chosen person to share a song is: **{userName}**");
SignalIntegration.Instance.SendMessageToUserAsync($"Congratulations, you have been chosen to share a song today!", luckyUser.SignalMemberId);
var suggestion = await dci.SuggestionHelpers.ElementAtAsync((new Random()).Next(await dci.SuggestionHelpers.CountAsync()));
SignalIntegration.Instance.SendMessageToUserAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*", luckyUser.SignalMemberId);
SignalIntegration.Instance.SendMessageToUserAsync($"For now please just share your suggestion with the group - in the future I might ask you to share directly with me or via the website to help me keep track of past suggestions!", luckyUser.SignalMemberId);
var newSongSuggestion = new SongSuggestion()
{
User = luckyUser,
SuggestionHelper = suggestion,
UserHasSubmitted = false,
HasUsedSuggestion = false,
Date = DateTime.Today
};
if (luckyUser.SignalMemberId is string signalId)
{
await dci.SongSuggestions.AddAsync(newSongSuggestion);
await dci.SaveChangesAsync();
await SignalIntegration.Instance.SendMessageToGroupAsync($"Today's chosen person to share a song is: **{userName}**");
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.Instance.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.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 dci.DisposeAsync();
};
pickOfTheDayTimer.Start();
var startUserAssociationProcess = async (User userToAssociate) =>
{
if (userToAssociate.SignalMemberId is string signalId)
{
await SignalIntegration.Instance.SendMessageToUserAsync($"Hi, I see you are not associated with any website user yet.", signalId);
await SignalIntegration.Instance.SendMessageToUserAsync($"If you haven't yet, please navigate to https://users.disi.dev to create a new account.", 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.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);
}
};
Console.WriteLine("Setting up LdapAssociation timer");
var ldapAssociationTimer = new CronTimer("*/10 * * * *", "Europe/Vienna", includingSeconds: false);
ldapAssociationTimer.OnOccurence += async (s, ea) =>
{
var dci = DataContext.Instance;
if (dci.Users == null)
{
Console.WriteLine("Unable to properly initialize DB context!");
await dci.DisposeAsync();
return;
}
var nonAssociatedUsers = dci.Users.Where(u => string.IsNullOrEmpty(u.LdapUserName) && !u.AssociationInProgress);
var needsSaving = false;
foreach (var user in nonAssociatedUsers)
{
user.AssociationInProgress = true;
await startUserAssociationProcess(user);
user.IsIntroduced = true;
needsSaving = true;
}
if (needsSaving)
{
await dci.SaveChangesAsync();
}
await dci.DisposeAsync();
};
ldapAssociationTimer.Start();
var searchResults = LdapIntegration.Instance.SearchInAD(
AppConfiguration.Instance.LDAPConfig.LDAPQueryBase,
$"(memberOf={AppConfiguration.Instance.LDAPConfig.CrewGroup})",
SearchScope.Subtree
);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddOpenApi();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Auth/Login";
});
builder.Services.AddSingleton<LdapAuthenticationService>();
builder.Services.AddSingleton<PhoneClaimCodeProviderService>();
var app = builder.Build();
@@ -102,5 +226,15 @@ app.UseAuthorization();
app.MapStaticAssets();
app.MapRazorPages()
.WithStaticAssets();
app.MapControllerRoute(
name: "login",
pattern: "{controller=Auth}/{action=Login}"
);
app.MapControllerRoute(
name: "logout",
pattern: "{controller=Auth}/{action=Logout}"
);
app.MapGet("/debug/routes", (IEnumerable<EndpointDataSource> endpointSources) =>
string.Join("\n", endpointSources.SelectMany(source => source.Endpoints)));
app.Run();

View File

@@ -1 +1 @@
0.1.4
0.3.2

View File

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