Files

273 lines
10 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// Менеджер для сохранения удалённых сообщений
/// Перехватывает сообщения перед удалением и архивирует их локально
public final class AntiDeleteManager {
public static let shared = AntiDeleteManager()
// MARK: - Settings
private let defaults = UserDefaults.standard
private let enabledKey = "antiDelete.enabled"
private let archiveMediaKey = "antiDelete.archiveMedia"
private let deletedMessageTransparencyKey = "antiDelete.deletedMessageTransparency"
private let archiveKey = "antiDelete.archive"
private let deletedIdsKey = "antiDelete.deletedIds"
/// Включено ли сохранение удалённых сообщений
public var isEnabled: Bool {
get { defaults.bool(forKey: enabledKey) }
set { defaults.set(newValue, forKey: enabledKey) }
}
/// Сохранять ли медиа-контент
public var archiveMedia: Bool {
get { defaults.bool(forKey: archiveMediaKey) }
set { defaults.set(newValue, forKey: archiveMediaKey) }
}
/// Минимальное значение прозрачности удалённого сообщения
public static let minDeletedMessageTransparency: Double = 0.0
/// Максимальное значение прозрачности удалённого сообщения
public static let maxDeletedMessageTransparency: Double = 0.8
/// Значение прозрачности удалённого сообщения по умолчанию
public static let defaultDeletedMessageTransparency: Double = 0.45
/// Прозрачность удалённых сообщений (0.0 = непрозрачно, 0.8 = максимально прозрачно)
public var deletedMessageTransparency: Double {
get {
let value = defaults.object(forKey: deletedMessageTransparencyKey) as? NSNumber
let resolvedValue = value?.doubleValue ?? Self.defaultDeletedMessageTransparency
return max(Self.minDeletedMessageTransparency, min(Self.maxDeletedMessageTransparency, resolvedValue))
}
set {
let clampedValue = max(Self.minDeletedMessageTransparency, min(Self.maxDeletedMessageTransparency, newValue))
defaults.set(clampedValue, forKey: deletedMessageTransparencyKey)
}
}
/// Альфа для отображения удалённых сообщений
public var deletedMessageDisplayAlpha: Double {
return 1.0 - self.deletedMessageTransparency
}
// MARK: - Deleted Message IDs Storage
private var deletedMessageIds: Set<String> = []
private let deletedIdsLock = NSLock()
/// Пометить сообщение как удалённое
public func markAsDeleted(peerId: Int64, messageId: Int32) {
let key = "\(peerId)_\(messageId)"
deletedIdsLock.lock()
deletedMessageIds.insert(key)
deletedIdsLock.unlock()
saveDeletedIds()
}
/// Проверить, является ли сообщение удалённым
public func isMessageDeleted(peerId: Int64, messageId: Int32) -> Bool {
guard isEnabled else { return false }
let key = "\(peerId)_\(messageId)"
deletedIdsLock.lock()
defer { deletedIdsLock.unlock() }
return deletedMessageIds.contains(key)
}
/// Проверить, является ли сообщение удалённым (по тексту - legacy)
public func isMessageDeleted(text: String) -> Bool {
guard isEnabled else { return false }
// Legacy: проверяем наличие дефолтного префикса для обратной совместимости
let defaultPrefix = "🗑️ "
return text.hasPrefix(defaultPrefix)
}
private func saveDeletedIds() {
deletedIdsLock.lock()
let ids = Array(deletedMessageIds)
deletedIdsLock.unlock()
defaults.set(ids, forKey: deletedIdsKey)
}
private func loadDeletedIds() {
if let ids = defaults.stringArray(forKey: deletedIdsKey) {
deletedIdsLock.lock()
deletedMessageIds = Set(ids)
deletedIdsLock.unlock()
}
}
// MARK: - Archived Messages Storage
/// Структура архивированного сообщения
public struct ArchivedMessage: Codable {
public let globalId: Int32
public let peerId: Int64
public let messageId: Int32
public let timestamp: Int32
public let deletedAt: Int32
public let authorId: Int64?
public let text: String
public let forwardAuthorId: Int64?
public let mediaDescription: String?
public init(
globalId: Int32,
peerId: Int64,
messageId: Int32,
timestamp: Int32,
deletedAt: Int32,
authorId: Int64?,
text: String,
forwardAuthorId: Int64?,
mediaDescription: String?
) {
self.globalId = globalId
self.peerId = peerId
self.messageId = messageId
self.timestamp = timestamp
self.deletedAt = deletedAt
self.authorId = authorId
self.text = text
self.forwardAuthorId = forwardAuthorId
self.mediaDescription = mediaDescription
}
}
private var archivedMessages: [ArchivedMessage] = []
private let archiveLock = NSLock()
private init() {
// Set default values
if defaults.object(forKey: enabledKey) == nil {
defaults.set(true, forKey: enabledKey)
}
if defaults.object(forKey: archiveMediaKey) == nil {
defaults.set(true, forKey: archiveMediaKey)
}
if defaults.object(forKey: deletedMessageTransparencyKey) == nil {
defaults.set(Self.defaultDeletedMessageTransparency, forKey: deletedMessageTransparencyKey)
}
loadArchive()
loadDeletedIds()
}
// MARK: - Archive Operations
/// Архивировать сообщение перед удалением
/// - Parameters:
/// - globalId: Глобальный ID сообщения
/// - peerId: ID чата
/// - messageId: Локальный ID сообщения
/// - timestamp: Время отправки
/// - authorId: ID автора
/// - text: Текст сообщения
/// - forwardAuthorId: ID автора пересланного сообщения
/// - mediaDescription: Описание медиа (тип, размер)
public func archiveMessage(
globalId: Int32,
peerId: Int64,
messageId: Int32,
timestamp: Int32,
authorId: Int64?,
text: String,
forwardAuthorId: Int64? = nil,
mediaDescription: String? = nil
) {
guard isEnabled else { return }
let archived = ArchivedMessage(
globalId: globalId,
peerId: peerId,
messageId: messageId,
timestamp: timestamp,
deletedAt: Int32(Date().timeIntervalSince1970),
authorId: authorId,
text: text,
forwardAuthorId: forwardAuthorId,
mediaDescription: mediaDescription
)
archiveLock.lock()
defer { archiveLock.unlock() }
// Avoid duplicates
if !archivedMessages.contains(where: { $0.globalId == globalId }) {
archivedMessages.append(archived)
saveArchive()
}
}
/// Получить все архивированные сообщения
public func getAllArchivedMessages() -> [ArchivedMessage] {
archiveLock.lock()
defer { archiveLock.unlock() }
return archivedMessages.sorted { $0.deletedAt > $1.deletedAt }
}
/// Получить архивированные сообщения для конкретного чата
/// - Parameter peerId: ID чата
public func getArchivedMessages(forPeerId peerId: Int64) -> [ArchivedMessage] {
archiveLock.lock()
defer { archiveLock.unlock() }
return archivedMessages
.filter { $0.peerId == peerId }
.sorted { $0.deletedAt > $1.deletedAt }
}
/// Количество архивированных сообщений
public var archivedCount: Int {
archiveLock.lock()
defer { archiveLock.unlock() }
return archivedMessages.count
}
/// Получить данные архивированных сообщений для удаления из диалогов
/// Возвращает массив (peerId, messageId)
public func getArchivedMessageData() -> [(peerId: Int64, messageId: Int32)] {
archiveLock.lock()
defer { archiveLock.unlock() }
return archivedMessages.map { (peerId: $0.peerId, messageId: $0.messageId) }
}
/// Очистить архив
public func clearArchive() {
archiveLock.lock()
defer { archiveLock.unlock() }
archivedMessages.removeAll()
saveArchive()
}
/// Удалить конкретное сообщение из архива
public func removeFromArchive(globalId: Int32) {
archiveLock.lock()
defer { archiveLock.unlock() }
archivedMessages.removeAll { $0.globalId == globalId }
saveArchive()
}
// MARK: - Persistence
private func saveArchive() {
do {
let data = try JSONEncoder().encode(archivedMessages)
defaults.set(data, forKey: archiveKey)
} catch {
print("[AntiDelete] Failed to save archive: \(error)")
}
}
private func loadArchive() {
guard let data = defaults.data(forKey: archiveKey) else { return }
do {
archivedMessages = try JSONDecoder().decode([ArchivedMessage].self, from: data)
} catch {
print("[AntiDelete] Failed to load archive: \(error)")
archivedMessages = []
}
}
}