Skip to content

Commit 37f3351

Browse files
authored
Chore/image moderation (#42)
* chore: image moderation * chore: update * chore: image moderation * chore: revert img * chore: code review * chore: fmt
1 parent 7c09ec7 commit 37f3351

File tree

14 files changed

+381
-306
lines changed

14 files changed

+381
-306
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,4 +459,6 @@ src/emailpaywall.client/.svelte-kit
459459

460460
# Include example tfvars files
461461
!**/*.tfvars.example
462-
!**/terraform.tfvars.example
462+
!**/terraform.tfvars.example
463+
464+
**/appsettings.Development.json

deploy/Terraform/container-app.tf

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ resource "azurerm_container_app" "app" {
8282
server = azurerm_container_registry.acr.login_server
8383
identity = azurerm_user_assigned_identity.uami.id
8484
}
85-
85+
8686
depends_on = [azurerm_role_assignment.acr_pull, azurerm_mssql_database.db]
8787

8888
# needed for container app to access other Microsoft Entra protected resources
@@ -179,6 +179,16 @@ resource "azurerm_container_app" "app" {
179179
name = "ASPNETCORE_ENVIRONMENT"
180180
value = "Production"
181181
}
182+
183+
env {
184+
name = "AzureAIFoundry__ContentSafetyKey"
185+
secret_name = "content-safety-key"
186+
}
187+
188+
env {
189+
name = "AzureAIFoundry__ContentSafetyEndpoint"
190+
value = var.content_safety_api
191+
}
182192
}
183193
}
184194

@@ -216,4 +226,9 @@ resource "azurerm_container_app" "app" {
216226
value = var.smtp_password
217227
}
218228

229+
secret {
230+
name = "content-safety-key"
231+
value = var.content_safety_key
232+
}
233+
219234
}

deploy/Terraform/variables.tf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,14 @@ variable "smtp_password" {
3535
type = string
3636
sensitive = true
3737
}
38+
39+
variable "content_safety_key" {
40+
description = "Azure AI foundry content safety key"
41+
type = string
42+
sensitive = true
43+
}
44+
45+
variable "content_safety_api" {
46+
description = "Azure AI foundry content safety api endpoint"
47+
type = string
48+
}

src/Evently.Server/Common/Domains/Interfaces/IObjectStorageService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ public interface IObjectStorageService {
1010
Task<Uri> GetFileUri(string containerName, string fileName);
1111
Task<BinaryData> GetFile(string containerName, string fileName);
1212
Task<bool> IsFileExists(string containerName, string fileName);
13+
Task<bool> PassesContentModeration(BinaryData binaryData);
1314
}

src/Evently.Server/Common/Domains/Models/Settings.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public sealed class Settings {
99
[NotMapped] public AuthSetting Authentication { get; init; } = new();
1010

1111
[NotMapped] public EmailSettings EmailSettings { get; init; } = new();
12+
[NotMapped] public AzureAIFoundry AzureAiFoundry { get; init; } = new();
1213
}
1314

1415
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
@@ -32,4 +33,9 @@ public sealed class OAuthSetting {
3233
public sealed class EmailSettings {
3334
public string ActualFrom { get; init; } = string.Empty;
3435
public string SmtpPassword { get; init; } = string.Empty;
36+
}
37+
38+
public sealed class AzureAIFoundry {
39+
public string ContentSafetyKey { get; init; } = string.Empty;
40+
public string ContentSafetyEndpoint { get; init; } = string.Empty;
3541
}

src/Evently.Server/Common/Extensions/LoggerExtension.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,12 @@ public static partial void LogSuccessEmail(
2828
Message = "Error occurred at {context}: {errorMsg}")]
2929
public static partial void LogErrorContext(
3030
this ILogger logger, string context, string errorMsg);
31+
32+
[LoggerMessage(
33+
EventId = 5,
34+
Level = LogLevel.Error,
35+
Message = "Analyze image failed. Status code: {statusCode}, Error code: {errorCode}, Error message: {errMsg}")]
36+
public static partial void LogContentModerationError(
37+
this ILogger logger, string statusCode, string errorCode, string errMsg);
38+
//
3139
}

src/Evently.Server/Evently.Server.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
</PropertyGroup>
1313

1414
<ItemGroup>
15+
<PackageReference Include="Azure.AI.ContentSafety" Version="1.0.0"/>
1516
<PackageReference Include="Azure.Storage.Blobs" Version="12.25.0"/>
1617
<PackageReference Include="FluentValidation" Version="12.0.0"/>
1718
<PackageReference Include="HtmlRenderer.PdfSharp" Version="1.5.0.6"/>

src/Evently.Server/Features/Files/Services/ObjectStorageService.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using Azure;
2+
using Azure.AI.ContentSafety;
23
using Azure.Storage.Blobs;
34
using Azure.Storage.Blobs.Models;
45
using Evently.Server.Common.Domains.Interfaces;
56
using Evently.Server.Common.Domains.Models;
7+
using Evently.Server.Common.Extensions;
68
using Microsoft.Extensions.Options;
79

810
namespace Evently.Server.Features.Files.Services;
@@ -11,6 +13,9 @@ namespace Evently.Server.Features.Files.Services;
1113
public sealed class ObjectStorageService(IOptions<Settings> settings, ILogger<ObjectStorageService> logger) : IObjectStorageService {
1214
private readonly BlobServiceClient _blobServiceClient =
1315
new(settings.Value.StorageAccount.AzureStorageConnectionString);
16+
private readonly ContentSafetyClient _contentSafetyClient = new(
17+
endpoint: new Uri(settings.Value.AzureAiFoundry.ContentSafetyEndpoint),
18+
credential: new AzureKeyCredential(settings.Value.AzureAiFoundry.ContentSafetyKey));
1419

1520
public async Task<Uri> UploadFile(string containerName, string fileName, BinaryData binaryData,
1621
string mimeType = "application/octet-stream") {
@@ -65,4 +70,23 @@ public async Task<BinaryData> GetFile(string containerName, string fileName) {
6570
BinaryData data = BinaryData.FromBytes(bytes);
6671
return data;
6772
}
73+
74+
public async Task<bool> PassesContentModeration(BinaryData binaryData) {
75+
ContentSafetyImageData image = new(binaryData);
76+
AnalyzeImageOptions request = new(image);
77+
Response<AnalyzeImageResult> response;
78+
try {
79+
response = await _contentSafetyClient.AnalyzeImageAsync(request);
80+
} catch (RequestFailedException ex) {
81+
logger.LogContentModerationError(ex.Status.ToString(), ex.ErrorCode ?? "", ex.Message);
82+
throw;
83+
}
84+
85+
AnalyzeImageResult result = response.Value;
86+
int? score = result.CategoriesAnalysis
87+
.Select(v => v.Severity)
88+
.Aggregate((a, b) => a + b)
89+
?? 0;
90+
return score == 0;
91+
}
6892
}

src/Evently.Server/Features/Gatherings/Controllers/GatheringsController.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ public async Task<ActionResult<Gathering>> CreateGathering([FromForm] GatheringR
6868
}
6969

7070
if (coverImg != null) {
71-
Uri uri = await UploadCoverImage(gatheringReqDto.GatheringId, coverImg: coverImg ?? throw new ArgumentNullException(nameof(coverImg)));
72-
gatheringReqDto = gatheringReqDto with { CoverSrc = uri.AbsoluteUri };
71+
string uri = await UploadCoverImage(gatheringReqDto.GatheringId, coverImg);
72+
gatheringReqDto = gatheringReqDto with { CoverSrc = uri };
7373
}
7474

7575
Gathering gathering = await gatheringService.CreateGathering(gatheringReqDto);
@@ -88,9 +88,8 @@ public async Task<ActionResult> UpdateGathering(long gatheringId, [FromForm] Gat
8888
}
8989

9090
if (coverImg != null) {
91-
Uri uri = await UploadCoverImage(gatheringReqDto.GatheringId, coverImg);
92-
gathering.CoverSrc = uri.AbsoluteUri;
93-
gatheringReqDto = gatheringReqDto with { CoverSrc = uri.AbsoluteUri };
91+
string uri = await UploadCoverImage(gatheringReqDto.GatheringId, coverImg);
92+
gatheringReqDto = gatheringReqDto with { CoverSrc = uri };
9493
}
9594

9695
gathering = await gatheringService.UpdateGathering(gatheringId, gatheringReqDto);
@@ -112,12 +111,17 @@ public async Task<ActionResult<Gathering>> DeleteGathering(long gatheringId) {
112111
return NoContent();
113112
}
114113

115-
private async Task<Uri> UploadCoverImage(long gatheringId, IFormFile coverImg) {
114+
private async Task<string> UploadCoverImage(long gatheringId, IFormFile coverImg) {
116115
string fileName = $"gatherings/{gatheringId}/cover-image{Path.GetExtension(coverImg.FileName)}";
117116
BinaryData binaryData = await coverImg.ToBinaryData();
118-
return await objectStorageService.UploadFile(_containerName,
117+
bool isContentSafe = await objectStorageService.PassesContentModeration(binaryData);
118+
if (!isContentSafe) {
119+
return string.Empty;
120+
}
121+
Uri uri = await objectStorageService.UploadFile(_containerName,
119122
fileName,
120123
binaryData,
121124
mimeType: MimeTypes.GetMimeType(coverImg.FileName));
125+
return uri.AbsoluteUri;
122126
}
123127
}

src/Evently.Server/appsettings.Development.json

Lines changed: 0 additions & 26 deletions
This file was deleted.

0 commit comments

Comments
 (0)