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.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
@@ -0,0 +1,58 @@
//// A copy of Atomic from SwiftSignalKit
//import Foundation
//
//public enum AtomicWrapperLockError: Error {
// case isLocked
//}
//
//public final class AtomicWrapper<T> {
// private var lock: pthread_mutex_t
// private var value: T
//
// public init(value: T) {
// self.lock = pthread_mutex_t()
// self.value = value
//
// pthread_mutex_init(&self.lock, nil)
// }
//
// deinit {
// pthread_mutex_destroy(&self.lock)
// }
//
// public func with<R>(_ f: (T) -> R) -> R {
// pthread_mutex_lock(&self.lock)
// let result = f(self.value)
// pthread_mutex_unlock(&self.lock)
//
// return result
// }
//
// public func tryWith<R>(_ f: (T) -> R) throws -> R {
// if pthread_mutex_trylock(&self.lock) == 0 {
// let result = f(self.value)
// pthread_mutex_unlock(&self.lock)
// return result
// } else {
// throw AtomicWrapperLockError.isLocked
// }
// }
//
// public func modify(_ f: (T) -> T) -> T {
// pthread_mutex_lock(&self.lock)
// let result = f(self.value)
// self.value = result
// pthread_mutex_unlock(&self.lock)
//
// return result
// }
//
// public func swap(_ value: T) -> T {
// pthread_mutex_lock(&self.lock)
// let previous = self.value
// self.value = value
// pthread_mutex_unlock(&self.lock)
//
// return previous
// }
//}
@@ -0,0 +1,7 @@
import Foundation
/// Global feature flags for GLEGram (Swiftgram).
public enum GLEGramFeatures {
/// Master toggle for JS plugins: without it, PluginRunner and chat hooks are disabled (less overhead and hangs).
public static let pluginsEnabled = true
}
@@ -0,0 +1,190 @@
import Foundation
/// Swiftgram iOS plugin hook integration (`.plugin` Python files).
///
/// Swift-сторона хуков: TelegramUI вызывает эти точки; рантайм Python (например PythonKit / CPython) задаёт провайдеры.
public enum SGPluginHookStrategy: String, Codable, Sendable {
/// Keep original message.
case passthrough
/// Replace outgoing message text.
case modify
/// Cancel sending (plugin handled it itself).
case cancel
}
public struct SGPluginHookResult: Codable, Sendable, Equatable {
public var strategy: SGPluginHookStrategy
public var message: String?
public init(strategy: SGPluginHookStrategy = .passthrough, message: String? = nil) {
self.strategy = strategy
self.message = message
}
}
/// Info about the message being replied to (for FileViewer-style plugins: open file in reply).
public struct ReplyMessageInfo: Sendable {
public let messageId: Int64
public let isDocument: Bool
public let filePath: String?
public let fileName: String?
public let mimeType: String?
public init(messageId: Int64, isDocument: Bool, filePath: String?, fileName: String?, mimeType: String?) {
self.messageId = messageId
self.isDocument = isDocument
self.filePath = filePath
self.fileName = fileName
self.mimeType = mimeType
}
}
/// Optional runner for outgoing message hook (e.g. PythonKit-based). Set by app/SGSettingsUI when Python is available.
/// When replying to a document, replyInfo may contain local file path and name for plugins like FileViewer.
public typealias PluginMessageHookRunner = (Int64, Int64, String, Int64?, ReplyMessageInfo?) -> SGPluginHookResult?
/// Display info for peer/user (Fake Profilestyle plugins can modify before UI shows).
public struct PluginDisplayUser: Equatable, Sendable {
public var firstName: String
public var lastName: String
public var username: String?
public var phone: String?
public var id: Int64
public var isPremium: Bool
public var isVerified: Bool
public var isScam: Bool
public var isFake: Bool
public var isSupport: Bool
public var isBot: Bool
public init(firstName: String, lastName: String, username: String?, phone: String?, id: Int64, isPremium: Bool, isVerified: Bool, isScam: Bool, isFake: Bool, isSupport: Bool, isBot: Bool) {
self.firstName = firstName
self.lastName = lastName
self.username = username
self.phone = phone
self.id = id
self.isPremium = isPremium
self.isVerified = isVerified
self.isScam = isScam
self.isFake = isFake
self.isSupport = isSupport
self.isBot = isBot
}
}
/// Optional runner for user/peer display hook (Fake Profile style). Set when Python runtime is available.
public typealias PluginUserDisplayRunner = (Int64, PluginDisplayUser) -> PluginDisplayUser?
/// Called when a new message is received (updateNewMessage / updateNewChannelMessage). Set by runner; observer posts from TelegramCore.
public typealias PluginIncomingMessageRunner = (Int64, Int64, Int64, String?, Bool) -> Void
/// One item to add to the chat context menu (CHAT_ACTION_MENU style). Plugins register these via chatMenuItemsProvider.
public struct PluginChatMenuItem: Sendable {
public let title: String
public let action: @Sendable () -> Void
public init(title: String, action: @escaping @Sendable () -> Void) {
self.title = title
self.action = action
}
}
/// Notification name posted by TelegramCore when a new message is processed (updateNewMessage / updateNewChannelMessage).
/// userInfo: accountId (Int64), peerId (Int64), messageId (Int64), text (String), outgoing (Bool).
public let SGPluginIncomingMessageNotificationName = Notification.Name("SGPluginIncomingMessage")
// MARK: - Дополнительные хуки для кастомизации клиента
/// Возвращает true, если плагин обработал URL (клиент не должен открывать его сам).
public typealias PluginOpenUrlRunner = (String) -> Bool
/// Уведомление перед открытием профиля (accountId, peerId).
public typealias PluginWillOpenProfileRunner = (Int64, Int64) -> Void
/// Уведомление перед открытием чата (accountId, peerId).
public typealias PluginWillOpenChatRunner = (Int64, Int64) -> Void
/// Возвращает false, чтобы скрыть сообщение в списке (например, фильтр по ключевым словам). accountId, peerId, messageId, text, outgoing.
public typealias PluginShouldShowMessageRunner = (Int64, Int64, Int64, String?, Bool) -> Bool
/// Возвращает false, чтобы скрыть кнопку подарка в чате. accountId, peerId.
public typealias PluginShouldShowGiftButtonRunner = (Int64, Int64) -> Bool
/// Дополнительные пункты контекстного меню в профиле пользователя/канала. accountId, peerId.
public typealias PluginProfileMenuItemsProvider = (Int64, Int64) -> [PluginChatMenuItem]
/// Entry point used by TelegramUI just before enqueueing outgoing messages.
///
/// If `messageHookRunner` is set and returns a result, that result is used; otherwise `.passthrough`.
public enum SGPluginHooks {
/// When set, called for each outgoing text message. Return nil to fall back to .passthrough.
public static var messageHookRunner: PluginMessageHookRunner?
/// When set, called when building user/peer display (Fake Profile style). Return nil to keep original.
public static var userDisplayRunner: PluginUserDisplayRunner?
/// When set, called when a new message is received (see notification SGPluginIncomingMessage). Used by sender-style plugins.
public static var incomingMessageHookRunner: PluginIncomingMessageRunner?
/// When set, returns extra items for the chat message context menu (CHAT_ACTION_MENU). accountId, peerId.
public static var chatMenuItemsProvider: ((Int64, Int64) -> [PluginChatMenuItem])?
/// Когда задан, вызывается при открытии URL (tg://, t.me и т.д.). Возврат true = плагин обработал, клиент не открывает.
public static var openUrlRunner: PluginOpenUrlRunner?
/// Когда задан, вызывается перед открытием экрана профиля (accountId, peerId).
public static var willOpenProfileRunner: PluginWillOpenProfileRunner?
/// Когда задан, вызывается перед открытием чата (accountId, peerId).
public static var willOpenChatRunner: PluginWillOpenChatRunner?
/// Когда задан, вызывается для каждого сообщения при отображении. Возврат false скрывает сообщение в списке.
public static var shouldShowMessageRunner: PluginShouldShowMessageRunner?
/// Когда задан, возврат false скрывает кнопку подарка в чате (accountId, peerId).
public static var shouldShowGiftButtonRunner: PluginShouldShowGiftButtonRunner?
/// Когда задан, возвращает доп. пункты контекстного меню в профиле (accountId, peerId).
public static var profileMenuItemsProvider: PluginProfileMenuItemsProvider?
public static func applyOutgoingMessageTextHooks(
accountPeerId: Int64,
peerId: Int64,
text: String,
replyToMessageId: Int64? = nil,
replyMessageInfo: ReplyMessageInfo? = nil
) -> SGPluginHookResult {
guard SGSimpleSettings.shared.pluginSystemEnabled else {
return SGPluginHookResult(strategy: .passthrough)
}
if let result = messageHookRunner?(accountPeerId, peerId, text, replyToMessageId, replyMessageInfo), result.strategy != .passthrough {
return result
}
return SGPluginHookResult(strategy: .passthrough)
}
/// Apply plugin display hooks (Fake Profile). Call when building peer title; returns modified user display or original.
public static func applyUserDisplayHooks(accountId: Int64, user: PluginDisplayUser) -> PluginDisplayUser {
guard SGSimpleSettings.shared.pluginSystemEnabled else { return user }
return userDisplayRunner?(accountId, user) ?? user
}
/// Вызвать перед открытием URL. Возвращает true, если плагин обработал URL и клиент не должен открывать его.
public static func applyOpenUrlHook(url: String) -> Bool {
guard SGSimpleSettings.shared.pluginSystemEnabled else { return false }
return openUrlRunner?(url) ?? false
}
/// Вызвать при отображении сообщения в списке. Возвращает false, чтобы скрыть сообщение.
public static func applyShouldShowMessageHook(accountId: Int64, peerId: Int64, messageId: Int64, text: String?, outgoing: Bool) -> Bool {
guard SGSimpleSettings.shared.pluginSystemEnabled else { return true }
return shouldShowMessageRunner?(accountId, peerId, messageId, text, outgoing) ?? true
}
/// Вызвать при решении показывать ли кнопку подарка в чате. Возвращает false, чтобы скрыть.
public static func applyShouldShowGiftButtonHook(accountId: Int64, peerId: Int64) -> Bool {
guard SGSimpleSettings.shared.pluginSystemEnabled else { return true }
return shouldShowGiftButtonRunner?(accountId, peerId) ?? true
}
}
+74
View File
@@ -0,0 +1,74 @@
// MARK: Swiftgram Plugin host (колбэки из плагинов в iOS UI)
//
// Плагины показывают тосты, алерты и отправляют сообщения через PluginHost.
import Foundation
/// Тип тоста для плагина (info / error / success).
public enum PluginBulletinType {
case info
case error
case success
}
/// Host callbacks that the app sets so plugins can show UI and send messages.
public final class PluginHost {
public static let shared = PluginHost()
/// Показать тост (баннер внизу экрана).
public var showBulletin: ((String, PluginBulletinType) -> Void)?
/// Показать алерт (заголовок и сообщение).
public var showAlert: ((String, String) -> Void)?
/// Отправить текстовое или файловое сообщение (accountPeerId, peerId, text, replyToMessageId?, filePath?). filePath: локальный путь к файлу для отправки как документ; fileName задаётся отдельно через sendMessageWithFileName при необходимости.
public var sendMessage: ((Int64, Int64, String, Int64?, String?) -> Void)?
/// Временная директория для плагина (например для FileViewer: скачать/редактировать файлы). Поддиректория в Caches; создаётся при первом обращении.
public func getPluginTempDirectory(pluginId: String) -> String {
let dir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?
.appendingPathComponent("Plugins", isDirectory: true)
.appendingPathComponent(pluginId, isDirectory: true).path ?? NSTemporaryDirectory()
try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
return dir
}
/// Выполнить блок на главной очереди.
public var runOnMain: ((@escaping () -> Void) -> Void)?
/// Выполнить блок в фоне.
public var runOnBackground: ((@escaping () -> Void) -> Void)?
// MARK: - Настройки плагина (get_setting / set_setting в Python)
private let pluginSettingsPrefix = "sg_plugin_"
/// Reads a plugin setting string (UserDefaults key: sg_plugin_{pluginId}_{key}).
public func getPluginSetting(pluginId: String, key: String) -> String? {
let k = "\(pluginSettingsPrefix)\(pluginId)_\(key)"
return UserDefaults.standard.string(forKey: k)
}
/// Reads a plugin setting bool (stored as "1"/"0" or "true"/"false").
public func getPluginSettingBool(pluginId: String, key: String, default: Bool) -> Bool {
guard let s = getPluginSetting(pluginId: pluginId, key: key) else { return `default` }
return s == "1" || s.lowercased() == "true"
}
/// Writes a plugin setting.
public func setPluginSetting(pluginId: String, key: String, value: String) {
UserDefaults.standard.set(value, forKey: "\(pluginSettingsPrefix)\(pluginId)_\(key)")
}
/// Writes a plugin setting bool.
public func setPluginSettingBool(pluginId: String, key: String, value: Bool) {
setPluginSetting(pluginId: pluginId, key: key, value: value ? "1" : "0")
}
private init() {
runOnMain = { block in DispatchQueue.main.async(execute: block) }
runOnBackground = { block in
DispatchQueue.global(qos: .userInitiated).async(execute: block)
}
}
}
@@ -0,0 +1,36 @@
//
// RWLock.swift
// SwiftConcurrentCollections
//
// Created by Pete Prokop on 09/02/2020.
// Copyright © 2020 Pete Prokop. All rights reserved.
//
import Foundation
public final class RWLock {
private var lock: pthread_rwlock_t
// MARK: Lifecycle
deinit {
pthread_rwlock_destroy(&lock)
}
public init() {
lock = pthread_rwlock_t()
pthread_rwlock_init(&lock, nil)
}
// MARK: Public
public func writeLock() {
pthread_rwlock_wrlock(&lock)
}
public func readLock() {
pthread_rwlock_rdlock(&lock)
}
public func unlock() {
pthread_rwlock_unlock(&lock)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,408 @@
import Foundation
public protocol AllowedUserDefaultTypes {}
/* // This one is more painful than helpful
extension Bool: AllowedUserDefaultTypes {}
extension String: AllowedUserDefaultTypes {}
extension Int: AllowedUserDefaultTypes {}
extension Int32: AllowedUserDefaultTypes {}
extension Double: AllowedUserDefaultTypes {}
extension Float: AllowedUserDefaultTypes {}
extension Data: AllowedUserDefaultTypes {}
extension URL: AllowedUserDefaultTypes {}
//extension Dictionary<String, Any>: AllowedUserDefaultTypes {}
extension Array: AllowedUserDefaultTypes where Element: AllowedUserDefaultTypes {}
*/
// Does not support Optional types due to caching
@propertyWrapper
public class UserDefault<T> /*where T: AllowedUserDefaultTypes*/ {
public let key: String
public let userDefaults: UserDefaults
private var cachedValue: T?
public init(key: String, userDefaults: UserDefaults = .standard) {
self.key = key
self.userDefaults = userDefaults
}
public var wrappedValue: T {
get {
#if DEBUG && false
SGtrace("UD.\(key)", what: "GET")
#endif
if let strongCachedValue = cachedValue {
#if DEBUG && false
SGtrace("UD", what: "CACHED \(key) \(strongCachedValue)")
#endif
return strongCachedValue
}
cachedValue = readFromUserDefaults()
#if DEBUG
SGtrace("UD.\(key)", what: "EXTRACTED: \(cachedValue!)")
#endif
return cachedValue!
}
set {
cachedValue = newValue
#if DEBUG
SGtrace("UD.\(key)", what: "CACHE UPDATED \(cachedValue!)")
#endif
userDefaults.set(newValue, forKey: key)
}
}
fileprivate func readFromUserDefaults() -> T {
switch T.self {
case is Bool.Type:
return (userDefaults.bool(forKey: key) as! T)
case is String.Type:
return (userDefaults.string(forKey: key) as! T)
case is Int64.Type:
return (Int64(exactly: userDefaults.integer(forKey: key)) as! T)
case is Int32.Type:
return (Int32(exactly: userDefaults.integer(forKey: key)) as! T)
case is Int.Type:
return (userDefaults.integer(forKey: key) as! T)
case is Double.Type:
return (userDefaults.double(forKey: key) as! T)
case is Float.Type:
return (userDefaults.float(forKey: key) as! T)
case is Data.Type:
return (userDefaults.data(forKey: key) as! T)
case is URL.Type:
return (userDefaults.url(forKey: key) as! T)
case is Array<String>.Type:
return (userDefaults.stringArray(forKey: key) as! T)
case is Array<Any>.Type:
return (userDefaults.array(forKey: key) as! T)
default:
fatalError("Unsupported UserDefault type \(T.self)")
// cachedValue = (userDefaults.object(forKey: key) as! T)
}
}
}
//public class AtomicUserDefault<T>: UserDefault<T> {
// private let atomicCachedValue: AtomicWrapper<T?> = AtomicWrapper(value: nil)
//
// public override var wrappedValue: T {
// get {
// return atomicCachedValue.modify({ value in
// if let strongValue = value {
// return strongValue
// }
// return self.readFromUserDefaults()
// })!
// }
// set {
// let _ = atomicCachedValue.modify({ _ in
// userDefaults.set(newValue, forKey: key)
// return newValue
// })
// }
// }
//}
// Based on ConcurrentDictionary.swift from https://github.com/peterprokop/SwiftConcurrentCollections
/// Thread-safe UserDefaults dictionary wrapper
/// - Important: Note that this is a `class`, i.e. reference (not value) type
/// - Important: Key can only be String type
public class UserDefaultsBackedDictionary<Key: Hashable, Value> {
public let userDefaultsKey: String
public let userDefaults: UserDefaults
private var container: [Key: Value]? = nil
private let rwlock = RWLock()
private let threadSafe: Bool
public var keys: [Key] {
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "KEYS")
#endif
let result: [Key]
if threadSafe {
rwlock.readLock()
}
if container == nil {
container = userDefaultsContainer
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "EXTRACTED: \(container!)")
#endif
} else {
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "FROM CACHE: \(container!)")
#endif
}
result = Array(container!.keys)
if threadSafe {
rwlock.unlock()
}
return result
}
public var values: [Value] {
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "VALUES")
#endif
let result: [Value]
if threadSafe {
rwlock.readLock()
}
if container == nil {
container = userDefaultsContainer
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "EXTRACTED: \(container!)")
#endif
} else {
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "FROM CACHE: \(container!)")
#endif
}
result = Array(container!.values)
if threadSafe {
rwlock.unlock()
}
return result
}
public init(userDefaultsKey: String, userDefaults: UserDefaults = .standard, threadSafe: Bool) {
self.userDefaultsKey = userDefaultsKey
self.userDefaults = userDefaults
self.threadSafe = threadSafe
}
/// Sets the value for key
///
/// - Parameters:
/// - value: The value to set for key
/// - key: The key to set value for
public func set(value: Value, forKey key: Key) {
if threadSafe {
rwlock.writeLock()
}
_set(value: value, forKey: key)
if threadSafe {
rwlock.unlock()
}
}
@discardableResult
public func remove(_ key: Key) -> Value? {
let result: Value?
if threadSafe {
rwlock.writeLock()
}
result = _remove(key)
if threadSafe {
rwlock.unlock()
}
return result
}
@discardableResult
public func removeValue(forKey: Key) -> Value? {
return self.remove(forKey)
}
public func contains(_ key: Key) -> Bool {
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "CONTAINS")
#endif
let result: Bool
if threadSafe {
rwlock.readLock()
}
if container == nil {
container = userDefaultsContainer
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "EXTRACTED: \(container!)")
#endif
} else {
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "FROM CACHE: \(container!)")
#endif
}
result = container!.index(forKey: key) != nil
if threadSafe {
rwlock.unlock()
}
return result
}
public func value(forKey key: Key) -> Value? {
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "VALUE")
#endif
let result: Value?
if threadSafe {
rwlock.readLock()
}
if container == nil {
container = userDefaultsContainer
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "EXTRACTED: \(container!)")
#endif
} else {
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "FROM CACHE: \(container!)")
#endif
}
result = container![key]
if threadSafe {
rwlock.unlock()
}
return result
}
public func mutateValue(forKey key: Key, mutation: (Value) -> Value) {
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "MUTATE")
#endif
if threadSafe {
rwlock.writeLock()
}
if container == nil {
container = userDefaultsContainer
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "EXTRACTED: \(container!)")
#endif
} else {
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "FROM CACHE: \(container!)")
#endif
}
if let value = container![key] {
container![key] = mutation(value)
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "UPDATING CACHE \(key): \(value), \(container!)")
#endif
userDefaultsContainer = container!
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "CACHE UPDATED \(key): \(value), \(container!)")
#endif
}
if threadSafe {
rwlock.unlock()
}
}
public var isEmpty: Bool {
return self.keys.isEmpty
}
// MARK: Subscript
public subscript(key: Key) -> Value? {
get {
return value(forKey: key)
}
set {
if threadSafe {
rwlock.writeLock()
}
defer {
if threadSafe {
rwlock.unlock()
}
}
guard let newValue = newValue else {
_remove(key)
return
}
_set(value: newValue, forKey: key)
}
}
// MARK: Private
@inline(__always)
private func _set(value: Value, forKey key: Key) {
if container == nil {
container = userDefaultsContainer
}
self.container![key] = value
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "UPDATING CACHE \(key): \(value), \(container!)")
#endif
userDefaultsContainer = container!
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "CACHE UPDATED \(key): \(value), \(container!)")
#endif
}
@inline(__always)
@discardableResult
private func _remove(_ key: Key) -> Value? {
if container == nil {
container = userDefaultsContainer
}
guard let index = container!.index(forKey: key) else { return nil }
let tuple = container!.remove(at: index)
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "UPDATING CACHE REMOVE \(key) \(container!)")
#endif
userDefaultsContainer = container!
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "CACHE UPDATED REMOVED \(key) \(container!)")
#endif
return tuple.value
}
private var userDefaultsContainer: [Key: Value] {
get {
return userDefaults.dictionary(forKey: userDefaultsKey) as! [Key: Value]
}
set {
userDefaults.set(newValue, forKey: userDefaultsKey)
}
}
public func drop() {
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "DROPPING")
#endif
if threadSafe {
rwlock.writeLock()
}
userDefaults.removeObject(forKey: userDefaultsKey)
container = userDefaultsContainer
#if DEBUG
SGtrace("UD.\(userDefaultsKey)\(threadSafe ? "-ts" : "")", what: "DROPPED: \(container!)")
#endif
if threadSafe {
rwlock.unlock()
}
}
}
#if DEBUG
private let queue = DispatchQueue(label: "app.swiftgram.ios.trace", qos: .utility)
public func SGtrace(_ domain: String, what: @autoclosure() -> String) {
let string = what()
var rawTime = time_t()
time(&rawTime)
var timeinfo = tm()
localtime_r(&rawTime, &timeinfo)
var curTime = timeval()
gettimeofday(&curTime, nil)
let seconds = Int(curTime.tv_sec % 60) // Extracting the current second
let microseconds = curTime.tv_usec // Full microsecond precision
queue.async {
let result = String(format: "[%@] %d-%d-%d %02d:%02d:%02d.%06d %@", arguments: [domain, Int(timeinfo.tm_year) + 1900, Int(timeinfo.tm_mon + 1), Int(timeinfo.tm_mday), Int(timeinfo.tm_hour), Int(timeinfo.tm_min), seconds, microseconds, string])
print(result)
}
}
#endif