[backend/core] Finish the polls implementation (ISH-130, ISH-131)
This commit is contained in:
parent
605c636e1e
commit
657bc43761
7 changed files with 166 additions and 38 deletions
|
@ -60,7 +60,6 @@ public class PollController(DatabaseContext db, PollRenderer pollRenderer, PollS
|
||||||
throw GracefulException.BadRequest("This poll is expired");
|
throw GracefulException.BadRequest("This poll is expired");
|
||||||
|
|
||||||
var existingVotes = await db.PollVotes.Where(p => p.User == user && p.Note == note).ToListAsync();
|
var existingVotes = await db.PollVotes.Where(p => p.User == user && p.Note == note).ToListAsync();
|
||||||
List<int> incr = [];
|
|
||||||
List<PollVote> votes = [];
|
List<PollVote> votes = [];
|
||||||
if (!poll.Multiple)
|
if (!poll.Multiple)
|
||||||
{
|
{
|
||||||
|
@ -81,7 +80,6 @@ public class PollController(DatabaseContext db, PollRenderer pollRenderer, PollS
|
||||||
};
|
};
|
||||||
|
|
||||||
await db.AddAsync(vote);
|
await db.AddAsync(vote);
|
||||||
incr.Add(choice);
|
|
||||||
votes.Add(vote);
|
votes.Add(vote);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -101,17 +99,11 @@ public class PollController(DatabaseContext db, PollRenderer pollRenderer, PollS
|
||||||
};
|
};
|
||||||
|
|
||||||
await db.AddAsync(vote);
|
await db.AddAsync(vote);
|
||||||
incr.Add(choice);
|
|
||||||
votes.Add(vote);
|
votes.Add(vote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
foreach (var c in incr)
|
|
||||||
{
|
|
||||||
await db.Database
|
|
||||||
.ExecuteSqlAsync($"""UPDATE "poll" SET "votes"[{c + 1}] = "votes"[{c + 1}] + 1 WHERE "noteId" = {note.Id}""");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var vote in votes)
|
foreach (var vote in votes)
|
||||||
await pollSvc.RegisterPollVote(vote, poll, note);
|
await pollSvc.RegisterPollVote(vote, poll, note);
|
||||||
|
|
|
@ -268,11 +268,17 @@ public class StatusController(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Text == null && request.MediaIds is not { Count: > 0 } && request.Poll == null)
|
if (string.IsNullOrWhiteSpace(request.Text) && request.MediaIds is not { Count: > 0 } && request.Poll == null)
|
||||||
throw GracefulException.BadRequest("Posts must have text, media or poll");
|
throw GracefulException.BadRequest("Posts must have text, media or poll");
|
||||||
|
|
||||||
if (request.Poll != null)
|
var poll = request.Poll != null
|
||||||
throw GracefulException.BadRequest("Polls haven't been implemented yet");
|
? new Poll
|
||||||
|
{
|
||||||
|
Choices = request.Poll.Options,
|
||||||
|
Multiple = request.Poll.Multiple,
|
||||||
|
ExpiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(request.Poll.ExpiresIn),
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
var visibility = StatusEntity.DecodeVisibility(request.Visibility);
|
var visibility = StatusEntity.DecodeVisibility(request.Visibility);
|
||||||
var reply = request.ReplyId != null
|
var reply = request.ReplyId != null
|
||||||
|
@ -302,7 +308,8 @@ public class StatusController(
|
||||||
if (quote != null && quoteUri != null && request.Text != null)
|
if (quote != null && quoteUri != null && request.Text != null)
|
||||||
request.Text = request.Text[..(request.Text.Length - quoteUri.Length - 1)];
|
request.Text = request.Text[..(request.Text.Length - quoteUri.Length - 1)];
|
||||||
|
|
||||||
var note = await noteSvc.CreateNoteAsync(user, visibility, request.Text, request.Cw, reply, quote, attachments);
|
var note = await noteSvc.CreateNoteAsync(user, visibility, request.Text, request.Cw, reply, quote, attachments,
|
||||||
|
poll);
|
||||||
|
|
||||||
if (idempotencyKey != null)
|
if (idempotencyKey != null)
|
||||||
await cache.SetAsync($"idempotency:{idempotencyKey}", note.Id, TimeSpan.FromHours(24));
|
await cache.SetAsync($"idempotency:{idempotencyKey}", note.Id, TimeSpan.FromHours(24));
|
||||||
|
@ -327,7 +334,7 @@ public class StatusController(
|
||||||
throw GracefulException.BadRequest("Posts must have text, media or poll");
|
throw GracefulException.BadRequest("Posts must have text, media or poll");
|
||||||
|
|
||||||
if (request.Poll != null)
|
if (request.Poll != null)
|
||||||
throw GracefulException.BadRequest("Polls haven't been implemented yet");
|
throw GracefulException.BadRequest("Poll edits haven't been implemented yet, please delete & redraft instead");
|
||||||
|
|
||||||
var attachments = request.MediaIds != null
|
var attachments = request.MediaIds != null
|
||||||
? await db.DriveFiles.Where(p => request.MediaIds.Contains(p.Id)).ToListAsync()
|
? await db.DriveFiles.Where(p => request.MediaIds.Contains(p.Id)).ToListAsync()
|
||||||
|
|
|
@ -89,11 +89,33 @@ public class NoteRenderer(IOptions<Config.InstanceSection> config, MfmConverter
|
||||||
var quoteUri = note.IsQuote ? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(config.Value) : null;
|
var quoteUri = note.IsQuote ? note.Renote?.Uri ?? note.Renote?.GetPublicUriOrNull(config.Value) : null;
|
||||||
var text = quoteUri != null ? note.Text + $"\n\nRE: {quoteUri}" : note.Text;
|
var text = quoteUri != null ? note.Text + $"\n\nRE: {quoteUri}" : note.Text;
|
||||||
|
|
||||||
return new ASNote
|
if (note.HasPoll)
|
||||||
|
{
|
||||||
|
var poll = await db.Polls.FirstOrDefaultAsync(p => p.Note == note);
|
||||||
|
if (poll != null)
|
||||||
|
{
|
||||||
|
var closed = poll.ExpiresAt != null && poll.ExpiresAt < DateTime.UtcNow ? poll.ExpiresAt : null;
|
||||||
|
var endTime = poll.ExpiresAt != null && poll.ExpiresAt > DateTime.UtcNow ? poll.ExpiresAt : null;
|
||||||
|
|
||||||
|
var choices = poll.Choices
|
||||||
|
.Select(p => new ASQuestion.ASQuestionOption
|
||||||
|
{
|
||||||
|
Name = p,
|
||||||
|
Replies = new ASCollectionBase
|
||||||
|
{
|
||||||
|
TotalItems =
|
||||||
|
(ulong)poll.Votes[poll.Choices.IndexOf(p)]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var anyOf = poll.Multiple ? choices : null;
|
||||||
|
var oneOf = !poll.Multiple ? choices : null;
|
||||||
|
|
||||||
|
return new ASQuestion
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
AttributedTo = [new ASObjectBase(userId)],
|
AttributedTo = [new ASObjectBase(userId)],
|
||||||
Type = $"{Constants.ActivityStreamsNs}#Note",
|
|
||||||
MkContent = note.Text,
|
MkContent = note.Text,
|
||||||
PublishedAt = note.CreatedAt,
|
PublishedAt = note.CreatedAt,
|
||||||
Sensitive = note.Cw != null,
|
Sensitive = note.Cw != null,
|
||||||
|
@ -104,8 +126,35 @@ public class NoteRenderer(IOptions<Config.InstanceSection> config, MfmConverter
|
||||||
Attachments = attachments,
|
Attachments = attachments,
|
||||||
Content = text != null ? await mfmConverter.ToHtmlAsync(text, mentions, note.UserHost) : null,
|
Content = text != null ? await mfmConverter.ToHtmlAsync(text, mentions, note.UserHost) : null,
|
||||||
Summary = note.Cw,
|
Summary = note.Cw,
|
||||||
Source =
|
Source = text != null
|
||||||
text != null
|
? new ASNoteSource { Content = text, MediaType = "text/x.misskeymarkdown" }
|
||||||
|
: null,
|
||||||
|
MkQuote = quoteUri,
|
||||||
|
QuoteUri = quoteUri,
|
||||||
|
QuoteUrl = quoteUri,
|
||||||
|
EndTime = endTime,
|
||||||
|
Closed = closed,
|
||||||
|
AnyOf = anyOf,
|
||||||
|
OneOf = oneOf
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ASNote
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
AttributedTo = [new ASObjectBase(userId)],
|
||||||
|
MkContent = note.Text,
|
||||||
|
PublishedAt = note.CreatedAt,
|
||||||
|
Sensitive = note.Cw != null,
|
||||||
|
InReplyTo = replyId,
|
||||||
|
Cc = cc,
|
||||||
|
To = to,
|
||||||
|
Tags = tags,
|
||||||
|
Attachments = attachments,
|
||||||
|
Content = text != null ? await mfmConverter.ToHtmlAsync(text, mentions, note.UserHost) : null,
|
||||||
|
Summary = note.Cw,
|
||||||
|
Source = text != null
|
||||||
? new ASNoteSource { Content = text, MediaType = "text/x.misskeymarkdown" }
|
? new ASNoteSource { Content = text, MediaType = "text/x.misskeymarkdown" }
|
||||||
: null,
|
: null,
|
||||||
MkQuote = quoteUri,
|
MkQuote = quoteUri,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Extensions;
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
|
@ -131,6 +132,9 @@ public abstract class BackgroundTaskQueue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery",
|
||||||
|
Justification = "IncludeCommonProperties()")]
|
||||||
|
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataUsage", Justification = "Same as above")]
|
||||||
private static async Task ProcessPollExpiry(
|
private static async Task ProcessPollExpiry(
|
||||||
PollExpiryJob job,
|
PollExpiryJob job,
|
||||||
IServiceProvider scope,
|
IServiceProvider scope,
|
||||||
|
@ -141,11 +145,31 @@ public abstract class BackgroundTaskQueue
|
||||||
var poll = await db.Polls.FirstOrDefaultAsync(p => p.NoteId == job.NoteId, cancellationToken: token);
|
var poll = await db.Polls.FirstOrDefaultAsync(p => p.NoteId == job.NoteId, cancellationToken: token);
|
||||||
if (poll == null) return;
|
if (poll == null) return;
|
||||||
if (poll.ExpiresAt > DateTime.UtcNow + TimeSpan.FromMinutes(5)) return;
|
if (poll.ExpiresAt > DateTime.UtcNow + TimeSpan.FromMinutes(5)) return;
|
||||||
var note = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == poll.NoteId, cancellationToken: token);
|
var note = await db.Notes.IncludeCommonProperties()
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == poll.NoteId, cancellationToken: token);
|
||||||
if (note == null) return;
|
if (note == null) return;
|
||||||
//TODO: try to update poll before completing it
|
|
||||||
var notificationSvc = scope.GetRequiredService<NotificationService>();
|
var notificationSvc = scope.GetRequiredService<NotificationService>();
|
||||||
await notificationSvc.GeneratePollEndedNotifications(note);
|
await notificationSvc.GeneratePollEndedNotifications(note);
|
||||||
|
if (note.User.Host == null)
|
||||||
|
{
|
||||||
|
var voters = await db.PollVotes.Where(p => p.Note == note && p.User.Host != null)
|
||||||
|
.Select(p => p.User)
|
||||||
|
.ToListAsync(cancellationToken: token);
|
||||||
|
|
||||||
|
if (voters.Count == 0) return;
|
||||||
|
|
||||||
|
var activityRenderer = scope.GetRequiredService<ActivityPub.ActivityRenderer>();
|
||||||
|
var userRenderer = scope.GetRequiredService<ActivityPub.UserRenderer>();
|
||||||
|
var noteRenderer = scope.GetRequiredService<ActivityPub.NoteRenderer>();
|
||||||
|
var deliverSvc = scope.GetRequiredService<ActivityPub.ActivityDeliverService>();
|
||||||
|
|
||||||
|
var actor = userRenderer.RenderLite(note.User);
|
||||||
|
var rendered = await noteRenderer.RenderAsync(note);
|
||||||
|
var activity = activityRenderer.RenderUpdate(rendered, actor);
|
||||||
|
|
||||||
|
await deliverSvc.DeliverToAsync(activity, note.User, voters.ToArray());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,8 @@ public class NoteService(
|
||||||
EmojiService emojiSvc,
|
EmojiService emojiSvc,
|
||||||
FollowupTaskService followupTaskSvc,
|
FollowupTaskService followupTaskSvc,
|
||||||
ActivityPub.ObjectResolver objectResolver,
|
ActivityPub.ObjectResolver objectResolver,
|
||||||
QueueService queueSvc
|
QueueService queueSvc,
|
||||||
|
PollService pollSvc
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
private readonly List<string> _resolverHistory = [];
|
private readonly List<string> _resolverHistory = [];
|
||||||
|
@ -49,7 +50,7 @@ public class NoteService(
|
||||||
|
|
||||||
public async Task<Note> CreateNoteAsync(
|
public async Task<Note> CreateNoteAsync(
|
||||||
User user, Note.NoteVisibility visibility, string? text = null, string? cw = null, Note? reply = null,
|
User user, Note.NoteVisibility visibility, string? text = null, string? cw = null, Note? reply = null,
|
||||||
Note? renote = null, IReadOnlyCollection<DriveFile>? attachments = null
|
Note? renote = null, IReadOnlyCollection<DriveFile>? attachments = null, Poll? poll = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (text?.Length > config.Value.CharacterLimit)
|
if (text?.Length > config.Value.CharacterLimit)
|
||||||
|
@ -65,6 +66,8 @@ public class NoteService(
|
||||||
throw GracefulException.BadRequest("Cannot renote or quote a pure renote");
|
throw GracefulException.BadRequest("Cannot renote or quote a pure renote");
|
||||||
if (reply?.IsPureRenote ?? false)
|
if (reply?.IsPureRenote ?? false)
|
||||||
throw GracefulException.BadRequest("Cannot reply to a pure renote");
|
throw GracefulException.BadRequest("Cannot reply to a pure renote");
|
||||||
|
if (poll is { Choices.Count: < 2 })
|
||||||
|
throw GracefulException.BadRequest("Polls must have at least two options");
|
||||||
|
|
||||||
var (mentionedUserIds, mentionedLocalUserIds, mentions, remoteMentions, splitDomainMapping) =
|
var (mentionedUserIds, mentionedLocalUserIds, mentions, remoteMentions, splitDomainMapping) =
|
||||||
await ResolveNoteMentionsAsync(text);
|
await ResolveNoteMentionsAsync(text);
|
||||||
|
@ -107,6 +110,18 @@ public class NoteService(
|
||||||
Tags = tags
|
Tags = tags
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (poll != null)
|
||||||
|
{
|
||||||
|
poll.Note = note;
|
||||||
|
poll.UserId = note.User.Id;
|
||||||
|
poll.UserHost = note.UserHost;
|
||||||
|
poll.Votes = poll.Choices.Select(_ => 0).ToList();
|
||||||
|
poll.NoteVisibility = note.Visibility;
|
||||||
|
await db.AddAsync(poll);
|
||||||
|
note.HasPoll = true;
|
||||||
|
await EnqueuePollExpiryTask(poll);
|
||||||
|
}
|
||||||
|
|
||||||
await UpdateNoteCountersAsync(note, true);
|
await UpdateNoteCountersAsync(note, true);
|
||||||
|
|
||||||
await db.AddAsync(note);
|
await db.AddAsync(note);
|
||||||
|
@ -371,7 +386,7 @@ public class NoteService(
|
||||||
eventSvc.RaiseNoteDeleted(this, hit);
|
eventSvc.RaiseNoteDeleted(this, hit);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Note> ProcessNoteAsync(ASNote note, User actor, User? user = null)
|
public async Task<Note?> ProcessNoteAsync(ASNote note, User actor, User? user = null)
|
||||||
{
|
{
|
||||||
var dbHit = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == note.Id);
|
var dbHit = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Uri == note.Id);
|
||||||
|
|
||||||
|
@ -398,11 +413,46 @@ public class NoteService(
|
||||||
throw GracefulException.UnprocessableEntity("Note.Id schema is invalid");
|
throw GracefulException.UnprocessableEntity("Note.Id schema is invalid");
|
||||||
if (note.Url?.Link != null && !note.Url.Link.StartsWith("https://"))
|
if (note.Url?.Link != null && !note.Url.Link.StartsWith("https://"))
|
||||||
throw GracefulException.UnprocessableEntity("Note.Url schema is invalid");
|
throw GracefulException.UnprocessableEntity("Note.Url schema is invalid");
|
||||||
if (note.PublishedAt is null or { Year: < 2007 } || note.PublishedAt > DateTime.Now + TimeSpan.FromDays(3))
|
|
||||||
throw GracefulException.UnprocessableEntity("Note.PublishedAt is nonsensical");
|
|
||||||
if (actor.IsSuspended)
|
if (actor.IsSuspended)
|
||||||
throw GracefulException.Forbidden("User is suspended");
|
throw GracefulException.Forbidden("User is suspended");
|
||||||
|
|
||||||
|
var reply = note.InReplyTo?.Id != null ? await ResolveNoteAsync(note.InReplyTo.Id, user: user) : null;
|
||||||
|
|
||||||
|
if (reply is { HasPoll: true } && note.Name != null)
|
||||||
|
{
|
||||||
|
if (reply.UserHost != null)
|
||||||
|
throw GracefulException.UnprocessableEntity("Poll vote not destined for this instance");
|
||||||
|
|
||||||
|
var poll = await db.Polls.FirstOrDefaultAsync(p => p.Note == reply) ??
|
||||||
|
throw GracefulException.UnprocessableEntity("Poll does not exist");
|
||||||
|
|
||||||
|
if (poll.Choices.All(p => p != note.Name))
|
||||||
|
throw GracefulException.UnprocessableEntity("Unknown poll option");
|
||||||
|
|
||||||
|
var existingVotes = await db.PollVotes.Where(p => p.User == actor && p.Note == reply).ToListAsync();
|
||||||
|
if (existingVotes.Any(p => poll.Choices[p.Choice] == note.Name))
|
||||||
|
throw GracefulException.UnprocessableEntity("Actor has already voted for this option");
|
||||||
|
if (!poll.Multiple && existingVotes.Count != 0)
|
||||||
|
throw GracefulException.UnprocessableEntity("Actor has already voted in this poll");
|
||||||
|
|
||||||
|
var vote = new PollVote
|
||||||
|
{
|
||||||
|
Id = IdHelpers.GenerateSlowflakeId(),
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
User = actor,
|
||||||
|
Note = reply,
|
||||||
|
Choice = poll.Choices.IndexOf(note.Name)
|
||||||
|
};
|
||||||
|
await db.AddAsync(vote);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
await pollSvc.RegisterPollVote(vote, poll, reply);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.PublishedAt is null or { Year: < 2007 } || note.PublishedAt > DateTime.Now + TimeSpan.FromDays(3))
|
||||||
|
throw GracefulException.UnprocessableEntity("Note.PublishedAt is nonsensical");
|
||||||
|
|
||||||
var (mentionedUserIds, mentionedLocalUserIds, mentions, remoteMentions, splitDomainMapping) =
|
var (mentionedUserIds, mentionedLocalUserIds, mentions, remoteMentions, splitDomainMapping) =
|
||||||
await ResolveNoteMentionsAsync(note);
|
await ResolveNoteMentionsAsync(note);
|
||||||
|
|
||||||
|
@ -422,7 +472,7 @@ public class NoteService(
|
||||||
CreatedAt = createdAt,
|
CreatedAt = createdAt,
|
||||||
UserHost = actor.Host,
|
UserHost = actor.Host,
|
||||||
Visibility = note.GetVisibility(actor),
|
Visibility = note.GetVisibility(actor),
|
||||||
Reply = note.InReplyTo?.Id != null ? await ResolveNoteAsync(note.InReplyTo.Id, user: user) : null,
|
Reply = reply,
|
||||||
Renote = quoteUrl != null ? await ResolveNoteAsync(quoteUrl, user: user) : null
|
Renote = quoteUrl != null ? await ResolveNoteAsync(quoteUrl, user: user) : null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -524,7 +574,7 @@ public class NoteService(
|
||||||
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataUsage",
|
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataUsage",
|
||||||
Justification = "Inspection doesn't understand IncludeCommonProperties()")]
|
Justification = "Inspection doesn't understand IncludeCommonProperties()")]
|
||||||
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery", Justification = "See above")]
|
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery", Justification = "See above")]
|
||||||
public async Task<Note> ProcessNoteUpdateAsync(ASNote note, User actor)
|
public async Task<Note?> ProcessNoteUpdateAsync(ASNote note, User actor)
|
||||||
{
|
{
|
||||||
var dbNote = await db.Notes.IncludeCommonProperties()
|
var dbNote = await db.Notes.IncludeCommonProperties()
|
||||||
.Include(p => p.Poll)
|
.Include(p => p.Poll)
|
||||||
|
@ -1016,7 +1066,7 @@ public class NoteService(
|
||||||
private async Task EnqueuePollExpiryTask(Poll poll)
|
private async Task EnqueuePollExpiryTask(Poll poll)
|
||||||
{
|
{
|
||||||
if (!poll.ExpiresAt.HasValue) return;
|
if (!poll.ExpiresAt.HasValue) return;
|
||||||
var job = new PollExpiryJob { NoteId = poll.NoteId };
|
var job = new PollExpiryJob { NoteId = poll.Note.Id };
|
||||||
await queueSvc.BackgroundTaskQueue.ScheduleAsync(job, poll.ExpiresAt.Value);
|
await queueSvc.BackgroundTaskQueue.ScheduleAsync(job, poll.ExpiresAt.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,11 @@
|
||||||
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Services;
|
namespace Iceshrimp.Backend.Core.Services;
|
||||||
|
|
||||||
public class PollService(
|
public class PollService(
|
||||||
|
DatabaseContext db,
|
||||||
ActivityPub.ActivityRenderer activityRenderer,
|
ActivityPub.ActivityRenderer activityRenderer,
|
||||||
ActivityPub.UserRenderer userRenderer,
|
ActivityPub.UserRenderer userRenderer,
|
||||||
ActivityPub.ActivityDeliverService deliverSvc
|
ActivityPub.ActivityDeliverService deliverSvc
|
||||||
|
@ -10,6 +13,8 @@ public class PollService(
|
||||||
{
|
{
|
||||||
public async Task RegisterPollVote(PollVote pollVote, Poll poll, Note note)
|
public async Task RegisterPollVote(PollVote pollVote, Poll poll, Note note)
|
||||||
{
|
{
|
||||||
|
await db.Database
|
||||||
|
.ExecuteSqlAsync($"""UPDATE "poll" SET "votes"[{pollVote.Choice + 1}] = "votes"[{pollVote.Choice + 1}] + 1 WHERE "noteId" = {note.Id}""");
|
||||||
if (poll.UserHost == null) return;
|
if (poll.UserHost == null) return;
|
||||||
|
|
||||||
var vote = activityRenderer.RenderVote(pollVote, poll, note);
|
var vote = activityRenderer.RenderVote(pollVote, poll, note);
|
||||||
|
|
|
@ -129,7 +129,7 @@ public class JobQueue<T>(
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var timestamp = (long)DateTime.Now.Subtract(DateTime.UnixEpoch).TotalSeconds;
|
var timestamp = (long)DateTime.UtcNow.Subtract(DateTime.UnixEpoch).TotalSeconds;
|
||||||
var res = await _redisDb.SortedSetRangeByScoreAsync("delayed", 0, timestamp, take: 10);
|
var res = await _redisDb.SortedSetRangeByScoreAsync("delayed", 0, timestamp, take: 10);
|
||||||
|
|
||||||
if (res.Length == 0)
|
if (res.Length == 0)
|
||||||
|
@ -233,13 +233,14 @@ public class JobQueue<T>(
|
||||||
await _redisDb.ListRemoveAsync("running", res, 1);
|
await _redisDb.ListRemoveAsync("running", res, 1);
|
||||||
if (targetQueue == "delayed")
|
if (targetQueue == "delayed")
|
||||||
{
|
{
|
||||||
job.DelayedUntil ??= DateTime.Now;
|
job.DelayedUntil = (job.DelayedUntil ?? DateTime.Now).ToLocalTime();
|
||||||
|
|
||||||
var logger = scope.ServiceProvider.GetRequiredService<ILogger<QueueService>>();
|
var logger = scope.ServiceProvider.GetRequiredService<ILogger<QueueService>>();
|
||||||
logger.LogTrace("Job in queue {queue} was delayed to {time} after {duration} ms, has been queued since {time}",
|
logger.LogTrace("Job in queue {queue} was delayed to {time} after {duration} ms, has been queued since {time}",
|
||||||
name, job.DelayedUntil.Value.ToStringIso8601Like(), job.Duration,
|
name, job.DelayedUntil.Value.ToStringIso8601Like(), job.Duration,
|
||||||
job.QueuedAt.ToStringIso8601Like());
|
job.QueuedAt.ToStringIso8601Like());
|
||||||
|
|
||||||
var timestamp = (long)job.DelayedUntil.Value.Subtract(DateTime.UnixEpoch).TotalSeconds;
|
var timestamp = (long)job.DelayedUntil.Value.ToUniversalTime().Subtract(DateTime.UnixEpoch).TotalSeconds;
|
||||||
await _redisDb.SortedSetAddAsync(targetQueue, RedisValue.Unbox(RedisHelpers.Serialize(job)), timestamp);
|
await _redisDb.SortedSetAddAsync(targetQueue, RedisValue.Unbox(RedisHelpers.Serialize(job)), timestamp);
|
||||||
await _subscriber.PublishAsync(_delayedChannel, "");
|
await _subscriber.PublishAsync(_delayedChannel, "");
|
||||||
}
|
}
|
||||||
|
@ -261,8 +262,8 @@ public class JobQueue<T>(
|
||||||
public async Task ScheduleAsync(T job, DateTime triggerAt)
|
public async Task ScheduleAsync(T job, DateTime triggerAt)
|
||||||
{
|
{
|
||||||
job.Status = Job.JobStatus.Delayed;
|
job.Status = Job.JobStatus.Delayed;
|
||||||
job.DelayedUntil = triggerAt;
|
job.DelayedUntil = triggerAt.ToLocalTime();
|
||||||
var timestamp = (long)job.DelayedUntil.Value.Subtract(DateTime.UnixEpoch).TotalSeconds;
|
var timestamp = (long)job.DelayedUntil.Value.ToUniversalTime().Subtract(DateTime.UnixEpoch).TotalSeconds;
|
||||||
await _redisDb.SortedSetAddAsync("delayed", RedisValue.Unbox(RedisHelpers.Serialize(job)), timestamp);
|
await _redisDb.SortedSetAddAsync("delayed", RedisValue.Unbox(RedisHelpers.Serialize(job)), timestamp);
|
||||||
await _subscriber.PublishAsync(_delayedChannel, "");
|
await _subscriber.PublishAsync(_delayedChannel, "");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue