Skip to content

Commit

Permalink
Add support for sending Watched Tags Emails
Browse files Browse the repository at this point in the history
  • Loading branch information
mythz committed May 11, 2024
1 parent 5c2b612 commit c563e08
Show file tree
Hide file tree
Showing 16 changed files with 448 additions and 55 deletions.
191 changes: 191 additions & 0 deletions MyApp.ServiceInterface/App/AppDbPeriodicTasksCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
using System.Data;
using CreatorKit.ServiceInterface;
using CreatorKit.ServiceModel;
using CreatorKit.ServiceModel.Types;
using MyApp.ServiceModel;
using ServiceStack;
using ServiceStack.Messaging;
using ServiceStack.OrmLite;
using Microsoft.Extensions.Logging;
using MyApp.Data;
using ServiceStack.Data;
using ServiceStack.Script;

namespace MyApp.ServiceInterface.App;

public class AppDbPeriodicTasksCommand(ILogger<AppDbPeriodicTasksCommand> log,
AppConfig appConfig, IDbConnectionFactory dbFactory, IMessageProducer mq, EmailRenderer renderer)
: IAsyncCommand<PeriodicTasks>
{
public async Task ExecuteAsync(PeriodicTasks request)
{
log.LogInformation("Executing {Type} {PeriodicFrequency} PeriodicTasks...", GetType().Name,
request.PeriodicFrequency);

await SendWatchedTagEmails();
}

private async Task SendWatchedTagEmails()
{
var yesterday = DateTime.UtcNow.AddDays(-1).Date;
var day = yesterday.ToString("yyyy-MM-dd");
using var db = await dbFactory.OpenDbConnectionAsync();
if (await db.ExistsAsync(db.From<WatchPostMail>().Where(x => x.Date == day)))
return;

var newPosts = await db.SelectAsync(db.From<Post>().Where(x =>
x.CreationDate >= yesterday && x.CreationDate < yesterday.AddDays(1)));
if (newPosts.Count == 0)
{
log.LogInformation("No new posts found for {Date}", day);
return;
}

var tagGroups = new Dictionary<string, List<Post>>();
foreach (var post in newPosts)
{
foreach (var tag in post.Tags)
{
if (!tagGroups.TryGetValue(tag, out var posts))
tagGroups[tag] = posts = new List<Post>();
posts.Add(post);
}
}

var uniqueTags = tagGroups.Keys.ToSet();
var watchTags = await db.SelectAsync(db.From<WatchTag>().Where(x => uniqueTags.Contains(x.Tag)));
if (watchTags.Count == 0)
{
log.LogInformation("No Tag Watchers found for {Date}", day);
return;
}

var uniqueUserNames = watchTags.Select(x => x.UserName).ToSet();
var users = await db.SelectAsync<ApplicationUser>(x => uniqueUserNames.Contains(x.UserName!));

using var dbCreatorKit = await dbFactory.OpenDbConnectionAsync(Databases.CreatorKit);

var mailRuns = 0;
var orderedTags = uniqueTags.OrderBy(x => x).ToList();
foreach (var tag in orderedTags)
{
if (!tagGroups.TryGetValue(tag, out var posts))
continue;

var tagWatchers = watchTags.Where(x => x.Tag == tag).ToList();
if (tagWatchers.Count == 0)
continue;

var postIds = posts.ConvertAll(x => x.Id);

var userNames = tagWatchers.Map(x => x.UserName);
var watchPostMail = new WatchPostMail
{
Date = day,
Tag = tag,
UserNames = userNames,
PostIds = postIds,
CreatedDate = DateTime.UtcNow,
};
watchPostMail.Id = (int)await db.InsertAsync(watchPostMail, selectIdentity: true);
log.LogInformation(
"Created {Day} WatchPostMail {Id} for {Tag} with posts:{PostIds} for users:{UserNames}",
day, watchPostMail.Id, tag, postIds.Join(","), userNames.Join(","));

var layout = "tags";
var template = "tagged-questions";
var context = renderer.CreateMailContext(layout: layout, page: template);
var monthDay = yesterday.ToString("MMMM dd");
var args = new Dictionary<string, object>
{
["tag"] = tag,
["date"] = monthDay,
["posts"] = posts,
};
var html = await new PageResult(context.GetPage("content"))
{
Layout = "layout",
Args = args,
}.RenderToStringAsync();

args.Remove("model");

var externalRef = $"{nameof(WatchPostMail)}:{watchPostMail.Id}";
var mailRun = new MailRun
{
MailingList = MailingList.WatchedTags,
CreatedDate = DateTime.UtcNow,
Layout = layout,
Generator = nameof(RenderTagQuestionsEmail),
Template = template,
GeneratorArgs = args,
ExternalRef = externalRef,
};
mailRun.Id = (int)await dbCreatorKit.InsertAsync(mailRun, selectIdentity: true);
mailRuns++;

await db.UpdateOnlyAsync(() => new WatchPostMail
{
MailRunId = mailRun.Id,
}, where: x => x.Id == watchPostMail.Id);

var emails = 0;
foreach (var tagWatcher in tagWatchers)
{
var user = users.Find(x => x.UserName == tagWatcher.UserName);
if (user == null)
{
log.LogInformation("User {UserName} not found for WatchTag {Tag}",
tagWatcher.UserName, tagWatcher.Tag);
continue;
}

var message = new EmailMessage
{
To = [new() { Email = user.Email!, Name = user.UserName! }],
Subject = $"New {tag} questions for {monthDay} - pvq.app",
BodyHtml = html,
};

var contact = await dbCreatorKit.GetOrCreateContact(user);

var mailMessage = new MailMessageRun
{
MailRunId = mailRun.Id,
ContactId = contact.Id,
Contact = contact,
Renderer = nameof(RenderTagQuestionsEmail),
RendererArgs = args,
Message = message,
CreatedDate = DateTime.UtcNow,
ExternalRef = externalRef,
};
mailMessage.Id = (int)await dbCreatorKit.InsertAsync(mailMessage, selectIdentity: true);
emails++;
}

var generatedDate = DateTime.UtcNow;
await db.UpdateOnlyAsync(() => new WatchPostMail
{
GeneratedDate = generatedDate,
}, where: x => x.Id == watchPostMail.Id);
await dbCreatorKit.UpdateOnlyAsync(() => new MailRun
{
EmailsCount = emails,
GeneratedDate = generatedDate,
}, where: x => x.Id == mailRun.Id);

log.LogInformation("Generated {Count} in {Day} MailRun {Id} for {Tag}",
emails, day, mailRun.Id, tag);

mq.Publish(new CreatorKitTasks
{
SendMailRun = new() {
Id = mailRun.Id
}
});
}

log.LogInformation("Generated {Count} MailRuns for {Day}", mailRuns, day);
}
}
43 changes: 2 additions & 41 deletions MyApp.ServiceInterface/CreatorKit/CreatorKitTasksServices.cs
Original file line number Diff line number Diff line change
@@ -1,50 +1,11 @@
using ServiceStack;
using CreatorKit.ServiceModel.Types;
using CreatorKit.ServiceModel;
using ServiceStack;
using MyApp.Data;
using MyApp.ServiceInterface;
using MyApp.ServiceModel;
using ServiceStack.Data;
using ServiceStack.OrmLite;

namespace CreatorKit.ServiceInterface;

public class CreatorKitTasksServices : Service
{
public object Any(CreatorKitTasks request) => Request.ExecuteCommandsAsync(request);
}

public class SendMessagesCommand(IDbConnectionFactory dbFactory, EmailProvider emailProvider) : IAsyncCommand<SendMailMessages>
{
public async Task ExecuteAsync(SendMailMessages request)
{
using var db = await dbFactory.OpenDbConnectionAsync(Databases.CreatorKit);

foreach (var msg in request.Messages.Safe())
{
if (msg.CompletedDate != null)
throw new Exception($"Message {msg.Id} has already been sent");

msg.Id = (int) await db.InsertAsync(msg, selectIdentity:true);

// ensure message is only sent once
if (await db.UpdateOnlyAsync(() => new MailMessage { StartedDate = DateTime.UtcNow, Draft = false },
where: x => x.Id == msg.Id && (x.StartedDate == null)) == 1)
{
try
{
emailProvider.Send(msg.Message);
}
catch (Exception e)
{
var error = e.ToResponseStatus();
await db.UpdateOnlyAsync(() => new MailMessage { Error = error },
where: x => x.Id == msg.Id);
throw;
}

await db.UpdateOnlyAsync(() => new MailMessage { CompletedDate = DateTime.UtcNow },
where: x => x.Id == msg.Id);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ public async Task<object> Any(RenderCustomHtml request)

public async Task<object> Any(RenderTagQuestionsEmail request)
{
OrmLiteUtils.PrintSql();
var context = renderer.CreateMailContext(layout:"tags", page:"tagged-questions");

var posts = await Db.SelectAsync(Db.From<Post>()
Expand Down
78 changes: 78 additions & 0 deletions MyApp.ServiceInterface/CreatorKit/SendMailRunCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using CreatorKit.ServiceInterface;
using CreatorKit.ServiceModel;
using CreatorKit.ServiceModel.Types;
using Microsoft.Extensions.Logging;
using MyApp.ServiceModel;
using ServiceStack;
using ServiceStack.Data;
using ServiceStack.OrmLite;

namespace MyApp.ServiceInterface.CreatorKit;

public class SendMailRunCommand(
ILogger<SendMailRunCommand> log,
IDbConnectionFactory dbFactory,
EmailProvider emailProvider)
: IAsyncCommand<SendMailRun>
{
public async Task ExecuteAsync(SendMailRun request)
{
using var db = HostContext.AppHost.GetDbConnection(Databases.CreatorKit);
var msgIdsToSend = await db.ColumnAsync<int>(db.From<MailMessageRun>()
.Where(x => x.MailRunId == request.Id && x.CompletedDate == null && x.StartedDate == null)
.Select(x => x.Id));

if (msgIdsToSend.Count == 0)
{
log.LogInformation("No remaining unsent Messages to send for MailRun {Id}", request.Id);
return;
}

await db.UpdateOnlyAsync(() => new MailRun { SentDate = DateTime.UtcNow },
where:x => x.Id == request.Id && x.SentDate == null);

log.LogInformation("Sending {Count} Messages for MailRun {Id}", msgIdsToSend.Count, request.Id);

foreach (var msgId in msgIdsToSend)
{
try
{
var msg = await db.SingleByIdAsync<MailMessageRun>(msgId);
if (msg.CompletedDate != null)
{
log.LogWarning("MailMessageRun {Id} has already been sent", msg.Id);
continue;
}

// ensure message is only sent once
if (await db.UpdateOnlyAsync(() => new MailMessageRun { StartedDate = DateTime.UtcNow },
where: x => x.Id == request.Id && x.StartedDate == null) == 1)
{
try
{
emailProvider.Send(msg.Message);

await db.UpdateOnlyAsync(() => new MailMessageRun { CompletedDate = DateTime.UtcNow },
where: x => x.Id == request.Id);
}
catch (Exception e)
{
var error = e.ToResponseStatus();
await db.UpdateOnlyAsync(() => new MailMessageRun { Error = error },
where: x => x.Id == request.Id);
}
}
}
catch (Exception e)
{
var error = e.ToResponseStatus();
await db.UpdateOnlyAsync(() => new MailMessageRun
{
Error = error
}, where: x => x.Id == msgId);

log.LogError(e, "Error sending MailMessageRun {Id}: {Message}", msgId, e.Message);
}
}
}
}
44 changes: 44 additions & 0 deletions MyApp.ServiceInterface/CreatorKit/SendMessagesCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using CreatorKit.ServiceModel.Types;
using MyApp.Data;
using MyApp.ServiceModel;
using ServiceStack;
using ServiceStack.Data;
using ServiceStack.OrmLite;

namespace CreatorKit.ServiceInterface;

public class SendMessagesCommand(IDbConnectionFactory dbFactory, EmailProvider emailProvider) : IAsyncCommand<SendMailMessages>
{
public async Task ExecuteAsync(SendMailMessages request)
{
using var db = await dbFactory.OpenDbConnectionAsync(Databases.CreatorKit);

foreach (var msg in request.Messages.Safe())
{
if (msg.CompletedDate != null)
throw new Exception($"Message {msg.Id} has already been sent");

msg.Id = (int) await db.InsertAsync(msg, selectIdentity:true);

// ensure message is only sent once
if (await db.UpdateOnlyAsync(() => new MailMessage { StartedDate = DateTime.UtcNow, Draft = false },
where: x => x.Id == msg.Id && (x.StartedDate == null)) == 1)
{
try
{
emailProvider.Send(msg.Message);
}
catch (Exception e)
{
var error = e.ToResponseStatus();
await db.UpdateOnlyAsync(() => new MailMessage { Error = error },
where: x => x.Id == msg.Id);
throw;
}

await db.UpdateOnlyAsync(() => new MailMessage { CompletedDate = DateTime.UtcNow },
where: x => x.Id == msg.Id);
}
}
}
}
7 changes: 6 additions & 1 deletion MyApp.ServiceInterface/Data/CreatorKitTasks.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using CreatorKit.ServiceInterface;
using CreatorKit.ServiceModel;
using CreatorKit.ServiceModel.Types;
using MyApp.ServiceInterface.CreatorKit;
using ServiceStack;

namespace MyApp.Data;
Expand All @@ -14,4 +16,7 @@ public class CreatorKitTasks
{
[Command<SendMessagesCommand>]
public SendMailMessages? SendMessages { get; set; }
}

[Command<SendMailRunCommand>]
public SendMailRun? SendMailRun { get; set; }
}
Loading

0 comments on commit c563e08

Please sign in to comment.