This commit is contained in:
Kylmakalle 2025-02-04 03:13:58 +02:00
parent 88f2f4d299
commit 82aa7f0857
22 changed files with 694 additions and 28 deletions

View File

@ -143,3 +143,46 @@ public func getSGAPIRegDate(token: String, deviceToken: String, userId: Int64) -
}
}
}
public func postSGReceipt(token: String, deviceToken: String, encodedReceiptData: Data) -> Signal<Void, SGAPIError> {
return Signal { subscriber in
let url = URL(string: buildApiUrl("validate"))!
let headers = [
SG_API_AUTHORIZATION_HEADER: "Token \(token)",
SG_API_DEVICE_TOKEN_HEADER: deviceToken
]
let completed = Atomic<Bool>(value: false)
var request = URLRequest(url: url)
headers.forEach { key, value in
request.addValue(value, forHTTPHeaderField: key)
}
request.httpMethod = "POST"
request.httpBody = encodedReceiptData
let dataSignal = requestsCustom(request: request).start(next: { data, urlResponse in
let _ = completed.swap(true)
if let httpResponse = urlResponse as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200...299:
subscriber.putCompletion()
default:
subscriber.putError(.generic("Error posting Receipt: \(httpResponse.statusCode). Response: \(String(data: data, encoding: .utf8) ?? "")"))
}
} else {
subscriber.putError(.generic("Not an HTTP response: \(String(describing: urlResponse))"))
}
}, error: { error in
subscriber.putError(.generic("Error posting Receipt: \(String(describing: error))"))
})
return ActionDisposable {
if !completed.with({ $0 }) {
dataSignal.dispose()
}
}
}
}

View File

@ -5,6 +5,7 @@ public struct SGConfig: Codable {
public var apiUrl: String = "https://api.swiftgram.app"
public var webappUrl: String = "https://my.swiftgram.app"
public var botUsername: String = "SwiftgramBot"
public var iaps: [String] = []
}
private func parseSGConfig(_ jsonString: String) -> SGConfig {

View File

@ -839,6 +839,7 @@ private enum SGDebugActions: String {
case flexing
case fileManager
case clearRegDateCache
case debugIAP
}
private enum SGDebugToggles: String {
@ -862,6 +863,7 @@ private func SGDebugControllerEntries(presentationData: PresentationData) -> [SG
#if DEBUG
entries.append(.action(id: id.count, section: .base, actionType: .flexing, text: "FLEX", kind: .generic))
entries.append(.action(id: id.count, section: .base, actionType: .fileManager, text: "FileManager", kind: .generic))
entries.append(.action(id: id.count, section: .base, actionType: .debugIAP, text: "Buy", kind: .generic))
#endif
if SGSimpleSettings.shared.b {
@ -1020,6 +1022,10 @@ public func sgDebugController(context: AccountContext) -> ViewController {
nil)
}
#endif
case .debugIAP:
#if DEBUG
preconditionFailure("IAP")
#endif
}
})

20
Swiftgram/SGIAP/BUILD Normal file
View File

@ -0,0 +1,20 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "SGIAP",
module_name = "SGIAP",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//Swiftgram/SGLogging:SGLogging",
"//Swiftgram/SGConfig:SGConfig",
"//submodules/AppBundle:AppBundle"
],
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,328 @@
import StoreKit
import SGConfig
import SGLogging
import AppBundle
private final class CurrencyFormatterEntry {
public let symbol: String
public let thousandsSeparator: String
public let decimalSeparator: String
public let symbolOnLeft: Bool
public let spaceBetweenAmountAndSymbol: Bool
public let decimalDigits: Int
public init(symbol: String, thousandsSeparator: String, decimalSeparator: String, symbolOnLeft: Bool, spaceBetweenAmountAndSymbol: Bool, decimalDigits: Int) {
self.symbol = symbol
self.thousandsSeparator = thousandsSeparator
self.decimalSeparator = decimalSeparator
self.symbolOnLeft = symbolOnLeft
self.spaceBetweenAmountAndSymbol = spaceBetweenAmountAndSymbol
self.decimalDigits = decimalDigits
}
}
private func getCurrencyExp(currency: String) -> Int {
switch currency {
case "CLF":
return 4
case "BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND":
return 3
case "BIF", "BYR", "CLP", "CVE", "DJF", "GNF", "ISK", "JPY", "KMF", "KRW", "MGA", "PYG", "RWF", "UGX", "UYI", "VND", "VUV", "XAF", "XOF", "XPF":
return 0
case "MRO":
return 1
default:
return 2
}
}
private func loadCurrencyFormatterEntries() -> [String: CurrencyFormatterEntry] {
guard let filePath = getAppBundle().path(forResource: "currencies", ofType: "json") else {
return [:]
}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else {
return [:]
}
guard let object = try? JSONSerialization.jsonObject(with: data, options: []), let dict = object as? [String: AnyObject] else {
return [:]
}
var result: [String: CurrencyFormatterEntry] = [:]
for (code, contents) in dict {
if let contentsDict = contents as? [String: AnyObject] {
let entry = CurrencyFormatterEntry(
symbol: contentsDict["symbol"] as! String,
thousandsSeparator: contentsDict["thousandsSeparator"] as! String,
decimalSeparator: contentsDict["decimalSeparator"] as! String,
symbolOnLeft: (contentsDict["symbolOnLeft"] as! NSNumber).boolValue,
spaceBetweenAmountAndSymbol: (contentsDict["spaceBetweenAmountAndSymbol"] as! NSNumber).boolValue,
decimalDigits: getCurrencyExp(currency: code.uppercased())
)
result[code] = entry
result[code.lowercased()] = entry
}
}
return result
}
private let currencyFormatterEntries = loadCurrencyFormatterEntries()
private func fractionalValueToCurrencyAmount(value: Double, currency: String) -> Int64? {
guard let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] else {
return nil
}
var factor: Double = 1.0
for _ in 0 ..< entry.decimalDigits {
factor *= 10.0
}
if value > Double(Int64.max) / factor {
return nil
} else {
return Int64(value * factor)
}
}
public extension Notification.Name {
static let SGIAPHelperPurchaseNotification = Notification.Name("SGIAPPurchaseNotification")
static let SGIAPHelperErrorNotification = Notification.Name("SGIAPErrorNotification")
}
public final class SGIAP: NSObject {
private var productRequest: SKProductsRequest?
private var productsRequestCompletion: (([SKProduct]) -> Void)?
private var purchaseCompletion: ((Bool, Error?) -> Void)?
public final class SGProduct: Equatable {
private lazy var numberFormatter: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .currency
numberFormatter.locale = self.skProduct.priceLocale
return numberFormatter
}()
let skProduct: SKProduct
init(skProduct: SKProduct) {
self.skProduct = skProduct
}
public var id: String {
return self.skProduct.productIdentifier
}
public var isSubscription: Bool {
if #available(iOS 12.0, *) {
return self.skProduct.subscriptionGroupIdentifier != nil
} else {
return self.skProduct.subscriptionPeriod != nil
}
}
public var price: String {
return self.numberFormatter.string(from: self.skProduct.price) ?? ""
}
public func pricePerMonth(_ monthsCount: Int) -> String {
let price = self.skProduct.price.dividing(by: NSDecimalNumber(value: monthsCount)).round(2)
return self.numberFormatter.string(from: price) ?? ""
}
public func defaultPrice(_ value: NSDecimalNumber, monthsCount: Int) -> String {
let price = value.multiplying(by: NSDecimalNumber(value: monthsCount)).round(2)
let prettierPrice = price
.multiplying(by: NSDecimalNumber(value: 2))
.rounding(accordingToBehavior:
NSDecimalNumberHandler(
roundingMode: .up,
scale: Int16(0),
raiseOnExactness: false,
raiseOnOverflow: false,
raiseOnUnderflow: false,
raiseOnDivideByZero: false
)
)
.dividing(by: NSDecimalNumber(value: 2))
.subtracting(NSDecimalNumber(value: 0.01))
return self.numberFormatter.string(from: prettierPrice) ?? ""
}
public func multipliedPrice(count: Int) -> String {
let price = self.skProduct.price.multiplying(by: NSDecimalNumber(value: count)).round(2)
let prettierPrice = price
.multiplying(by: NSDecimalNumber(value: 2))
.rounding(accordingToBehavior:
NSDecimalNumberHandler(
roundingMode: .up,
scale: Int16(0),
raiseOnExactness: false,
raiseOnOverflow: false,
raiseOnUnderflow: false,
raiseOnDivideByZero: false
)
)
.dividing(by: NSDecimalNumber(value: 2))
.subtracting(NSDecimalNumber(value: 0.01))
return self.numberFormatter.string(from: prettierPrice) ?? ""
}
public var priceValue: NSDecimalNumber {
return self.skProduct.price
}
public var priceCurrencyAndAmount: (currency: String, amount: Int64) {
if let currencyCode = self.numberFormatter.currencyCode,
let amount = fractionalValueToCurrencyAmount(value: self.priceValue.doubleValue, currency: currencyCode) {
return (currencyCode, amount)
} else {
return ("", 0)
}
}
public static func ==(lhs: SGProduct, rhs: SGProduct) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.isSubscription != rhs.isSubscription {
return false
}
if lhs.priceValue != rhs.priceValue {
return false
}
return true
}
}
public init(foo: Bool = false) {
super.init()
SKPaymentQueue.default().add(self)
self.requestProducts()
}
deinit {
SKPaymentQueue.default().remove(self)
}
var canMakePayments: Bool {
return SKPaymentQueue.canMakePayments()
}
public func buyProduct(_ product: SKProduct) {
SGLogger.shared.log("SGIAP", "Buying \(product.productIdentifier)...")
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
private func requestProducts() {
SGLogger.shared.log("SGIAP", "Requesting products...")
let productRequest = SKProductsRequest(productIdentifiers: Set(SG_CONFIG.iaps))
productRequest.delegate = self
productRequest.start()
self.productRequest = productRequest
}
public func restorePurchases(completion: @escaping (RestoreState) -> Void) {
SGLogger.shared.log("SGIAP", "Restoring purchases...")
let paymentQueue = SKPaymentQueue.default()
paymentQueue.restoreCompletedTransactions()
}
public enum RestoreState {
case succeed(Bool)
case failed
}
}
extension SGIAP: SKProductsRequestDelegate {
public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
self.productRequest = nil
DispatchQueue.main.async {
let products = response.products
SGLogger.shared.log("SGIAP", "Received products \(products.map({ $0.productIdentifier }).joined(separator: ", "))")
}
}
public func request(_ request: SKRequest, didFailWithError error: Error) {
SGLogger.shared.log("SGIAP", "Failed to load list of products. Error \(error.localizedDescription)")
self.productRequest = nil
}
}
extension SGIAP: SKPaymentTransactionObserver {
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
SGLogger.shared.log("SGIAP", "paymentQueue transactions \(transactions.count)")
for transaction in transactions {
SGLogger.shared.log("SGIAP", "Transaction state for \(transaction.payment.productIdentifier): \(transaction.transactionState)")
switch transaction.transactionState {
case .purchased, .restored:
NotificationCenter.default.post(name: .SGIAPHelperPurchaseNotification, object: transaction)
case .purchasing, .deferred:
break
case .failed:
if let transactionError = transaction.error as NSError?,
let localizedDescription = transaction.error?.localizedDescription,
transactionError.code != SKError.paymentCancelled.rawValue {
SGLogger.shared.log("SGIAP", "Transaction Error: \(localizedDescription)")
}
NotificationCenter.default.post(name: .SGIAPHelperErrorNotification, object: transaction)
default:
SGLogger.shared.log("SGIAP", "Unknown transaction state \(transaction.transactionState). Finishing transaction.")
SKPaymentQueue.default().finishTransaction(transaction)
}
}
}
}
private extension NSDecimalNumber {
func round(_ decimals: Int) -> NSDecimalNumber {
return self.rounding(accordingToBehavior:
NSDecimalNumberHandler(roundingMode: .down,
scale: Int16(decimals),
raiseOnExactness: false,
raiseOnOverflow: false,
raiseOnUnderflow: false,
raiseOnDivideByZero: false))
}
func prettyPrice() -> NSDecimalNumber {
return self.multiplying(by: NSDecimalNumber(value: 2))
.rounding(accordingToBehavior:
NSDecimalNumberHandler(
roundingMode: .plain,
scale: Int16(0),
raiseOnExactness: false,
raiseOnOverflow: false,
raiseOnUnderflow: false,
raiseOnDivideByZero: false
)
)
.dividing(by: NSDecimalNumber(value: 2))
.subtracting(NSDecimalNumber(value: 0.01))
}
}
public func getPurchaceReceiptData() -> Data? {
var receiptData: Data?
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
do {
receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
} catch {
SGLogger.shared.log("SGIAP", "Couldn't read receipt data with error: \(error.localizedDescription)")
}
} else {
SGLogger.shared.log("SGIAP", "Couldn't find receipt path")
}
return receiptData
}

View File

@ -23,22 +23,23 @@ public func makeIqtpQuery(_ api: Int, _ method: String, _ args: [String] = []) -
}
public func sgIqtpQuery(engine: TelegramEngine, query: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal<SGIQTPResponse?, NoError> {
let queryId = arc4random()
#if DEBUG
SGLogger.shared.log("SGIQTP", "Query: \(query)")
SGLogger.shared.log("SGIQTP", "[\(queryId)] Query: \(query)")
#else
SGLogger.shared.log("SGIQTP", "Query")
SGLogger.shared.log("SGIQTP", "[\(queryId)] Query")
#endif
return engine.peers.resolvePeerByName(name: SG_CONFIG.botUsername, referrer: nil)
return engine.peers.resolvePeerByName(forIQTP: true, name: SG_CONFIG.botUsername, referrer: nil)
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
guard case let .result(result) = result else {
SGLogger.shared.log("SGIQTP", "Failed to resolve peer")
SGLogger.shared.log("SGIQTP", "[\(queryId)] Failed to resolve peer \(SG_CONFIG.botUsername)")
return .complete()
}
return .single(result)
}
|> mapToSignal { peer -> Signal<ChatContextResultCollection?, NoError> in
guard let peer = peer else {
SGLogger.shared.log("SGIQTP", "Empty peer")
SGLogger.shared.log("SGIQTP", "[\(queryId)] Empty peer")
return .single(nil)
}
return engine.messages.requestChatContextResults(botId: peer.id, peerId: engine.account.peerId, query: query, offset: "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults)
@ -46,13 +47,13 @@ public func sgIqtpQuery(engine: TelegramEngine, query: String, incompleteResults
return results?.results
}
|> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in
SGLogger.shared.log("SGIQTP", "Failed to request inline results")
SGLogger.shared.log("SGIQTP", "[\(queryId)] Failed to request inline results")
return .single(nil)
}
}
|> map { contextResult -> SGIQTPResponse? in
guard let contextResult, let firstResult = contextResult.results.first else {
SGLogger.shared.log("SGIQTP", "Empty inline result")
SGLogger.shared.log("SGIQTP", "[\(queryId)] Empty inline result")
return nil
}
@ -70,7 +71,7 @@ public func sgIqtpQuery(engine: TelegramEngine, query: String, incompleteResults
description: firstResult.description,
text: t
)
SGLogger.shared.log("SGIQTP", "Response: \(response)")
SGLogger.shared.log("SGIQTP", "[\(queryId)] Response: \(response)")
return response
}
}

View File

@ -126,6 +126,7 @@ public class SGSimpleSettings {
case inputToolbar
case pinnedMessageNotifications
case mentionsAndRepliesNotifications
case primaryUserId
}
public enum DownloadSpeedBoostValues: String, CaseIterable {
@ -236,7 +237,8 @@ public class SGSimpleSettings {
Keys.confirmCalls.rawValue: true,
Keys.videoPIPSwipeDirection.rawValue: VideoPIPSwipeDirection.up.rawValue,
Keys.messageFilterKeywords.rawValue: [],
Keys.inputToolbar.rawValue: false
Keys.inputToolbar.rawValue: false,
Keys.primaryUserId.rawValue: ""
]
public static let groupDefaultValues: [String: Any] = [
@ -437,6 +439,9 @@ public class SGSimpleSettings {
@UserDefault(key: Keys.mentionsAndRepliesNotifications.rawValue, userDefaults: UserDefaults(suiteName: appGroupIdentifier) ?? .standard)
public var mentionsAndRepliesNotifications: String
@UserDefault(key: Keys.primaryUserId.rawValue)
public var primaryUserId: String
}
extension SGSimpleSettings {

9
Swiftgram/SGStatus/BUILD Normal file
View File

@ -0,0 +1,9 @@
filegroup(
name = "SGStatus",
srcs = glob([
"Sources/**/*.swift",
]),
visibility = [
"//visibility:public",
],
)

View File

@ -0,0 +1,41 @@
import Foundation
import SwiftSignalKit
import TelegramCore
public struct SGStatus: Equatable, Codable {
public var status: Int64
public static var `default`: SGStatus {
return SGStatus(status: 1)
}
public init(status: Int64) {
self.status = status
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
self.status = try container.decodeIfPresent(Int64.self, forKey: "status") ?? 1
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encodeIfPresent(self.status, forKey: "status")
}
}
public func updateSGStatusInteractively(accountManager: AccountManager<TelegramAccountManagerTypes>, _ f: @escaping (SGStatus) -> SGStatus) -> Signal<Void, NoError> {
return accountManager.transaction { transaction -> Void in
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.sgStatus, { entry in
let currentSettings: SGStatus
if let entry = entry?.get(SGStatus.self) {
currentSettings = entry
} else {
currentSettings = SGStatus.default
}
return SharedPreferencesEntry(f(currentSettings))
})
}
}

View File

@ -27,7 +27,17 @@ public func ignoreSignalErrors<T, E>(onError: ((E) -> Void)? = nil) -> (Signal<T
}
}
extension Signal where E: Error {
// Wrapper for non-Error types
public struct SignalError<E>: Error {
public let error: E
public init(_ error: E) {
self.error = error
}
}
// Extension for Signals with Error types
extension Signal {
@available(iOS 13.0, *)
public func awaitable() async throws -> T {
return try await withCheckedThrowingContinuation { continuation in
@ -38,7 +48,11 @@ extension Signal where E: Error {
disposable?.dispose()
},
error: { error in
continuation.resume(throwing: error)
if let error = error as? Error {
continuation.resume(throwing: error)
} else {
continuation.resume(throwing: SignalError(error))
}
disposable?.dispose()
},
completed: {
@ -49,6 +63,7 @@ extension Signal where E: Error {
}
}
// Extension for Signals with NoError
extension Signal where E == NoError {
@available(iOS 13.0, *)
public func awaitable() async -> T {
@ -71,6 +86,7 @@ extension Signal where E == NoError {
}
}
// Extension for general Signal types - AsyncStream support
extension Signal {
@available(iOS 13.0, *)
public func awaitableStream() -> AsyncStream<T> {
@ -94,7 +110,7 @@ extension Signal {
}
}
// Extension for NoError Signal types - AsyncStream support
extension Signal where E == NoError {
@available(iOS 13.0, *)
public func awaitableStream() -> AsyncStream<T> {

View File

@ -933,6 +933,8 @@ public protocol SharedAccountContext: AnyObject {
var automaticMediaDownloadSettings: Signal<MediaAutoDownloadSettings, NoError> { get }
var currentAutodownloadSettings: Atomic<AutodownloadSettings> { get }
var immediateExperimentalUISettings: ExperimentalUISettings { get }
// MARK: Swiftgram
var immediateSGStatus: SGStatus { get }
var currentInAppNotificationSettings: Atomic<InAppNotificationSettings> { get }
var currentMediaInputSettings: Atomic<MediaInputSettings> { get }
var currentStickerSettings: Atomic<StickerSettings> { get }

View File

@ -52,3 +52,18 @@ public func activeAccountsAndPeers(context: AccountContext, includePrimary: Bool
}
}
}
// MARK: Swiftgram
public func getContextForUserId(context: AccountContext, userId: Int64) -> Signal<AccountContext?, NoError> {
if context.account.peerId.id._internalGetInt64Value() == userId {
return .single(context)
}
return context.sharedContext.activeAccountContexts
|> take(1)
|> map { _, activeAccounts, _ -> AccountContext? in
if let account = activeAccounts.first(where: { $0.1.account.peerId.id._internalGetInt64Value() == userId }) {
return account.1
}
return nil
}
}

View File

@ -236,12 +236,12 @@ public final class InAppPurchaseManager: NSObject {
super.init()
SKPaymentQueue.default().add(self)
// SKPaymentQueue.default().add(self) // MARK: Swiftgram
self.requestProducts()
}
deinit {
SKPaymentQueue.default().remove(self)
// SKPaymentQueue.default().remove(self) // MARK: Swiftgram
}
var canMakePayments: Bool {
@ -249,6 +249,7 @@ public final class InAppPurchaseManager: NSObject {
}
private func requestProducts() {
if ({ return true }()) { return } // MARK: Swiftgram
Logger.shared.log("InAppPurchaseManager", "Requesting products")
let productRequest = SKProductsRequest(productIdentifiers: Set(productIdentifiers))
productRequest.delegate = self
@ -296,7 +297,7 @@ public final class InAppPurchaseManager: NSObject {
let payment = SKMutablePayment(product: product.skProduct)
payment.applicationUsername = accountPeerId
payment.quantity = Int(quantity)
SKPaymentQueue.default().add(payment)
// SKPaymentQueue.default().add(payment) // MARK: Swiftgram
let productIdentifier = payment.productIdentifier
let signal = Signal<PurchaseState, PurchaseError> { subscriber in

View File

@ -31,6 +31,7 @@ public let SGLocalizations: [LocalizationInfo] = [
LocalizationInfo(languageCode: "zhcncc", baseLanguageCode: "zh-hans-raw", customPluralizationCode: "zh", title: "Chinese (Simplified) zhcncc", localizedTitle: "简体中文 (聪聪) - 已更完", isOfficial: true, totalStringCount: 7160, translatedStringCount: 7144, platformUrl: "https://translations.telegram.org/zhcncc/"),
LocalizationInfo(languageCode: "taiwan", baseLanguageCode: "zh-hant-raw", customPluralizationCode: "zh", title: "Chinese (zh-Hant-TW) @zh_Hant_TW", localizedTitle: "正體中文", isOfficial: true, totalStringCount: 7160, translatedStringCount: 3761, platformUrl: "https://translations.telegram.org/taiwan/"),
LocalizationInfo(languageCode: "hongkong", baseLanguageCode: "zh-hant-raw", customPluralizationCode: "zh", title: "Chinese (Hong Kong)", localizedTitle: "中文(香港)", isOfficial: true, totalStringCount: 7358, translatedStringCount: 6083, platformUrl: "https://translations.telegram.org/hongkong/"),
// TODO(swiftgram): Japanese beta
// baseLanguageCode is actually nil, since it's an "official" beta language
LocalizationInfo(languageCode: "vi-raw", baseLanguageCode: "vi-raw", customPluralizationCode: "vi", title: "Vietnamese", localizedTitle: "Tiếng Việt (beta)", isOfficial: true, totalStringCount: 7160, translatedStringCount: 3795, platformUrl: "https://translations.telegram.org/vi/"),
LocalizationInfo(languageCode: "hi-raw", baseLanguageCode: "hi-raw", customPluralizationCode: "hi", title: "Hindi", localizedTitle: "हिन्दी (beta)", isOfficial: true, totalStringCount: 7358, translatedStringCount: 992, platformUrl: "https://translations.telegram.org/hi/"),

View File

@ -52,7 +52,7 @@ public struct RequestChatContextResultsResult {
}
}
func _internal_requestChatContextResults(account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> {
func _internal_requestChatContextResults(forIQTP: Bool = false, account: Account, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> {
return account.postbox.transaction { transaction -> (bot: Peer, peer: Peer)? in
if let bot = transaction.getPeer(botId), let peer = transaction.getPeer(peerId) {
return (bot, peer)
@ -77,7 +77,7 @@ func _internal_requestChatContextResults(account: Account, botId: PeerId, peerId
return .single(nil)
}
return account.postbox.transaction { transaction -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> in
return account.postbox.transaction(ignoreDisabled: forIQTP) { transaction -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> in
var staleResult: RequestChatContextResultsResult?
if offset.isEmpty && location == nil {
@ -127,6 +127,7 @@ func _internal_requestChatContextResults(account: Account, botId: PeerId, peerId
return ChatContextResultCollection(apiResults: result, botId: bot.id, peerId: peerId, query: query, geoPoint: location)
}
|> mapError { error -> RequestChatContextResultsError in
print("error.errorDescription", error.errorDescription ?? "")
if error.errorDescription == "BOT_INLINE_GEO_REQUIRED" {
return .locationRequired
} else {
@ -138,7 +139,7 @@ func _internal_requestChatContextResults(account: Account, botId: PeerId, peerId
return .single(nil)
}
return account.postbox.transaction { transaction -> RequestChatContextResultsResult? in
return account.postbox.transaction(ignoreDisabled: forIQTP) { transaction -> RequestChatContextResultsResult? in
if result.cacheTimeout > 10, offset.isEmpty && location == nil {
if let resultData = try? JSONEncoder().encode(result) {
let requestData = RequestData(version: requestVersion, botId: botId, peerId: peerId, query: query)

View File

@ -361,8 +361,8 @@ public extension TelegramEngine {
return _internal_updateStarsReactionIsAnonymous(account: self.account, messageId: id, isAnonymous: isAnonymous)
}
public func requestChatContextResults(botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> {
return _internal_requestChatContextResults(account: self.account, botId: botId, peerId: peerId, query: query, location: location, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults)
public func requestChatContextResults(forIQTP: Bool = false, botId: PeerId, peerId: PeerId, query: String, location: Signal<(Double, Double)?, NoError> = .single(nil), offset: String, incompleteResults: Bool = false, staleCachedResults: Bool = false) -> Signal<RequestChatContextResultsResult?, RequestChatContextResultsError> {
return _internal_requestChatContextResults(forIQTP: Bool = false, account: self.account, botId: botId, peerId: peerId, query: query, location: location, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults)
}
public func removeRecentlyUsedHashtag(string: String) -> Signal<Void, NoError> {

View File

@ -179,7 +179,7 @@ public extension TelegramEngine {
return _internal_inactiveChannelList(network: self.account.network)
}
public func resolvePeerByName(name: String, referrer: String?, ageLimit: Int32 = 2 * 60 * 60 * 24) -> Signal<ResolvePeerResult, NoError> {
public func resolvePeerByName(forIQTP: Bool = false, name: String, referrer: String?, ageLimit: Int32 = 2 * 60 * 60 * 24) -> Signal<ResolvePeerResult, NoError> {
return _internal_resolvePeerByName(account: self.account, name: name, referrer: referrer, ageLimit: ageLimit)
|> mapToSignal { result -> Signal<ResolvePeerResult, NoError> in
switch result {
@ -189,7 +189,7 @@ public extension TelegramEngine {
guard let peerId = peerId else {
return .single(.result(nil))
}
return self.account.postbox.transaction { transaction -> ResolvePeerResult in
return self.account.postbox.transaction(ignoreDisabled: forIQTP) { transaction -> ResolvePeerResult in
return .result(transaction.getPeer(peerId).flatMap(EnginePeer.init))
}
}

View File

@ -18,6 +18,7 @@ sgdeps = [
"//Swiftgram/SGDeviceToken:SGDeviceToken",
"//Swiftgram/SGDebugUI:SGDebugUI",
"//Swiftgram/SGInputToolbar:SGInputToolbar",
"//Swiftgram/SGIAP:SGIAP",
# "//Swiftgram/SGContentAnalysis:SGContentAnalysis"
]

View File

@ -1,4 +1,10 @@
// MARK: Swiftgram
import StoreKit
import SGIAP
import SGAPI
import SGDeviceToken
import SGAPIToken
import SGActionRequestHandlerSanitizer
import SGAPIWebSettings
import SGLogging
@ -1265,11 +1271,14 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
let _ = (context.context.sharedContext.presentationData.start(next: { presentationData in
SGLocalizationManager.shared.downloadLocale(presentationData.strings.baseLanguageCode)
}))
let _ = sgIqtpQuery(engine: context.context.engine, query: makeIqtpQuery(0, "b")).start(next: { response in
guard let response else { return }
SGLogger.shared.log("IQTP", "Response: \(response)")
SGSimpleSettings.shared.b = response.description == "1"
})
if #available(iOS 13.0, *) {
let _ = Task {
let primaryContext = await self.getPrimaryContext(anyContext: context.context)
SGLogger.shared.log("SGIAP", "Verifying Status \(primaryContext.sharedContext.immediateSGStatus.status) for: \(primaryContext.account.peerId.id._internalGetInt64Value())")
let _ = await self.fetchSGStatus(primaryContext: primaryContext)
}
}
}))
} else {
self.mainWindow.viewController = nil
@ -3051,3 +3060,136 @@ private func getMemoryConsumption() -> Int {
}
return Int(info.phys_footprint)
}
// MARK: Swiftgram
@available(iOS 13.0, *)
extension AppDelegate {
func setupIAP() {
NotificationCenter.default.addObserver(forName: .SGIAPHelperPurchaseNotification, object: nil, queue: nil) { [weak self] notification in
guard let strongSelf = self else { return }
if let transaction = notification.object as? SKPaymentTransaction {
let _ = (strongSelf.context.get()
|> take(1)
|> deliverOnMainQueue).start(next: { context in
guard let context = context else {
SGLogger.shared.log("SGIAP", "Empty app context (how?)")
SGLogger.shared.log("SGIAP", "Finishing transaction")
SKPaymentQueue.default().finishTransaction(transaction)
return
}
let _ = Task {
await strongSelf.sendReceiptForVerification(primaryContext: context.context)
await strongSelf.fetchSGStatus(primaryContext: context.context)
SGLogger.shared.log("SGIAP", "Finishing transaction")
SKPaymentQueue.default().finishTransaction(transaction)
}
})
} else {
SGLogger.shared.log("SGIAP", "Wrong object in SGIAPHelperPurchaseNotification")
}
}
}
func getPrimaryContext(anyContext context: AccountContext, fallbackToCurrent: Bool = false) async -> AccountContext {
var primaryUserId: Int64 = Int64(SGSimpleSettings.shared.primaryUserId) ?? 0
if primaryUserId == 0 {
primaryUserId = context.account.peerId.id._internalGetInt64Value()
SGLogger.shared.log("SGIAP", "Setting new primary user id: \(primaryUserId)")
SGSimpleSettings.shared.primaryUserId = String(primaryUserId)
}
var primaryContext = await getContextForUserId(context: context, userId: primaryUserId).awaitable()
if let primaryContext = primaryContext {
SGLogger.shared.log("SGIAP", "Got primary context for user id: \(primaryContext.account.peerId.id._internalGetInt64Value())")
return primaryContext
} else {
primaryContext = context
let newPrimaryUserId = context.account.peerId.id._internalGetInt64Value()
SGLogger.shared.log("SGIAP", "Primary context for user id \(primaryUserId) is nil! Falling back to current context with user id: \(context.account.peerId.id._internalGetInt64Value())")
SGLogger.shared.log("SGIAP", "Setting new primary user id: \(primaryUserId)")
SGSimpleSettings.shared.primaryUserId = String(newPrimaryUserId)
return context
}
}
func sendReceiptForVerification(primaryContext: AccountContext) async {
guard let receiptData = getPurchaceReceiptData() else {
return
}
let encodedReceiptData = receiptData.base64EncodedData(options: [])
var deviceToken: String?
var apiToken: String?
do {
async let deviceTokenTask = getDeviceToken().awaitable()
async let apiTokenTask = getSGApiToken(context: primaryContext).awaitable()
(deviceToken, apiToken) = try await (deviceTokenTask, apiTokenTask)
} catch {
SGLogger.shared.log("SGIAP", "Error getting device token or API token: \(error)")
return
}
if let deviceToken, let apiToken {
do {
let _ = try await postSGReceipt(token: apiToken,
deviceToken: deviceToken,
encodedReceiptData: encodedReceiptData).awaitable()
} catch {
SGLogger.shared.log("SGIAP", "Error: \(error)")
}
}
}
func fetchSGStatus(primaryContext: AccountContext) async {
// let currentShouldKeepConnection = await (primaryContext.account.network.shouldKeepConnection.get() |> take(1) |> deliverOnMainQueue).awaitable()
let currentShouldKeepConnection = false
let userId = primaryContext.account.peerId.id._internalGetInt64Value()
// SGLogger.shared.log("SGIAP", "User id \(userId) currently keeps connection: \(currentShouldKeepConnection)")
if !currentShouldKeepConnection {
SGLogger.shared.log("SGIAP", "Asking user id \(userId) to keep connection: true")
primaryContext.account.network.shouldKeepConnection.set(.single(true))
}
let iqtpResponse = await sgIqtpQuery(engine: primaryContext.engine, query: makeIqtpQuery(0, "s")).awaitable()
guard let iqtpResponse = iqtpResponse else {
SGLogger.shared.log("SGIAP", "IQTP response is nil!")
// if !currentShouldKeepConnection {
// SGLogger.shared.log("SGIAP", "Setting user id \(userId) keep connection back to false")
// primaryContext.account.network.shouldKeepConnection.set(.single(false))
// }
return
}
SGLogger.shared.log("SGIAP", "Got IQTP response: \(iqtpResponse)")
let _ = await updateSGStatusInteractively(accountManager: primaryContext.sharedContext.accountManager, { value in
var value = value
let newStatus: Int64
if let description = iqtpResponse.description, let status = Int64(description) {
newStatus = status
} else {
SGLogger.shared.log("SGIAP", "Can't parse IQTP response into status!")
newStatus = value.status // unparseable
}
let userId = primaryContext.account.peerId.id._internalGetInt64Value()
if value.status != newStatus {
SGLogger.shared.log("SGIAP", "Updating \(userId) status \(value.status) -> \(newStatus)")
if newStatus > 1 {
SGSimpleSettings.shared.primaryUserId = String(userId)
}
value.status = newStatus
} else {
SGLogger.shared.log("SGIAP", "Status \(value.status) for \(userId) hasn't changed")
}
return value
}).awaitable()
// if !currentShouldKeepConnection {
// SGLogger.shared.log("SGIAP", "Setting user id \(userId) keep connection back to false")
// primaryContext.account.network.shouldKeepConnection.set(.single(false))
// }
}
}

View File

@ -1,3 +1,5 @@
// MARK: Swiftgram
import SGIAP
import Foundation
import UIKit
import AsyncDisplayKit
@ -240,6 +242,13 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return self.immediateExperimentalUISettingsValue.with { $0 }
}
private var experimentalUISettingsDisposable: Disposable?
private var immediateSGStatusValue = Atomic<SGStatus>(value: SGStatus.default)
public var immediateSGStatus: SGStatus {
return self.immediateSGStatusValue.with { $0 }
}
private var sgStatusDisposable: Disposable?
public var presentGlobalController: (ViewController, Any?) -> Void = { _, _ in }
public var presentCrossfadeController: () -> Void = {}
@ -468,6 +477,14 @@ public final class SharedAccountContextImpl: SharedAccountContext {
let _ = immediateExperimentalUISettingsValue.swap(settings)
}
})
// MARK: Swiftgram
let immediateSGStatusValue = self.immediateSGStatusValue
self.sgStatusDisposable = (self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.sgStatus])
|> deliverOnMainQueue).start(next: { sharedData in
if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.sgStatus]?.get(SGStatus.self) {
let _ = immediateSGStatusValue.swap(settings)
}
})
let _ = self.contactDataManager?.personNameDisplayOrder().start(next: { order in
let _ = updateContactSettingsInteractively(accountManager: accountManager, { settings in
@ -3020,3 +3037,11 @@ private func peerInfoControllerImpl(context: AccountContext, updatedPresentation
}
return nil
}
// MARK: Swiftgram
//public extension SharedAccountContextImpl {
// public func initSGIAP() {
// self.sgIAP = SGIAP
// }
//}

View File

@ -1,9 +1,13 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
sgsrcs = [
"//Swiftgram/SGStatus:SGStatus"
]
swift_library(
name = "TelegramUIPreferences",
module_name = "TelegramUIPreferences",
srcs = glob([
srcs = sgsrcs + glob([
"Sources/**/*.swift",
]),
copts = [

View File

@ -21,6 +21,8 @@ public struct ApplicationSpecificPreferencesKeys {
}
private enum ApplicationSpecificSharedDataKeyValues: Int32 {
// MARK: Swiftgram
case sgStatus = 999
case inAppNotificationSettings = 0
case presentationPasscodeSettings = 1
case automaticMediaDownloadSettings = 2
@ -45,6 +47,8 @@ private enum ApplicationSpecificSharedDataKeyValues: Int32 {
}
public struct ApplicationSpecificSharedDataKeys {
// MARK: Swiftgram
public static let sgStatus = applicationSpecificSharedDataKey(ApplicationSpecificSharedDataKeyValues.sgStatus.rawValue)
public static let inAppNotificationSettings = applicationSpecificSharedDataKey(ApplicationSpecificSharedDataKeyValues.inAppNotificationSettings.rawValue)
public static let presentationPasscodeSettings = applicationSpecificSharedDataKey(ApplicationSpecificSharedDataKeyValues.presentationPasscodeSettings.rawValue)
public static let automaticMediaDownloadSettings = applicationSpecificSharedDataKey(ApplicationSpecificSharedDataKeyValues.automaticMediaDownloadSettings.rawValue)