Files
Leeksov 4647310322 GLEGram 12.5 — Initial public release
Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
2026-04-06 09:48:12 +03:00

406 lines
18 KiB
Swift

import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
#if canImport(SGDeletedMessages)
import SGDeletedMessages
#endif
#if canImport(SGSimpleSettings)
import SGSimpleSettings
#endif
#if canImport(SGLogging)
import SGLogging
#endif
func addMessageMediaResourceIdsToRemove(media: Media, resourceIds: inout [MediaResourceId]) {
if let image = media as? TelegramMediaImage {
for representation in image.representations {
resourceIds.append(representation.resource.id)
}
} else if let file = media as? TelegramMediaFile {
for representation in file.previewRepresentations {
resourceIds.append(representation.resource.id)
}
resourceIds.append(file.resource.id)
}
}
func addMessageMediaResourceIdsToRemove(message: Message, resourceIds: inout [MediaResourceId]) {
for media in message.media {
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
}
}
// MARK: - GLEGram - Force-delete message ids (bypasses "save deleted messages" marking).
/// Returns the message ids that were actually deleted.
public func _internal_forceDeleteMessages(transaction: Transaction, mediaBox: MediaBox, ids: [MessageId], deleteMedia: Bool = true, manualAddMessageThreadStatsDifference: ((MessageThreadKey, Int, Int) -> Void)? = nil) -> [MessageId] {
let idsToDelete = ids
var resourceIds: [MediaResourceId] = []
if deleteMedia {
for id in idsToDelete {
if id.peerId.namespace == Namespaces.Peer.SecretChat {
if let message = transaction.getMessage(id) {
addMessageMediaResourceIdsToRemove(message: message, resourceIds: &resourceIds)
}
}
}
}
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
for id in idsToDelete {
if id.peerId.namespace == Namespaces.Peer.CloudChannel && id.namespace == Namespaces.Message.Cloud {
if let message = transaction.getMessage(id) {
if let threadId = message.threadId {
let messageThreadKey = MessageThreadKey(peerId: message.id.peerId, threadId: threadId)
if id.peerId.namespace == Namespaces.Peer.CloudChannel {
if let manualAddMessageThreadStatsDifference = manualAddMessageThreadStatsDifference {
manualAddMessageThreadStatsDifference(messageThreadKey, 0, 1)
} else {
updateMessageThreadStats(transaction: transaction, threadKey: messageThreadKey, removedCount: 1, addedMessagePeers: [])
}
}
}
}
}
}
transaction.deleteMessages(idsToDelete, forEachMedia: { _ in
})
return idsToDelete
}
/// Returns the message ids that were actually deleted (not marked as saved-deleted).
@discardableResult
public func _internal_deleteMessages(transaction: Transaction, mediaBox: MediaBox, ids: [MessageId], deleteMedia: Bool = true, manualAddMessageThreadStatsDifference: ((MessageThreadKey, Int, Int) -> Void)? = nil) -> [MessageId] {
// MARK: GLEGram - Mark messages as deleted instead of actually deleting them
#if canImport(SGDeletedMessages)
let savedSnapshots = SGDeletedMessages.saveSnapshots(
ids: ids,
transaction: transaction,
shouldSave: { id, _ in
#if canImport(SGSimpleSettings)
// AyuGram-style: don't save bot chats if disabled
if id.peerId.namespace == Namespaces.Peer.CloudUser,
let peer = transaction.getPeer(id.peerId) as? TelegramUser,
peer.botInfo != nil,
!SGSimpleSettings.shared.saveDeletedMessagesForBots {
return false
}
#endif
return true
},
transformAttributes: { _, attributes in
#if canImport(SGSimpleSettings)
if !SGSimpleSettings.shared.saveDeletedMessagesReactions {
attributes.removeAll(where: { $0 is ReactionsMessageAttribute })
}
#endif
},
transformMedia: { message, _ in
// AyuGram-style: copy completed media resources to "Saved Attachments"
return sgTransformMediaForSavedDeletedSnapshot(message: message, mediaBox: mediaBox)
}
)
let idsToDelete = ids
#if canImport(SGLogging)
SGLogger.shared.log("SGDeletedMessages", "_internal_deleteMessages: ids=\(ids), savedSnapshots=\(savedSnapshots.count), deleting originals=\(idsToDelete.count)")
#endif
#else
let idsToDelete = ids
#endif
var resourceIds: [MediaResourceId] = []
if deleteMedia {
for id in idsToDelete {
if id.peerId.namespace == Namespaces.Peer.SecretChat {
if let message = transaction.getMessage(id) {
#if canImport(SGDeletedMessages)
#if canImport(SGSimpleSettings)
// AyuGram-style: if we saved a snapshot and media saving is enabled,
// don't immediately purge cached resources for secret chats.
if savedSnapshots.contains(id) && SGSimpleSettings.shared.saveDeletedMessagesMedia {
continue
}
#endif
#endif
addMessageMediaResourceIdsToRemove(message: message, resourceIds: &resourceIds)
}
}
}
}
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
// If we are deleting SavedDeleted snapshots, remove their copied attachments from disk.
let savedDeletedToDelete = idsToDelete.filter { $0.namespace == Namespaces.Message.SavedDeleted }
if !savedDeletedToDelete.isEmpty {
for id in savedDeletedToDelete {
if let message = transaction.getMessage(id) {
sgDeleteSavedDeletedAttachmentsForMessage(message)
}
}
}
for id in idsToDelete {
if id.peerId.namespace == Namespaces.Peer.CloudChannel && id.namespace == Namespaces.Message.Cloud {
if let message = transaction.getMessage(id) {
if let threadId = message.threadId {
let messageThreadKey = MessageThreadKey(peerId: message.id.peerId, threadId: threadId)
if id.peerId.namespace == Namespaces.Peer.CloudChannel {
if let manualAddMessageThreadStatsDifference = manualAddMessageThreadStatsDifference {
manualAddMessageThreadStatsDifference(messageThreadKey, 0, 1)
} else {
updateMessageThreadStats(transaction: transaction, threadKey: messageThreadKey, removedCount: 1, addedMessagePeers: [])
}
}
}
}
}
}
transaction.deleteMessages(idsToDelete, forEachMedia: { _ in
})
return idsToDelete
}
// MARK: - GLEGram - Safely delete messages in range, preserving saved deleted messages
func _internal_deleteMessagesInRangeSafely(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, namespace: MessageId.Namespace, minId: MessageId.Id, maxId: MessageId.Id, forEachMedia: ((Media) -> Void)?) {
#if canImport(SGDeletedMessages)
guard SGDeletedMessages.showDeletedMessages else {
// If feature is disabled, use normal deletion
var resourceIds: [MediaResourceId] = []
transaction.deleteMessagesInRange(peerId: peerId, namespace: namespace, minId: minId, maxId: maxId, forEachMedia: { media in
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
forEachMedia?(media)
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
return
}
// Collect all message IDs in range (ascending order stop early once past maxId)
var messageIdsInRange: [MessageId] = []
transaction.withAllMessages(peerId: peerId, namespace: namespace, reversed: false) { message in
if message.id.id > maxId {
return false // Past the range stop scanning
}
if message.id.id >= minId {
messageIdsInRange.append(message.id)
}
return true
}
#if canImport(SGLogging)
SGLogger.shared.log("SGDeletedMessages", "_internal_deleteMessagesInRangeSafely: peerId=\(peerId), namespace=\(namespace), range=[\(minId)...\(maxId)], found \(messageIdsInRange.count) messages in range")
#endif
// Filter out saved deleted messages
var idsToDelete: [MessageId] = []
var idsToKeep: [MessageId] = []
var resourceIds: [MediaResourceId] = []
for messageId in messageIdsInRange {
if let message = transaction.getMessage(messageId) {
// Check if message is saved (marked as deleted but should be kept)
let isSaved = message.sgDeletedAttribute.isDeleted
if isSaved {
idsToKeep.append(messageId)
#if canImport(SGLogging)
SGLogger.shared.log("SGDeletedMessages", "_internal_deleteMessagesInRangeSafely: KEEPING saved message \(messageId)")
#endif
} else {
idsToDelete.append(messageId)
// Collect media resources for deletion
for media in message.media {
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
forEachMedia?(media)
}
}
}
}
#if canImport(SGLogging)
SGLogger.shared.log("SGDeletedMessages", "_internal_deleteMessagesInRangeSafely: keeping \(idsToKeep.count), deleting \(idsToDelete.count)")
#endif
// Delete only non-saved messages
if !idsToDelete.isEmpty {
_ = _internal_deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: idsToDelete, deleteMedia: false)
}
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
#else
// No SGDeletedMessages support - use normal deletion
var resourceIds: [MediaResourceId] = []
transaction.deleteMessagesInRange(peerId: peerId, namespace: namespace, minId: minId, maxId: maxId, forEachMedia: { media in
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
forEachMedia?(media)
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
#endif
}
func _internal_deleteAllMessagesWithAuthor(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, authorId: PeerId, namespace: MessageId.Namespace) {
var resourceIds: [MediaResourceId] = []
transaction.removeAllMessagesWithAuthor(peerId, authorId: authorId, namespace: namespace, forEachMedia: { media in
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds))).start()
}
}
func _internal_deleteAllMessagesWithForwardAuthor(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, forwardAuthorId: PeerId, namespace: MessageId.Namespace) {
var resourceIds: [MediaResourceId] = []
transaction.removeAllMessagesWithForwardAuthor(peerId, forwardAuthorId: forwardAuthorId, namespace: namespace, forEachMedia: { media in
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
}
func _internal_clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, threadId: Int64?, namespaces: MessageIdNamespaces) {
if peerId.namespace == Namespaces.Peer.SecretChat {
var resourceIds: [MediaResourceId] = []
transaction.withAllMessages(peerId: peerId, { message in
addMessageMediaResourceIdsToRemove(message: message, resourceIds: &resourceIds)
return true
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
}
transaction.clearHistory(peerId, threadId: threadId, minTimestamp: nil, maxTimestamp: nil, namespaces: namespaces, forEachMedia: { _ in
})
}
func _internal_clearHistoryInRange(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, threadId: Int64?, minTimestamp: Int32, maxTimestamp: Int32, namespaces: MessageIdNamespaces) {
if peerId.namespace == Namespaces.Peer.SecretChat {
var resourceIds: [MediaResourceId] = []
transaction.withAllMessages(peerId: peerId, { message in
if message.timestamp >= minTimestamp && message.timestamp <= maxTimestamp {
addMessageMediaResourceIdsToRemove(message: message, resourceIds: &resourceIds)
}
return true
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
}
transaction.clearHistory(peerId, threadId: threadId, minTimestamp: minTimestamp, maxTimestamp: maxTimestamp, namespaces: namespaces, forEachMedia: { _ in
})
}
public enum ClearCallHistoryError {
case generic
}
func _internal_clearCallHistory(account: Account, forEveryone: Bool) -> Signal<Never, ClearCallHistoryError> {
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
var flags: Int32 = 0
if forEveryone {
flags |= 1 << 0
}
let signal = account.network.request(Api.functions.messages.deletePhoneCallHistory(flags: flags))
|> map { result -> Api.messages.AffectedFoundMessages? in
return result
}
|> `catch` { _ -> Signal<Api.messages.AffectedFoundMessages?, Bool> in
return .fail(false)
}
|> mapToSignal { result -> Signal<Void, Bool> in
if let result = result {
switch result {
case let .affectedFoundMessages(affectedFoundMessagesData):
let (pts, ptsCount, offset) = (affectedFoundMessagesData.pts, affectedFoundMessagesData.ptsCount, affectedFoundMessagesData.offset)
account.stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
if offset == 0 {
return .fail(true)
} else {
return .complete()
}
}
} else {
return .fail(true)
}
}
return (signal
|> restart)
|> `catch` { success -> Signal<Void, NoError> in
if success {
return account.postbox.transaction { transaction -> Void in
transaction.removeAllMessagesWithGlobalTag(tag: GlobalMessageTags.Calls)
}
} else {
return .complete()
}
}
}
|> switchToLatest
|> ignoreValues
|> castError(ClearCallHistoryError.self)
}
public enum SetChatMessageAutoremoveTimeoutError {
case generic
}
func _internal_setChatMessageAutoremoveTimeoutInteractively(account: Account, peerId: PeerId, timeout: Int32?) -> Signal<Never, SetChatMessageAutoremoveTimeoutError> {
return account.postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> castError(SetChatMessageAutoremoveTimeoutError.self)
|> mapToSignal { inputPeer -> Signal<Never, SetChatMessageAutoremoveTimeoutError> in
guard let inputPeer = inputPeer else {
return .fail(.generic)
}
return account.network.request(Api.functions.messages.setHistoryTTL(peer: inputPeer, period: timeout ?? 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> castError(SetChatMessageAutoremoveTimeoutError.self)
|> mapToSignal { result -> Signal<Never, SetChatMessageAutoremoveTimeoutError> in
if let result = result {
account.stateManager.addUpdates(result)
return account.postbox.transaction { transaction -> Void in
transaction.updatePeerCachedData(peerIds: [peerId], update: { _, current in
let updatedTimeout: CachedPeerAutoremoveTimeout
if let timeout = timeout {
updatedTimeout = .known(CachedPeerAutoremoveTimeout.Value(peerValue: timeout))
} else {
updatedTimeout = .known(nil)
}
if peerId.namespace == Namespaces.Peer.CloudUser {
let current = (current as? CachedUserData) ?? CachedUserData()
return current.withUpdatedAutoremoveTimeout(updatedTimeout)
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
let current = (current as? CachedChannelData) ?? CachedChannelData()
return current.withUpdatedAutoremoveTimeout(updatedTimeout)
} else if peerId.namespace == Namespaces.Peer.CloudGroup {
let current = (current as? CachedGroupData) ?? CachedGroupData()
return current.withUpdatedAutoremoveTimeout(updatedTimeout)
} else {
return current
}
})
}
|> castError(SetChatMessageAutoremoveTimeoutError.self)
|> ignoreValues
} else {
return .fail(.generic)
}
}
|> `catch` { _ -> Signal<Never, SetChatMessageAutoremoveTimeoutError> in
return .complete()
}
}
}