Files
IKAR/tool/add_scheduling.py

66 lines
6.2 KiB
Python

from pathlib import Path
root = Path(__file__).resolve().parents[1]
server = root / 'ikar_server/src/Ikar.Server'
def edit(path, before, after, count=1):
p = server / path
text = p.read_text(encoding='utf-8-sig')
assert text.count(before) == count, (path, before, text.count(before))
p.write_text(text.replace(before, after), encoding='utf-8')
edit('Data/Entities/Entities.cs', 'public sealed class Message\n{', 'public sealed class Message\n{\n public long? ScheduledForUnixMs { get; set; }')
edit('Infrastructure/Storage/DatabaseSchemaBootstrapper.cs', 'await EnsureColumnAsync("Messages", "EditedAt",', 'await EnsureColumnAsync("Messages", "ScheduledForUnixMs", "INTEGER NULL", cancellationToken);\n await EnsureColumnAsync("Messages", "EditedAt",')
edit('Program.cs', 'builder.Services.AddHostedService<PushDispatchBackgroundService>();', 'builder.Services.AddHostedService<PushDispatchBackgroundService>();\nbuilder.Services.AddHostedService<IkarScheduledMessageService>();')
path = 'Controllers/QlyraCompatibilityController.cs'
edit(path, ''' var visibleMessages = chat.Messages
.Where(message => message.DeletedAt is null && !hiddenMessageIds.Contains(message.Id));''', ''' var delayed = ReadString(payload, "itemType") == "DELAYED";
var visibleMessages = chat.Messages
.Where(message => message.DeletedAt is null && !hiddenMessageIds.Contains(message.Id))
.Where(message => delayed ? message.ScheduledForUnixMs != null && message.SenderId == user.Id : message.ScheduledForUnixMs == null);
if (delayed) anchor = DateTimeOffset.MaxValue;''')
edit(path, ''' chat.LastActivityAt = message.SentAt;
if (dbContext.Entry(message).State == EntityState.Detached) dbContext.Messages.Add(message);''', ''' var scheduled = ReadScheduledTime(payload, "message");
if (scheduled is not null && (scheduled <= DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() || scheduled > DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds()))
return BadRequest(QlyraCompatibilityResponse.Error(opcode, "SCHEDULE_INVALID", "Укажите время отправки в пределах года."));
message.ScheduledForUnixMs = scheduled;
if (scheduled is null) chat.LastActivityAt = message.SentAt;
if (dbContext.Entry(message).State == EntityState.Detached) dbContext.Messages.Add(message);
if (scheduled is null) pushDispatchQueue?.StageMessages(dbContext, message);''')
edit(path, ''' chat.Members.Select(member => member.UserId),
128,
new { chatId = clientChatId, message = mappedMessage, unread = 0 },
cancellationToken);
return Ok(QlyraCompatibilityResponse.Ok(opcode, new { message = mappedMessage }));''', ''' message.ScheduledForUnixMs is not null ? [user.Id] : chat.Members.Select(member => member.UserId),
message.ScheduledForUnixMs is not null ? 154 : 128,
new { chatId = clientChatId, message = mappedMessage, unread = 0 },
cancellationToken);
if (message.ScheduledForUnixMs is null && pushDispatchQueue is not null) await pushDispatchQueue.EnqueueAsync(chat.Id, message.Id, cancellationToken);
return Ok(QlyraCompatibilityResponse.Ok(opcode, new { message = mappedMessage }));''', 2)
edit(path, ''' message.Text = text;
message.EditedAt = DateTimeOffset.UtcNow;''', ''' message.Text = text;
if (message.ScheduledForUnixMs is not null && ReadScheduledTime(payload) is long rescheduled)
{
if (rescheduled <= DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() || rescheduled > DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeMilliseconds())
return BadRequest(QlyraCompatibilityResponse.Error(opcode, "SCHEDULE_INVALID", "Некорректное время отправки."));
message.ScheduledForUnixMs = rescheduled;
}
message.EditedAt = DateTimeOffset.UtcNow;''')
edit(path, ''' if (ReadBool(payload, "forMe") == true)
{''', ''' if (ReadBool(payload, "forMe") == true && ReadString(payload, "itemType") != "DELAYED")
{''')
edit(path, ''' .SingleOrDefaultAsync(entity => entity.ChatId == chatId, cancellationToken);''', ''' .SingleOrDefaultAsync(entity => entity.ChatId == chatId && (entity.ScheduledForUnixMs == null || entity.SenderId == User.GetRequiredUserId()), cancellationToken);''')
edit(path, ''' status = message.DeletedAt is not null ? "REMOVED" : message.EditedAt is not null ? "EDITED" : "SENT",
attaches = mappedAttachments''', ''' status = message.DeletedAt is not null ? "REMOVED" : message.EditedAt is not null ? "EDITED" : "SENT",
delayedAttributes = message.ScheduledForUnixMs is null ? null : new { timeToFire = message.ScheduledForUnixMs.Value, notifySender = true },
attaches = mappedAttachments''')
# Ordinary history, summaries, search and media must never leak queued messages.
p = server / path
text = p.read_text(encoding='utf-8')
text = text.replace('message.DeletedAt is null && !hiddenMessageIds.Contains(message.Id))', 'message.DeletedAt is null && message.ScheduledForUnixMs == null && !hiddenMessageIds.Contains(message.Id))')
# Restore the dedicated regular/delayed branch above.
text = text.replace('var visibleMessages = chat.Messages\n .Where(message => message.DeletedAt is null && message.ScheduledForUnixMs == null && !hiddenMessageIds.Contains(message.Id))', 'var visibleMessages = chat.Messages\n .Where(message => message.DeletedAt is null && !hiddenMessageIds.Contains(message.Id))')
text = text.replace('message.DeletedAt == null && !hiddenMessageIds.Contains(message.Id)', 'message.DeletedAt == null && message.ScheduledForUnixMs == null && !hiddenMessageIds.Contains(message.Id)')
text = text.replace('message.DeletedAt is null && message.PinnedAt', 'message.DeletedAt is null && message.ScheduledForUnixMs == null && message.PinnedAt')
text = text.replace('message.DeletedAt is null &&\n message.SenderId', 'message.DeletedAt is null && message.ScheduledForUnixMs == null &&\n message.SenderId')
p.write_text(text, encoding='utf-8')