feat: add Ghostgram features

- Anti-Delete: save deleted messages locally
- Ghost Mode: hide online status, read receipts
- Voice Morpher: audio processing effects
- Device Spoof: spoof device info
- Custom GhostIcon app icon
- User Notes: personal notes for contacts
- Misc settings and controllers
- GPLv2 License
This commit is contained in:
ichmagmaus 812
2026-01-19 12:01:00 +01:00
parent b61c8870fd
commit 03f35f40b5
42 changed files with 3659 additions and 0 deletions
@@ -0,0 +1,241 @@
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 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) }
}
// 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)
}
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 = []
}
}
}
@@ -0,0 +1,47 @@
import Foundation
import Postbox
// MARK: - DeletedMessageAttribute
// This attribute marks a message as "deleted but visible" in the chat
// When anti-delete is enabled, messages are not removed but marked with this attribute
public class DeletedMessageAttribute: MessageAttribute, Equatable {
public let deletedAt: Int32
public let deletedByPeerId: Int64?
public var associatedMessageIds: [MessageId] { return [] }
public var associatedPeerIds: [PeerId] { return [] }
public var automaticTimestampBasedAttribute: (UInt32, Int32)? { return nil }
public init(deletedAt: Int32, deletedByPeerId: Int64? = nil) {
self.deletedAt = deletedAt
self.deletedByPeerId = deletedByPeerId
}
public required init(decoder: PostboxDecoder) {
self.deletedAt = decoder.decodeInt32ForKey("d", orElse: 0)
self.deletedByPeerId = decoder.decodeOptionalInt64ForKey("p")
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt32(self.deletedAt, forKey: "d")
if let peerId = self.deletedByPeerId {
encoder.encodeInt64(peerId, forKey: "p")
}
}
public static func ==(lhs: DeletedMessageAttribute, rhs: DeletedMessageAttribute) -> Bool {
return lhs.deletedAt == rhs.deletedAt && lhs.deletedByPeerId == rhs.deletedByPeerId
}
}
// MARK: - Helper extension for Message
public extension Message {
var isDeletedButVisible: Bool {
return self.attributes.contains(where: { $0 is DeletedMessageAttribute })
}
var deletedMessageAttribute: DeletedMessageAttribute? {
return self.attributes.first(where: { $0 is DeletedMessageAttribute }) as? DeletedMessageAttribute
}
}
@@ -0,0 +1,111 @@
import Foundation
/// Manages edit history for messages
/// GHOSTGRAM: Stores original message text before edits for history viewing
public final class EditHistoryManager {
public static let shared = EditHistoryManager()
private let historyKey = "ghostgram_edit_history"
private var editHistory: [String: [EditRecord]] = [:]
private let lock = NSLock()
public struct EditRecord: Codable, Equatable {
public let text: String
public let editDate: Int32
public init(text: String, editDate: Int32) {
self.text = text
self.editDate = editDate
}
}
private init() {
loadHistory()
}
/// Creates a unique key for message identification
private func messageKey(peerId: Int64, messageId: Int32) -> String {
return "\(peerId)_\(messageId)"
}
/// Saves the original text before an edit
/// Call this BEFORE the message is updated with new text
public func saveOriginalText(peerId: Int64, messageId: Int32, originalText: String, editDate: Int32) {
lock.lock()
defer { lock.unlock() }
let key = messageKey(peerId: peerId, messageId: messageId)
// Don't save empty text
guard !originalText.isEmpty else { return }
// Get existing history or create new
var history = editHistory[key] ?? []
// Check if this exact text already exists (avoid duplicates)
if history.last?.text != originalText {
let record = EditRecord(text: originalText, editDate: editDate)
history.append(record)
editHistory[key] = history
saveHistory()
}
}
/// Gets edit history for a message
public func getEditHistory(peerId: Int64, messageId: Int32) -> [EditRecord] {
lock.lock()
defer { lock.unlock() }
let key = messageKey(peerId: peerId, messageId: messageId)
return editHistory[key] ?? []
}
/// Checks if a message has edit history
public func hasEditHistory(peerId: Int64, messageId: Int32) -> Bool {
lock.lock()
defer { lock.unlock() }
let key = messageKey(peerId: peerId, messageId: messageId)
return !(editHistory[key]?.isEmpty ?? true)
}
/// Clears history for a specific message
public func clearHistory(peerId: Int64, messageId: Int32) {
lock.lock()
defer { lock.unlock() }
let key = messageKey(peerId: peerId, messageId: messageId)
editHistory.removeValue(forKey: key)
saveHistory()
}
/// Clears all edit history
public func clearAllHistory() {
lock.lock()
defer { lock.unlock() }
editHistory.removeAll()
saveHistory()
}
// MARK: - Persistence
private func saveHistory() {
do {
let data = try JSONEncoder().encode(editHistory)
UserDefaults.standard.set(data, forKey: historyKey)
} catch {
// Silent fail - non-critical feature
}
}
private func loadHistory() {
guard let data = UserDefaults.standard.data(forKey: historyKey) else { return }
do {
editHistory = try JSONDecoder().decode([String: [EditRecord]].self, from: data)
} catch {
editHistory = [:]
}
}
}