Skip to content

Commit

Permalink
Merge pull request #1 from Hekku2/feat/randomization
Browse files Browse the repository at this point in the history
Improve image randomization
  • Loading branch information
Hekku2 authored Jun 21, 2024
2 parents ea5a42e + c052f5b commit f140bca
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 2 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ Storage.

Currently this is designed to work with one server and one channel.

## How does it work?

In short, software periodically selects random image from image storage and sends it to discord channel.

In depth
1. Image sending is periodically triggered by Function App TimerTrigger.
1. When Image sending is triggered, software selects a random image from index. Selection is not really random, it prefers less posted images. See [RandomizationService](src/Common/RandomizationService/RandomizationService.cs) for implementation details.
* If there is no index, software builds an index of images available in source.
1. After image is selected, software downloads the image from storage and sends the image to the chosen Discord channel.
* If Imageis removed, error is logged and image is not sent to channel.

Indexing
* Indexing is performed automatically if index doesn't exist.
* Image index can be regenerated by calling the related function.
* Currently changes in storage don't trigger image index regeneration.
* The index contains data of the images and how often those have been posted and other similar metadata.
* Images can also be ignored. Ignored images are not selected by randomization logic.

## Deployment and running

Following section describes what needs be done to deploy and run this
Expand Down
4 changes: 4 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# TODO
* Validate that Discord configuration is given
* Validate image source configuration
* Validate index configuration
* Add CI
* Document architecture
* Add support for Managed identity authentication for storage access
9 changes: 9 additions & 0 deletions discord-image-poster.sln
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FunctionApp.Isolated", "src
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "src\Common\Common.csproj", "{35A7D6EB-EB36-4D03-ADB5-57747A4AB9CB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{36ABE9BE-AC32-4AC3-B1A2-37BD45F070DC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommonTests", "tests\CommonTests\CommonTests.csproj", "{BC112AA9-6D19-4429-8286-8DE23A069700}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -32,10 +36,15 @@ Global
{35A7D6EB-EB36-4D03-ADB5-57747A4AB9CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35A7D6EB-EB36-4D03-ADB5-57747A4AB9CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{35A7D6EB-EB36-4D03-ADB5-57747A4AB9CB}.Release|Any CPU.Build.0 = Release|Any CPU
{BC112AA9-6D19-4429-8286-8DE23A069700}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BC112AA9-6D19-4429-8286-8DE23A069700}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC112AA9-6D19-4429-8286-8DE23A069700}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC112AA9-6D19-4429-8286-8DE23A069700}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{B45B29EC-4A28-4AC2-B2BD-50139AB865AE} = {5A26BEBA-EBEE-4505-9D99-198B8A2B9662}
{5772B8E6-5101-40CB-862D-F4C4626EEB0D} = {5A26BEBA-EBEE-4505-9D99-198B8A2B9662}
{35A7D6EB-EB36-4D03-ADB5-57747A4AB9CB} = {5A26BEBA-EBEE-4505-9D99-198B8A2B9662}
{BC112AA9-6D19-4429-8286-8DE23A069700} = {36ABE9BE-AC32-4AC3-B1A2-37BD45F070DC}
EndGlobalSection
EndGlobal
16 changes: 16 additions & 0 deletions src/Common/RandomizationService/IRandomizationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using DiscordImagePoster.Common.IndexService;

namespace DiscordImagePoster.Common.RandomizationService;

/// <summary>
/// Randomization service proivdes randomization logic for ImageIndex
/// </summary>
public interface IRandomizationService
{
/// <summary>
/// Returns random image from given image index.
/// </summary>
/// <param name="imageIndex">Index</param>
/// <returns>Random image from index or null if there are no valid images available.</returns>
ImageIndexMetadata? GetRandomImage(ImageIndex imageIndex);
}
32 changes: 32 additions & 0 deletions src/Common/RandomizationService/RandomizationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using DiscordImagePoster.Common.IndexService;
using Microsoft.Extensions.Logging;

namespace DiscordImagePoster.Common.RandomizationService;

public class RandomizationService : IRandomizationService
{
private readonly ILogger<RandomizationService> _logger;

public RandomizationService(ILogger<RandomizationService> logger)
{
_logger = logger;
}

public ImageIndexMetadata? GetRandomImage(ImageIndex imageIndex)
{
_logger.LogTrace("Getting random image from image index.");

var allowedImages = imageIndex.Images.Where(image => !image.Ignore);
if (!allowedImages.Any())
{
_logger.LogTrace("No allowed images found in index.");
return null;
}
var minimunPosts = allowedImages.Min(image => image.TimesPosted);
_logger.LogTrace("Minimum posts for allowed images: {MinimunPosts}", minimunPosts);
return allowedImages
.Where(image => image.TimesPosted == minimunPosts)
.OrderBy(x => Guid.NewGuid())
.FirstOrDefault();
}
}
9 changes: 7 additions & 2 deletions src/FunctionApp.Isolated/Functions/ImageSendFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using DiscordImagePoster.Common.BlobStorageImageService;
using DiscordImagePoster.Common.Discord;
using DiscordImagePoster.Common.IndexService;
using DiscordImagePoster.Common.RandomizationService;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
Expand All @@ -16,19 +17,23 @@ public class ImageSendFunction
private readonly IDiscordImagePoster _discordImagePoster;
private readonly IBlobStorageImageService _imageService;
private readonly IIndexService _indexService;
private readonly IRandomizationService _randomizationService;

public ImageSendFunction(
ILogger<ImageSendFunction> logger,
IOptions<FeatureSettings> featureSettings,
IDiscordImagePoster discordImagePoster,
IBlobStorageImageService imageService,
IIndexService indexService)
IIndexService indexService,
IRandomizationService randomizationService
)
{
_logger = logger;
_featureSettings = featureSettings.Value;
_discordImagePoster = discordImagePoster;
_imageService = imageService;
_indexService = indexService;
_randomizationService = randomizationService;
}

[Function("SendImage")]
Expand Down Expand Up @@ -62,7 +67,7 @@ public async Task TriggerTimerSendRandomImage([TimerTrigger("0 0 */4 * * *")] Ti
private async Task SendRandomImage()
{
var index = await _indexService.GetIndexOrCreateNew();
var randomImage = index.Images.OrderBy(x => Guid.NewGuid()).FirstOrDefault();
var randomImage = _randomizationService.GetRandomImage(index);

if (randomImage is null)
{
Expand Down
2 changes: 2 additions & 0 deletions src/FunctionApp.Isolated/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using DiscordImagePoster.Common.BlobStorageImageService;
using DiscordImagePoster.Common.Discord;
using DiscordImagePoster.Common.IndexService;
using DiscordImagePoster.Common.RandomizationService;
using DiscordImagePoster.FunctionApp.Isolated;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
Expand Down Expand Up @@ -30,6 +31,7 @@
services.AddTransient<IBlobStorageImageService, BlobStorageImageService>();
services.AddTransient<IIndexStorageService, BlobStorageIndexStorageService>();
services.AddTransient<IIndexService, IndexService>();
services.AddTransient<IRandomizationService, RandomizationService>();
services.AddKeyedTransient(KeyedServiceConstants.ImageBlobContainerClient, (services, _) =>
{
Expand Down
27 changes: 27 additions & 0 deletions tests/CommonTests/CommonTests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Tests.Common</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.2.0" />
<PackageReference Include="coverlet.collector" Version="6.0.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Common\Common.csproj" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions tests/CommonTests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using NUnit.Framework;
123 changes: 123 additions & 0 deletions tests/CommonTests/RandomizationService/RandomizationServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using Castle.Core.Logging;
using DiscordImagePoster.Common.IndexService;
using DiscordImagePoster.Common.RandomizationService;
using FluentAssertions;
using Microsoft.Extensions.Logging;
using NSubstitute;

namespace Tests.Common;

public class RandomizationServiceTests
{
private RandomizationService _randomizationService;

[SetUp]
public void Setup()
{
ILogger<RandomizationService> mockLogger = Substitute.For<ILogger<RandomizationService>>();

_randomizationService = new RandomizationService(mockLogger);

}

[Test]
public void GetRandomImage_ForEmptyCollection_ReturnsNull()
{
var imageIndex = new ImageIndex()
{
RefreshedAt = DateTimeOffset.UtcNow,
Images = new List<ImageIndexMetadata>()
};

var result = _randomizationService.GetRandomImage(imageIndex);

result.Should().BeNull();
}


[Test]
public void GetRandomImage_ForIgnoredImages_ReturnsNull()
{
var imageIndex = new ImageIndex()
{
RefreshedAt = DateTimeOffset.UtcNow,
Images = new List<ImageIndexMetadata>
{
new ImageIndexMetadata
{
Ignore = true,
AddedAt = DateTime.UtcNow,
Description = null,
LastPostedAt = null,
TimesPosted = 0,
Name = "Test Image"
}
}
};

var result = _randomizationService.GetRandomImage(imageIndex);

result.Should().BeNull();
}

[Test]
public void GetRandomImage_ForSomeImage_ReturnsImage()
{
var imageIndex = new ImageIndex()
{
RefreshedAt = DateTimeOffset.UtcNow,
Images = new List<ImageIndexMetadata>
{
new ImageIndexMetadata
{
Ignore = false,
AddedAt = DateTime.UtcNow,
Description = null,
LastPostedAt = null,
TimesPosted = 27,
Name = "Test Image"
}
}
};

var result = _randomizationService.GetRandomImage(imageIndex);

result.Should().BeEquivalentTo(imageIndex.Images.First());
}

[Test]
public void GetRandomImage_TwoImages_ReturnsLessPostedImage()
{
var imageIndex = new ImageIndex()
{
RefreshedAt = DateTimeOffset.UtcNow,
Images = new List<ImageIndexMetadata>
{
new ImageIndexMetadata
{
Ignore = false,
AddedAt = DateTime.UtcNow,
Description = null,
LastPostedAt = null,
TimesPosted = 27,
Name = "Test Image 1"
},
new ImageIndexMetadata
{
Ignore = false,
AddedAt = DateTime.UtcNow,
Description = null,
LastPostedAt = null,
TimesPosted = 25,
Name = "Test Image 2"
}
}
};

for (var i = 0; i < 10; i++)
{
var result = _randomizationService.GetRandomImage(imageIndex);
result.Should().BeEquivalentTo(imageIndex.Images.Last());
}
}
}

0 comments on commit f140bca

Please sign in to comment.