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();', 'builder.Services.AddHostedService();\nbuilder.Services.AddHostedService();') 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')