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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,533 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
#if canImport(SGDeletedMessages)
import SGDeletedMessages
#endif
private enum AccountKind {
case authorized
case unauthorized
}
public struct AccountSupportUserInfo: Codable, Equatable {
public init() {
}
}
public enum TelegramAccountRecordAttribute: AccountRecordAttribute, Equatable {
enum CodingKeys: String, CodingKey {
case backupData
case environment
case sortOrder
case loggedOut
case supportUserInfo
case legacyRootObject = "_"
}
case backupData(AccountBackupDataAttribute)
case environment(AccountEnvironmentAttribute)
case sortOrder(AccountSortOrderAttribute)
case loggedOut(LoggedOutAccountAttribute)
case supportUserInfo(AccountSupportUserInfo)
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let backupData = try? container.decodeIfPresent(AccountBackupDataAttribute.self, forKey: .backupData) {
self = .backupData(backupData)
} else if let environment = try? container.decodeIfPresent(AccountEnvironmentAttribute.self, forKey: .environment) {
self = .environment(environment)
} else if let sortOrder = try? container.decodeIfPresent(AccountSortOrderAttribute.self, forKey: .sortOrder) {
self = .sortOrder(sortOrder)
} else if let loggedOut = try? container.decodeIfPresent(LoggedOutAccountAttribute.self, forKey: .loggedOut) {
self = .loggedOut(loggedOut)
} else if let supportUserInfo = try? container.decodeIfPresent(AccountSupportUserInfo.self, forKey: .supportUserInfo) {
self = .supportUserInfo(supportUserInfo)
} else {
let legacyRootObjectData = try! container.decode(AdaptedPostboxDecoder.RawObjectData.self, forKey: .legacyRootObject)
if legacyRootObjectData.typeHash == postboxEncodableTypeHash(AccountBackupDataAttribute.self) {
self = .backupData(try! AdaptedPostboxDecoder().decode(AccountBackupDataAttribute.self, from: legacyRootObjectData.data))
} else if legacyRootObjectData.typeHash == postboxEncodableTypeHash(AccountEnvironmentAttribute.self) {
self = .environment(try! AdaptedPostboxDecoder().decode(AccountEnvironmentAttribute.self, from: legacyRootObjectData.data))
} else if legacyRootObjectData.typeHash == postboxEncodableTypeHash(AccountSortOrderAttribute.self) {
self = .sortOrder(try! AdaptedPostboxDecoder().decode(AccountSortOrderAttribute.self, from: legacyRootObjectData.data))
} else if legacyRootObjectData.typeHash == postboxEncodableTypeHash(LoggedOutAccountAttribute.self) {
self = .loggedOut(try! AdaptedPostboxDecoder().decode(LoggedOutAccountAttribute.self, from: legacyRootObjectData.data))
} else {
preconditionFailure()
}
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .backupData(backupData):
try container.encode(backupData, forKey: .backupData)
case let .environment(environment):
try container.encode(environment, forKey: .environment)
case let .sortOrder(sortOrder):
try container.encode(sortOrder, forKey: .sortOrder)
case let .loggedOut(loggedOut):
try container.encode(loggedOut, forKey: .loggedOut)
case let .supportUserInfo(supportUserInfo):
try container.encode(supportUserInfo, forKey: .supportUserInfo)
}
}
public func isEqual(to: AccountRecordAttribute) -> Bool {
return self == to as? TelegramAccountRecordAttribute
}
}
public final class TelegramAccountManagerTypes: AccountManagerTypes {
public typealias Attribute = TelegramAccountRecordAttribute
}
private var declaredEncodables: Void = {
declareEncodable(UnauthorizedAccountState.self, f: { UnauthorizedAccountState(decoder: $0) })
declareEncodable(AuthorizedAccountState.self, f: { AuthorizedAccountState(decoder: $0) })
declareEncodable(TelegramUser.self, f: { TelegramUser(decoder: $0) })
declareEncodable(TelegramGroup.self, f: { TelegramGroup(decoder: $0) })
declareEncodable(TelegramChannel.self, f: { TelegramChannel(decoder: $0) })
declareEncodable(TelegramMediaImage.self, f: { TelegramMediaImage(decoder: $0) })
declareEncodable(TelegramMediaImageRepresentation.self, f: { TelegramMediaImageRepresentation(decoder: $0) })
declareEncodable(TelegramMediaContact.self, f: { TelegramMediaContact(decoder: $0) })
declareEncodable(TelegramMediaMap.self, f: { TelegramMediaMap(decoder: $0) })
declareEncodable(TelegramMediaFile.self, f: { TelegramMediaFile(decoder: $0) })
declareEncodable(TelegramMediaFileAttribute.self, f: { TelegramMediaFileAttribute(decoder: $0) })
declareEncodable(CloudFileMediaResource.self, f: { CloudFileMediaResource(decoder: $0) })
declareEncodable(ChannelState.self, f: { ChannelState(decoder: $0) })
declareEncodable(RegularChatState.self, f: { RegularChatState(decoder: $0) })
declareEncodable(InlineBotMessageAttribute.self, f: { InlineBotMessageAttribute(decoder: $0) })
declareEncodable(InlineBusinessBotMessageAttribute.self, f: { InlineBusinessBotMessageAttribute(decoder: $0) })
declareEncodable(TextEntitiesMessageAttribute.self, f: { TextEntitiesMessageAttribute(decoder: $0) })
declareEncodable(ReplyMessageAttribute.self, f: { ReplyMessageAttribute(decoder: $0) })
declareEncodable(QuotedReplyMessageAttribute.self, f: { QuotedReplyMessageAttribute(decoder: $0) })
declareEncodable(ReplyStoryAttribute.self, f: { ReplyStoryAttribute(decoder: $0) })
declareEncodable(ReplyThreadMessageAttribute.self, f: { ReplyThreadMessageAttribute(decoder: $0) })
declareEncodable(ReactionsMessageAttribute.self, f: { ReactionsMessageAttribute(decoder: $0) })
declareEncodable(PendingReactionsMessageAttribute.self, f: { PendingReactionsMessageAttribute(decoder: $0) })
declareEncodable(PendingStarsReactionsMessageAttribute.self, f: { PendingStarsReactionsMessageAttribute(decoder: $0) })
declareEncodable(CloudDocumentMediaResource.self, f: { CloudDocumentMediaResource(decoder: $0) })
declareEncodable(TelegramMediaWebpage.self, f: { TelegramMediaWebpage(decoder: $0) })
declareEncodable(ViewCountMessageAttribute.self, f: { ViewCountMessageAttribute(decoder: $0) })
declareEncodable(ForwardCountMessageAttribute.self, f: { ForwardCountMessageAttribute(decoder: $0) })
declareEncodable(BoostCountMessageAttribute.self, f: { BoostCountMessageAttribute(decoder: $0) })
declareEncodable(ParticipantRankMessageAttribute.self, f: { ParticipantRankMessageAttribute(decoder: $0) })
// MARK: - GLEGram
declareEncodable(GhostDelayedSendAttribute.self, f: { GhostDelayedSendAttribute(decoder: $0) })
#if canImport(SGDeletedMessages)
declareEncodable(SGDeletedMessageAttribute.self, f: { SGDeletedMessageAttribute(decoder: $0) })
#endif
declareEncodable(NotificationInfoMessageAttribute.self, f: { NotificationInfoMessageAttribute(decoder: $0) })
declareEncodable(TelegramMediaAction.self, f: { TelegramMediaAction(decoder: $0) })
declareEncodable(TelegramPeerNotificationSettings.self, f: { TelegramPeerNotificationSettings(decoder: $0) })
declareEncodable(CachedUserData.self, f: { CachedUserData(decoder: $0) })
declareEncodable(BotInfo.self, f: { BotInfo(decoder: $0) })
declareEncodable(CachedGroupData.self, f: { CachedGroupData(decoder: $0) })
declareEncodable(CachedChannelData.self, f: { CachedChannelData(decoder: $0) })
declareEncodable(TelegramUserPresence.self, f: { TelegramUserPresence(decoder: $0) })
declareEncodable(LocalFileMediaResource.self, f: { LocalFileMediaResource(decoder: $0) })
declareEncodable(StickerPackCollectionInfo.self, f: { StickerPackCollectionInfo(decoder: $0) })
declareEncodable(StickerPackItem.self, f: { StickerPackItem(decoder: $0) })
declareEncodable(LocalFileReferenceMediaResource.self, f: { LocalFileReferenceMediaResource(decoder: $0) })
declareEncodable(OutgoingMessageInfoAttribute.self, f: { OutgoingMessageInfoAttribute(decoder: $0) })
declareEncodable(ForwardSourceInfoAttribute.self, f: { ForwardSourceInfoAttribute(decoder: $0) })
declareEncodable(SourceReferenceMessageAttribute.self, f: { SourceReferenceMessageAttribute(decoder: $0) })
declareEncodable(SourceAuthorInfoMessageAttribute.self, f: { SourceAuthorInfoMessageAttribute(decoder: $0) })
declareEncodable(EditedMessageAttribute.self, f: { EditedMessageAttribute(decoder: $0) })
declareEncodable(ReplyMarkupMessageAttribute.self, f: { ReplyMarkupMessageAttribute(decoder: $0) })
declareEncodable(OutgoingChatContextResultMessageAttribute.self, f: { OutgoingChatContextResultMessageAttribute(decoder: $0) })
declareEncodable(HttpReferenceMediaResource.self, f: { HttpReferenceMediaResource(decoder: $0) })
declareEncodable(WebFileReferenceMediaResource.self, f: { WebFileReferenceMediaResource(decoder: $0) })
declareEncodable(EmptyMediaResource.self, f: { EmptyMediaResource(decoder: $0) })
declareEncodable(TelegramSecretChat.self, f: { TelegramSecretChat(decoder: $0) })
declareEncodable(SecretChatState.self, f: { SecretChatState(decoder: $0) })
declareEncodable(SecretChatIncomingEncryptedOperation.self, f: { SecretChatIncomingEncryptedOperation(decoder: $0) })
declareEncodable(SecretChatIncomingDecryptedOperation.self, f: { SecretChatIncomingDecryptedOperation(decoder: $0) })
declareEncodable(SecretChatOutgoingOperation.self, f: { SecretChatOutgoingOperation(decoder: $0) })
declareEncodable(SecretFileMediaResource.self, f: { SecretFileMediaResource(decoder: $0) })
declareEncodable(CloudChatRemoveMessagesOperation.self, f: { CloudChatRemoveMessagesOperation(decoder: $0) })
declareEncodable(AutoremoveTimeoutMessageAttribute.self, f: { AutoremoveTimeoutMessageAttribute(decoder: $0) })
declareEncodable(AutoclearTimeoutMessageAttribute.self, f: { AutoclearTimeoutMessageAttribute(decoder: $0) })
declareEncodable(CloudChatRemoveChatOperation.self, f: { CloudChatRemoveChatOperation(decoder: $0) })
declareEncodable(SynchronizePinnedChatsOperation.self, f: { SynchronizePinnedChatsOperation(decoder: $0) })
declareEncodable(SynchronizeConsumeMessageContentsOperation.self, f: { SynchronizeConsumeMessageContentsOperation(decoder: $0) })
declareEncodable(CloudChatClearHistoryOperation.self, f: { CloudChatClearHistoryOperation(decoder: $0) })
declareEncodable(OutgoingContentInfoMessageAttribute.self, f: { OutgoingContentInfoMessageAttribute(decoder: $0) })
declareEncodable(ConsumableContentMessageAttribute.self, f: { ConsumableContentMessageAttribute(decoder: $0) })
declareEncodable(TelegramMediaGame.self, f: { TelegramMediaGame(decoder: $0) })
declareEncodable(TelegramMediaInvoice.self, f: { TelegramMediaInvoice(decoder: $0) })
declareEncodable(TelegramMediaWebFile.self, f: { TelegramMediaWebFile(decoder: $0) })
declareEncodable(SynchronizeInstalledStickerPacksOperation.self, f: { SynchronizeInstalledStickerPacksOperation(decoder: $0) })
declareEncodable(SynchronizeMarkFeaturedStickerPacksAsSeenOperation.self, f: { SynchronizeMarkFeaturedStickerPacksAsSeenOperation(decoder: $0) })
declareEncodable(SynchronizeChatInputStateOperation.self, f: { SynchronizeChatInputStateOperation(decoder: $0) })
declareEncodable(SynchronizeSavedGifsOperation.self, f: { SynchronizeSavedGifsOperation(decoder: $0) })
declareEncodable(SynchronizeSavedStickersOperation.self, f: { SynchronizeSavedStickersOperation(decoder: $0) })
declareEncodable(SynchronizeRecentlyUsedMediaOperation.self, f: { SynchronizeRecentlyUsedMediaOperation(decoder: $0) })
declareEncodable(SynchronizeLocalizationUpdatesOperation.self, f: { SynchronizeLocalizationUpdatesOperation(decoder: $0) })
declareEncodable(ChannelMessageStateVersionAttribute.self, f: { ChannelMessageStateVersionAttribute(decoder: $0) })
declareEncodable(PeerGroupMessageStateVersionAttribute.self, f: { PeerGroupMessageStateVersionAttribute(decoder: $0) })
declareEncodable(CachedSecretChatData.self, f: { CachedSecretChatData(decoder: $0) })
declareEncodable(AuthorSignatureMessageAttribute.self, f: { AuthorSignatureMessageAttribute(decoder: $0) })
declareEncodable(TelegramMediaExpiredContent.self, f: { TelegramMediaExpiredContent(decoder: $0) })
declareEncodable(ConsumablePersonalMentionMessageAttribute.self, f: { ConsumablePersonalMentionMessageAttribute(decoder: $0) })
declareEncodable(ConsumePersonalMessageAction.self, f: { ConsumePersonalMessageAction(decoder: $0) })
declareEncodable(ReadReactionAction.self, f: { ReadReactionAction(decoder: $0) })
declareEncodable(SynchronizeGroupedPeersOperation.self, f: { SynchronizeGroupedPeersOperation(decoder: $0) })
declareEncodable(TelegramDeviceContactImportedData.self, f: { TelegramDeviceContactImportedData(decoder: $0) })
declareEncodable(SecureFileMediaResource.self, f: { SecureFileMediaResource(decoder: $0) })
declareEncodable(SynchronizeMarkAllUnseenPersonalMessagesOperation.self, f: { SynchronizeMarkAllUnseenPersonalMessagesOperation(decoder: $0) })
declareEncodable(SynchronizeMarkAllUnseenReactionsOperation.self, f: { SynchronizeMarkAllUnseenReactionsOperation(decoder: $0) })
declareEncodable(SynchronizeAppLogEventsOperation.self, f: { SynchronizeAppLogEventsOperation(decoder: $0) })
declareEncodable(TelegramMediaPoll.self, f: { TelegramMediaPoll(decoder: $0) })
declareEncodable(TelegramMediaUnsupported.self, f: { TelegramMediaUnsupported(decoder: $0) })
declareEncodable(EmojiKeywordCollectionInfo.self, f: { EmojiKeywordCollectionInfo(decoder: $0) })
declareEncodable(EmojiKeywordItem.self, f: { EmojiKeywordItem(decoder: $0) })
declareEncodable(SynchronizeEmojiKeywordsOperation.self, f: { SynchronizeEmojiKeywordsOperation(decoder: $0) })
declareEncodable(CloudPhotoSizeMediaResource.self, f: { CloudPhotoSizeMediaResource(decoder: $0) })
declareEncodable(CloudDocumentSizeMediaResource.self, f: { CloudDocumentSizeMediaResource(decoder: $0) })
declareEncodable(CloudPeerPhotoSizeMediaResource.self, f: { CloudPeerPhotoSizeMediaResource(decoder: $0) })
declareEncodable(CloudStickerPackThumbnailMediaResource.self, f: { CloudStickerPackThumbnailMediaResource(decoder: $0) })
declareEncodable(ContentRequiresValidationMessageAttribute.self, f: { ContentRequiresValidationMessageAttribute(decoder: $0) })
declareEncodable(PendingProcessingMessageAttribute.self, f: { PendingProcessingMessageAttribute(decoder: $0) })
declareEncodable(OutgoingScheduleInfoMessageAttribute.self, f: { OutgoingScheduleInfoMessageAttribute(decoder: $0) })
declareEncodable(UpdateMessageReactionsAction.self, f: { UpdateMessageReactionsAction(decoder: $0) })
declareEncodable(SendStarsReactionsAction.self, f: { SendStarsReactionsAction(decoder: $0) })
declareEncodable(PostponeSendPaidMessageAction.self, f: { PostponeSendPaidMessageAction(decoder: $0) })
declareEncodable(RestrictedContentMessageAttribute.self, f: { RestrictedContentMessageAttribute(decoder: $0) })
declareEncodable(SendScheduledMessageImmediatelyAction.self, f: { SendScheduledMessageImmediatelyAction(decoder: $0) })
declareEncodable(EmbeddedMediaStickersMessageAttribute.self, f: { EmbeddedMediaStickersMessageAttribute(decoder: $0) })
declareEncodable(TelegramMediaWebpageAttribute.self, f: { TelegramMediaWebpageAttribute(decoder: $0) })
declareEncodable(TelegramMediaDice.self, f: { TelegramMediaDice(decoder: $0) })
declareEncodable(SynchronizeChatListFiltersOperation.self, f: { SynchronizeChatListFiltersOperation(decoder: $0) })
declareEncodable(PromoChatListItem.self, f: { PromoChatListItem(decoder: $0) })
declareEncodable(TelegramMediaFile.VideoThumbnail.self, f: { TelegramMediaFile.VideoThumbnail(decoder: $0) })
declareEncodable(PeerAccessRestrictionInfo.self, f: { PeerAccessRestrictionInfo(decoder: $0) })
declareEncodable(TelegramMediaImage.VideoRepresentation.self, f: { TelegramMediaImage.VideoRepresentation(decoder: $0) })
declareEncodable(ValidationMessageAttribute.self, f: { ValidationMessageAttribute(decoder: $0) })
declareEncodable(EmojiSearchQueryMessageAttribute.self, f: { EmojiSearchQueryMessageAttribute(decoder: $0) })
declareEncodable(WallpaperDataResource.self, f: { WallpaperDataResource(decoder: $0) })
declareEncodable(ForwardOptionsMessageAttribute.self, f: { ForwardOptionsMessageAttribute(decoder: $0) })
declareEncodable(SendAsMessageAttribute.self, f: { SendAsMessageAttribute(decoder: $0) })
declareEncodable(ForwardVideoTimestampAttribute.self, f: { ForwardVideoTimestampAttribute(decoder: $0) })
declareEncodable(AudioTranscriptionMessageAttribute.self, f: { AudioTranscriptionMessageAttribute(decoder: $0) })
declareEncodable(NonPremiumMessageAttribute.self, f: { NonPremiumMessageAttribute(decoder: $0) })
declareEncodable(TelegramExtendedMedia.self, f: { TelegramExtendedMedia(decoder: $0) })
declareEncodable(TelegramPeerUsername.self, f: { TelegramPeerUsername(decoder: $0) })
declareEncodable(MediaSpoilerMessageAttribute.self, f: { MediaSpoilerMessageAttribute(decoder: $0) })
declareEncodable(AuthSessionInfoAttribute.self, f: { AuthSessionInfoAttribute(decoder: $0) })
declareEncodable(TranslationMessageAttribute.self, f: { TranslationMessageAttribute(decoder: $0) })
declareEncodable(TranslationMessageAttribute.Additional.self, f: { TranslationMessageAttribute.Additional(decoder: $0) })
// MARK: Swiftgram
declareEncodable(QuickTranslationMessageAttribute.self, f: { QuickTranslationMessageAttribute(decoder: $0) })
declareEncodable(SynchronizeAutosaveItemOperation.self, f: { SynchronizeAutosaveItemOperation(decoder: $0) })
declareEncodable(TelegramMediaStory.self, f: { TelegramMediaStory(decoder: $0) })
declareEncodable(SynchronizeViewStoriesOperation.self, f: { SynchronizeViewStoriesOperation(decoder: $0) })
declareEncodable(SynchronizePeerStoriesOperation.self, f: { SynchronizePeerStoriesOperation(decoder: $0) })
declareEncodable(MapVenue.self, f: { MapVenue(decoder: $0) })
declareEncodable(MapGeoAddress.self, f: { MapGeoAddress(decoder: $0) })
declareEncodable(TelegramMediaGiveaway.self, f: { TelegramMediaGiveaway(decoder: $0) })
declareEncodable(TelegramMediaGiveawayResults.self, f: { TelegramMediaGiveawayResults(decoder: $0) })
declareEncodable(WebpagePreviewMessageAttribute.self, f: { WebpagePreviewMessageAttribute(decoder: $0) })
declareEncodable(InvertMediaMessageAttribute.self, f: { InvertMediaMessageAttribute(decoder: $0) })
declareEncodable(DerivedDataMessageAttribute.self, f: { DerivedDataMessageAttribute(decoder: $0) })
declareEncodable(TelegramApplicationIcons.self, f: { TelegramApplicationIcons(decoder: $0) })
declareEncodable(OutgoingQuickReplyMessageAttribute.self, f: { OutgoingQuickReplyMessageAttribute(decoder: $0) })
declareEncodable(EffectMessageAttribute.self, f: { EffectMessageAttribute(decoder: $0) })
declareEncodable(FactCheckMessageAttribute.self, f: { FactCheckMessageAttribute(decoder: $0) })
declareEncodable(TelegramMediaPaidContent.self, f: { TelegramMediaPaidContent(decoder: $0) })
declareEncodable(ReportDeliveryMessageAttribute.self, f: { ReportDeliveryMessageAttribute(decoder: $0) })
declareEncodable(PaidStarsMessageAttribute.self, f: { PaidStarsMessageAttribute(decoder: $0) })
declareEncodable(TelegramMediaTodo.self, f: { TelegramMediaTodo(decoder: $0) })
declareEncodable(TelegramMediaTodo.Item.self, f: { TelegramMediaTodo.Item(decoder: $0) })
declareEncodable(TelegramMediaTodo.Completion.self, f: { TelegramMediaTodo.Completion(decoder: $0) })
declareEncodable(SuggestedPostMessageAttribute.self, f: { SuggestedPostMessageAttribute(decoder: $0) })
declareEncodable(PublishedSuggestedPostMessageAttribute.self, f: { PublishedSuggestedPostMessageAttribute(decoder: $0) })
declareEncodable(TelegramMediaLiveStream.self, f: { TelegramMediaLiveStream(decoder: $0) })
declareEncodable(ScheduledRepeatAttribute.self, f: { ScheduledRepeatAttribute(decoder: $0) })
declareEncodable(SummarizationMessageAttribute.self, f: { SummarizationMessageAttribute(decoder: $0) })
return
}()
public func initializeAccountManagement() {
let _ = declaredEncodables
}
public func rootPathForBasePath(_ appGroupPath: String) -> String {
return appGroupPath + "/telegram-data"
}
public func performAppGroupUpgrades(appGroupPath: String, rootPath: String) {
DispatchQueue.global(qos: .default).async {
let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: rootPath), withIntermediateDirectories: true, attributes: nil)
if let items = FileManager.default.enumerator(at: URL(fileURLWithPath: appGroupPath), includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) {
let allowedDirectories: [String] = [
"telegram-data",
"Library"
]
for url in items {
guard let url = url as? URL else {
continue
}
if let isDirectory = try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory, isDirectory {
if !allowedDirectories.contains(url.lastPathComponent) {
let _ = try? FileManager.default.removeItem(at: url)
}
}
}
}
}
do {
var resourceValues = URLResourceValues()
resourceValues.isExcludedFromBackup = true
var mutableUrl = URL(fileURLWithPath: rootPath)
try mutableUrl.setResourceValues(resourceValues)
} catch let e {
print("\(e)")
}
}
public func currentAccount(allocateIfNotExists: Bool, networkArguments: NetworkInitializationArguments, supplementary: Bool, manager: AccountManager<TelegramAccountManagerTypes>, rootPath: String, auxiliaryMethods: AccountAuxiliaryMethods, encryptionParameters: ValueBoxEncryptionParameters) -> Signal<AccountResult?, NoError> {
return manager.currentAccountRecord(allocateIfNotExists: allocateIfNotExists)
|> distinctUntilChanged(isEqual: { lhs, rhs in
return lhs?.0 == rhs?.0
})
|> mapToSignal { record -> Signal<AccountResult?, NoError> in
if let record = record {
let reload = ValuePromise<Bool>(true, ignoreRepeated: false)
return reload.get()
|> mapToSignal { _ -> Signal<AccountResult?, NoError> in
let beginWithTestingEnvironment = record.1.contains(where: { attribute in
if case let .environment(environment) = attribute, case .test = environment.environment {
return true
} else {
return false
}
})
let isSupportUser = record.1.contains(where: { attribute in
if case .supportUserInfo = attribute {
return true
} else {
return false
}
})
return accountWithId(accountManager: manager, networkArguments: networkArguments, id: record.0, encryptionParameters: encryptionParameters, supplementary: supplementary, isSupportUser: isSupportUser, rootPath: rootPath, beginWithTestingEnvironment: beginWithTestingEnvironment, backupData: nil, auxiliaryMethods: auxiliaryMethods)
|> mapToSignal { accountResult -> Signal<AccountResult?, NoError> in
let postbox: Postbox
let initialKind: AccountKind
switch accountResult {
case .upgrading:
return .complete()
case let .unauthorized(account):
postbox = account.postbox
initialKind = .unauthorized
case let .authorized(account):
postbox = account.postbox
initialKind = .authorized
}
let updatedKind = postbox.stateView()
|> map { view -> Bool in
let kind: AccountKind
if view.state is AuthorizedAccountState {
kind = .authorized
} else {
kind = .unauthorized
}
if kind != initialKind {
return true
} else {
return false
}
}
|> distinctUntilChanged
return Signal { subscriber in
subscriber.putNext(accountResult)
return updatedKind.start(next: { value in
if value {
reload.set(true)
}
})
}
}
}
} else {
return .single(nil)
}
}
}
public func logoutFromAccount(id: AccountRecordId, accountManager: AccountManager<TelegramAccountManagerTypes>, alreadyLoggedOutRemotely: Bool) -> Signal<Void, NoError> {
Logger.shared.log("AccountManager", "logoutFromAccount \(id)")
return accountManager.transaction { transaction -> Void in
transaction.updateRecord(id, { current in
if alreadyLoggedOutRemotely {
return nil
} else if let current = current {
var found = false
for attribute in current.attributes {
if case .loggedOut = attribute {
found = true
break
}
}
if found {
return current
} else {
return AccountRecord(id: current.id, attributes: current.attributes + [.loggedOut(LoggedOutAccountAttribute())], temporarySessionId: nil)
}
} else {
return nil
}
})
}
}
public func managedCleanupAccounts(networkArguments: NetworkInitializationArguments, accountManager: AccountManager<TelegramAccountManagerTypes>, rootPath: String, auxiliaryMethods: AccountAuxiliaryMethods, encryptionParameters: ValueBoxEncryptionParameters) -> Signal<Void, NoError> {
let currentTemporarySessionId = accountManager.temporarySessionId
return Signal { subscriber in
let loggedOutAccounts = Atomic<[AccountRecordId: MetaDisposable]>(value: [:])
let _ = (accountManager.transaction { transaction -> Void in
for record in transaction.getRecords() {
if let temporarySessionId = record.temporarySessionId, temporarySessionId != currentTemporarySessionId {
transaction.updateRecord(record.id, { _ in
return nil
})
}
}
}).start()
let disposable = accountManager.accountRecords().start(next: { view in
var disposeList: [(AccountRecordId, MetaDisposable)] = []
var beginList: [(AccountRecordId, [TelegramAccountManagerTypes.Attribute], MetaDisposable)] = []
let _ = loggedOutAccounts.modify { disposables in
var validIds: [AccountRecordId: [TelegramAccountManagerTypes.Attribute]] = [:]
outer: for record in view.records {
for attribute in record.attributes {
if case .loggedOut = attribute {
validIds[record.id] = record.attributes
continue outer
}
}
}
var disposables = disposables
for id in disposables.keys {
if validIds[id] == nil {
disposeList.append((id, disposables[id]!))
}
}
for (id, _) in disposeList {
disposables.removeValue(forKey: id)
}
for (id, attributes) in validIds {
if disposables[id] == nil {
let disposable = MetaDisposable()
beginList.append((id, attributes, disposable))
disposables[id] = disposable
}
}
return disposables
}
for (_, disposable) in disposeList {
disposable.dispose()
}
for (id, attributes, disposable) in beginList {
Logger.shared.log("managedCleanupAccounts", "cleanup \(id), current is \(String(describing: view.currentRecord?.id))")
disposable.set(cleanupAccount(networkArguments: networkArguments, accountManager: accountManager, id: id, encryptionParameters: encryptionParameters, attributes: attributes, rootPath: rootPath, auxiliaryMethods: auxiliaryMethods).start())
}
var validPaths = Set<String>()
for record in view.records {
if let temporarySessionId = record.temporarySessionId, temporarySessionId != currentTemporarySessionId {
continue
}
validPaths.insert("\(accountRecordIdPathName(record.id))")
}
if let record = view.currentAuthAccount {
validPaths.insert("\(accountRecordIdPathName(record.id))")
}
DispatchQueue.global(qos: .utility).async {
if let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: rootPath), includingPropertiesForKeys: [], options: []) {
for url in files {
if url.lastPathComponent.hasPrefix("account-") {
if !validPaths.contains(url.lastPathComponent) {
try? FileManager.default.removeItem(at: url)
}
}
}
}
}
})
return ActionDisposable {
disposable.dispose()
}
}
}
public typealias AccountManagerPreferencesEntry = PreferencesEntry
private func cleanupAccount(networkArguments: NetworkInitializationArguments, accountManager: AccountManager<TelegramAccountManagerTypes>, id: AccountRecordId, encryptionParameters: ValueBoxEncryptionParameters, attributes: [TelegramAccountManagerTypes.Attribute], rootPath: String, auxiliaryMethods: AccountAuxiliaryMethods) -> Signal<Void, NoError> {
let beginWithTestingEnvironment = attributes.contains(where: { attribute in
if case let .environment(accountEnvironment) = attribute, case .test = accountEnvironment.environment {
return true
} else {
return false
}
})
let isSupportUser = attributes.contains(where: { attribute in
if case .supportUserInfo = attribute {
return true
} else {
return false
}
})
return accountWithId(accountManager: accountManager, networkArguments: networkArguments, id: id, encryptionParameters: encryptionParameters, supplementary: true, isSupportUser: isSupportUser, rootPath: rootPath, beginWithTestingEnvironment: beginWithTestingEnvironment, backupData: nil, auxiliaryMethods: auxiliaryMethods)
|> mapToSignal { account -> Signal<Void, NoError> in
switch account {
case .upgrading:
return .complete()
case .unauthorized:
return .complete()
case let .authorized(account):
account.shouldBeServiceTaskMaster.set(.single(.always))
return account.network.request(Api.functions.auth.logOut())
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.auth.LoggedOut?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
switch result {
case let .loggedOut(loggedOutData):
let futureAuthToken = loggedOutData.futureAuthToken
if let futureAuthToken = futureAuthToken {
storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData())
}
default:
break
}
account.shouldBeServiceTaskMaster.set(.single(.never))
return accountManager.transaction { transaction -> Void in
transaction.updateRecord(id, { _ in
return nil
})
}
}
}
}
}
@@ -0,0 +1,31 @@
import Foundation
import Postbox
final class MutableAccessChallengeDataView {
var data: PostboxAccessChallengeData
init(data: PostboxAccessChallengeData) {
self.data = data
}
func replay(updatedData: PostboxAccessChallengeData?) -> Bool {
var updated = false
if let data = updatedData {
if self.data != data {
self.data = data
updated = true
}
}
return updated
}
}
public final class AccessChallengeDataView: PostboxView {
public let data: PostboxAccessChallengeData
init(_ view: MutableAccessChallengeDataView) {
self.data = view.data
}
}
@@ -0,0 +1,57 @@
import Foundation
final class AccountManagerAtomicState<Types: AccountManagerTypes>: Codable {
enum CodingKeys: String, CodingKey {
case records
case currentRecordId
case currentAuthRecord
case accessChallengeData
}
var records: [AccountRecordId: AccountRecord<Types.Attribute>]
var currentRecordId: AccountRecordId?
var currentAuthRecord: AuthAccountRecord<Types.Attribute>?
var accessChallengeData: PostboxAccessChallengeData
init(records: [AccountRecordId: AccountRecord<Types.Attribute>] = [:], currentRecordId: AccountRecordId? = nil, currentAuthRecord: AuthAccountRecord<Types.Attribute>? = nil, accessChallengeData: PostboxAccessChallengeData = .none) {
self.records = records
self.currentRecordId = currentRecordId
self.currentAuthRecord = currentAuthRecord
self.accessChallengeData = accessChallengeData
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let records = try? container.decode(Array<AccountRecord<Types.Attribute>>.self, forKey: .records) {
var recordDict: [AccountRecordId: AccountRecord<Types.Attribute>] = [:]
for record in records {
recordDict[record.id] = record
}
self.records = recordDict
} else {
self.records = try container.decode(Dictionary<AccountRecordId, AccountRecord<Types.Attribute>>.self, forKey: .records)
}
if let idString = try? container.decodeIfPresent(String.self, forKey: .currentRecordId), let idValue = Int64(idString) {
self.currentRecordId = AccountRecordId(rawValue: idValue)
} else {
self.currentRecordId = try container.decodeIfPresent(AccountRecordId.self, forKey: .currentRecordId)
}
self.currentAuthRecord = try container.decodeIfPresent(AuthAccountRecord<Types.Attribute>.self, forKey: .currentAuthRecord)
if let accessChallengeData = try? container.decodeIfPresent(PostboxAccessChallengeData.self, forKey: .accessChallengeData) {
self.accessChallengeData = accessChallengeData
} else {
self.accessChallengeData = .none
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let recordsArray: Array<AccountRecord> = Array(self.records.values)
try container.encode(recordsArray, forKey: .records)
let currentRecordIdString: String? = self.currentRecordId.flatMap({ "\($0.rawValue)" })
try container.encodeIfPresent(currentRecordIdString, forKey: .currentRecordId)
try container.encodeIfPresent(self.currentAuthRecord, forKey: .currentAuthRecord)
try container.encode(self.accessChallengeData, forKey: .accessChallengeData)
}
}
@@ -0,0 +1,669 @@
import Foundation
import SwiftSignalKit
import Postbox
public protocol AccountManagerTypes {
associatedtype Attribute: AccountRecordAttribute
}
public typealias SharedPreferencesEntry = PreferencesEntry
public struct AccountManagerModifier<Types: AccountManagerTypes> {
public let getRecords: () -> [AccountRecord<Types.Attribute>]
public let updateRecord: (AccountRecordId, (AccountRecord<Types.Attribute>?) -> (AccountRecord<Types.Attribute>?)) -> Void
public let getCurrent: () -> (AccountRecordId, [Types.Attribute])?
public let setCurrentId: (AccountRecordId) -> Void
public let getCurrentAuth: () -> AuthAccountRecord<Types.Attribute>?
public let createAuth: ([Types.Attribute]) -> AuthAccountRecord<Types.Attribute>?
public let removeAuth: () -> Void
public let createRecord: ([Types.Attribute]) -> AccountRecordId
public let getSharedData: (ValueBoxKey) -> PreferencesEntry?
public let updateSharedData: (ValueBoxKey, (PreferencesEntry?) -> PreferencesEntry?) -> Void
public let getAccessChallengeData: () -> PostboxAccessChallengeData
public let setAccessChallengeData: (PostboxAccessChallengeData) -> Void
public let getVersion: () -> Int32?
public let setVersion: (Int32) -> Void
public let getNotice: (NoticeEntryKey) -> CodableEntry?
public let setNotice: (NoticeEntryKey, CodableEntry?) -> Void
public let clearNotices: () -> Void
public let getStoredLoginTokens: () -> [Data]
public let setStoredLoginTokens: ([Data]) -> Void
}
final class AccountManagerImpl<Types: AccountManagerTypes> {
private let queue: Queue
private let basePath: String
private let atomicStatePath: String
private let loginTokensPath: String
private let temporarySessionId: Int64
private let guardValueBox: ValueBox?
private let valueBox: ValueBox
private var tables: [Table] = []
private var currentAtomicState: AccountManagerAtomicState<Types>
private var currentAtomicStateUpdated = false
private let legacyMetadataTable: AccountManagerMetadataTable<Types.Attribute>
private let legacyRecordTable: AccountManagerRecordTable<Types.Attribute>
let sharedDataTable: AccountManagerSharedDataTable
let noticeTable: NoticeTable
private var currentRecordOperations: [AccountManagerRecordOperation<Types.Attribute>] = []
private var currentMetadataOperations: [AccountManagerMetadataOperation<Types.Attribute>] = []
private var currentUpdatedSharedDataKeys = Set<ValueBoxKey>()
private var currentUpdatedNoticeEntryKeys = Set<NoticeEntryKey>()
private var currentUpdatedAccessChallengeData: PostboxAccessChallengeData?
private var recordsViews = Bag<(MutableAccountRecordsView<Types>, ValuePipe<AccountRecordsView<Types>>)>()
private var sharedDataViews = Bag<(MutableAccountSharedDataView<Types>, ValuePipe<AccountSharedDataView<Types>>)>()
private var noticeEntryViews = Bag<(MutableNoticeEntryView<Types>, ValuePipe<NoticeEntryView<Types>>)>()
private var accessChallengeDataViews = Bag<(MutableAccessChallengeDataView, ValuePipe<AccessChallengeDataView>)>()
static func getCurrentRecords(basePath: String) -> (records: [AccountRecord<Types.Attribute>], currentId: AccountRecordId?) {
let atomicStatePath = "\(basePath)/atomic-state"
do {
let data = try Data(contentsOf: URL(fileURLWithPath: atomicStatePath))
let atomicState = try JSONDecoder().decode(AccountManagerAtomicState<Types>.self, from: data)
return (atomicState.records.sorted(by: { $0.key.int64 < $1.key.int64 }).map({ $1 }), atomicState.currentRecordId)
} catch let e {
postboxLog("decode atomic state error: \(e)")
postboxLogSync()
preconditionFailure()
}
}
fileprivate init?(queue: Queue, basePath: String, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool, removeDatabaseOnError: Bool, temporarySessionId: Int64) {
let startTime = CFAbsoluteTimeGetCurrent()
self.queue = queue
self.basePath = basePath
self.atomicStatePath = "\(basePath)/atomic-state"
self.loginTokensPath = "\(basePath)/login-tokens"
self.temporarySessionId = temporarySessionId
let _ = try? FileManager.default.createDirectory(atPath: basePath, withIntermediateDirectories: true, attributes: nil)
guard let guardValueBox = SqliteValueBox(basePath: basePath + "/guard_db", queue: queue, isTemporary: isTemporary, isReadOnly: false, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: nil, upgradeProgress: { _ in }) else {
postboxLog("Could not open guard value box at \(basePath + "/guard_db")")
postboxLogSync()
preconditionFailure()
return nil
}
self.guardValueBox = guardValueBox
var valueBox: SqliteValueBox?
for i in 0 ..< 3 {
if let valueBoxValue = SqliteValueBox(basePath: basePath + "/db", queue: queue, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, encryptionParameters: nil, upgradeProgress: { _ in }) {
valueBox = valueBoxValue
break
} else {
postboxLog("Could not open value box at \(basePath + "/db") (try \(i))")
postboxLogSync()
Thread.sleep(forTimeInterval: 0.1 + 0.5 * Double(i))
}
}
guard let valueBox = valueBox else {
postboxLog("Giving up on opening value box at \(basePath + "/db")")
postboxLogSync()
preconditionFailure()
}
self.valueBox = valueBox
self.legacyMetadataTable = AccountManagerMetadataTable<Types.Attribute>(valueBox: self.valueBox, table: AccountManagerMetadataTable<Types.Attribute>.tableSpec(0), useCaches: useCaches)
self.legacyRecordTable = AccountManagerRecordTable<Types.Attribute>(valueBox: self.valueBox, table: AccountManagerRecordTable<Types.Attribute>.tableSpec(1), useCaches: useCaches)
self.sharedDataTable = AccountManagerSharedDataTable(valueBox: self.valueBox, table: AccountManagerSharedDataTable.tableSpec(2), useCaches: useCaches)
self.noticeTable = NoticeTable(valueBox: self.valueBox, table: NoticeTable.tableSpec(3), useCaches: useCaches)
do {
let data = try Data(contentsOf: URL(fileURLWithPath: self.atomicStatePath))
do {
let atomicState = try JSONDecoder().decode(AccountManagerAtomicState<Types>.self, from: data)
self.currentAtomicState = atomicState
} catch let e {
postboxLog("decode atomic state error: \(e)")
postboxLogSync()
if removeDatabaseOnError {
let _ = try? FileManager.default.removeItem(atPath: self.atomicStatePath)
}
preconditionFailure()
}
} catch let e {
postboxLog("load atomic state error: \(e)")
postboxLogSync()
if removeDatabaseOnError {
var legacyRecordDict: [AccountRecordId: AccountRecord<Types.Attribute>] = [:]
for record in self.legacyRecordTable.getRecords() {
legacyRecordDict[record.id] = record
}
self.currentAtomicState = AccountManagerAtomicState(records: legacyRecordDict, currentRecordId: self.legacyMetadataTable.getCurrentAccountId(), currentAuthRecord: self.legacyMetadataTable.getCurrentAuthAccount(), accessChallengeData: self.legacyMetadataTable.getAccessChallengeData())
self.syncAtomicStateToFile()
} else {
preconditionFailure()
}
}
let tableAccessChallengeData = self.legacyMetadataTable.getAccessChallengeData()
if self.currentAtomicState.accessChallengeData != .none {
if tableAccessChallengeData == .none {
self.legacyMetadataTable.setAccessChallengeData(self.currentAtomicState.accessChallengeData)
}
} else if tableAccessChallengeData != .none {
self.currentAtomicState.accessChallengeData = tableAccessChallengeData
self.syncAtomicStateToFile()
}
postboxLog("AccountManager: currentAccountId = \(String(describing: currentAtomicState.currentRecordId))")
self.tables.append(self.legacyMetadataTable)
self.tables.append(self.legacyRecordTable)
self.tables.append(self.sharedDataTable)
self.tables.append(self.noticeTable)
postboxLog("AccountManager initialization took \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
}
deinit {
assert(self.queue.isCurrent())
}
fileprivate func transactionSync<T>(ignoreDisabled: Bool, _ f: (AccountManagerModifier<Types>) -> T) -> T {
self.valueBox.begin()
let transaction = AccountManagerModifier<Types>(getRecords: {
return self.currentAtomicState.records.map { $0.1 }
}, updateRecord: { id, update in
let current = self.currentAtomicState.records[id]
let updated = update(current)
if updated != current {
if let updated = updated {
self.currentAtomicState.records[id] = updated
} else {
self.currentAtomicState.records.removeValue(forKey: id)
}
self.currentAtomicStateUpdated = true
self.currentRecordOperations.append(.set(id: id, record: updated))
}
}, getCurrent: {
if let id = self.currentAtomicState.currentRecordId, let record = self.currentAtomicState.records[id] {
return (record.id, record.attributes)
} else {
return nil
}
}, setCurrentId: { id in
self.currentAtomicState.currentRecordId = id
self.currentMetadataOperations.append(.updateCurrentAccountId(id))
self.currentAtomicStateUpdated = true
}, getCurrentAuth: {
if let record = self.currentAtomicState.currentAuthRecord {
return record
} else {
return nil
}
}, createAuth: { attributes in
let record = AuthAccountRecord<Types.Attribute>(id: generateAccountRecordId(), attributes: attributes)
self.currentAtomicState.currentAuthRecord = record
self.currentAtomicStateUpdated = true
self.currentMetadataOperations.append(.updateCurrentAuthAccountRecord(record))
return record
}, removeAuth: {
self.currentAtomicState.currentAuthRecord = nil
self.currentMetadataOperations.append(.updateCurrentAuthAccountRecord(nil))
self.currentAtomicStateUpdated = true
}, createRecord: { attributes in
let id = generateAccountRecordId()
let record = AccountRecord<Types.Attribute>(id: id, attributes: attributes, temporarySessionId: nil)
self.currentAtomicState.records[id] = record
self.currentRecordOperations.append(.set(id: id, record: record))
self.currentAtomicStateUpdated = true
return id
}, getSharedData: { key in
return self.sharedDataTable.get(key: key)
}, updateSharedData: { key, f in
let updated = f(self.sharedDataTable.get(key: key))
self.sharedDataTable.set(key: key, value: updated, updatedKeys: &self.currentUpdatedSharedDataKeys)
}, getAccessChallengeData: {
return self.legacyMetadataTable.getAccessChallengeData()
}, setAccessChallengeData: { data in
self.currentUpdatedAccessChallengeData = data
self.currentAtomicStateUpdated = true
self.legacyMetadataTable.setAccessChallengeData(data)
self.currentAtomicState.accessChallengeData = data
}, getVersion: {
return self.legacyMetadataTable.getVersion()
}, setVersion: { version in
self.legacyMetadataTable.setVersion(version)
}, getNotice: { key in
self.noticeTable.get(key: key)
}, setNotice: { key, value in
self.noticeTable.set(key: key, value: value)
self.currentUpdatedNoticeEntryKeys.insert(key)
}, clearNotices: {
self.noticeTable.clear()
}, getStoredLoginTokens: {
return self.getLoginTokens()
}, setStoredLoginTokens: { list in
self.setLoginTokens(list: list)
})
let result = f(transaction)
self.beforeCommit()
self.valueBox.commit()
return result
}
fileprivate func transaction<T>(ignoreDisabled: Bool, _ f: @escaping (AccountManagerModifier<Types>) -> T) -> Signal<T, NoError> {
return Signal { subscriber in
self.queue.justDispatch {
let result = self.transactionSync(ignoreDisabled: ignoreDisabled, f)
subscriber.putNext(result)
subscriber.putCompletion()
}
return EmptyDisposable
}
}
private func syncAtomicStateToFile() {
if let data = try? JSONEncoder().encode(self.currentAtomicState) {
if let _ = try? data.write(to: URL(fileURLWithPath: self.atomicStatePath), options: [.atomic]) {
} else {
postboxLogSync()
preconditionFailure()
}
} else {
postboxLogSync()
preconditionFailure()
}
}
private func getLoginTokens() -> [Data] {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: self.loginTokensPath)) else {
return []
}
guard let list = try? JSONDecoder().decode([Data].self, from: data) else {
return []
}
return list
}
private func setLoginTokens(list: [Data]) {
if let data = try? JSONEncoder().encode(list) {
if let _ = try? data.write(to: URL(fileURLWithPath: self.loginTokensPath), options: [.atomic]) {
}
}
}
private func beforeCommit() {
if self.currentAtomicStateUpdated {
self.syncAtomicStateToFile()
}
if !self.currentRecordOperations.isEmpty || !self.currentMetadataOperations.isEmpty {
for (view, pipe) in self.recordsViews.copyItems() {
if view.replay(operations: self.currentRecordOperations, metadataOperations: self.currentMetadataOperations) {
pipe.putNext(AccountRecordsView<Types>(view))
}
}
}
if !self.currentUpdatedSharedDataKeys.isEmpty {
for (view, pipe) in self.sharedDataViews.copyItems() {
if view.replay(accountManagerImpl: self, updatedKeys: self.currentUpdatedSharedDataKeys) {
pipe.putNext(AccountSharedDataView<Types>(view))
}
}
}
if !self.currentUpdatedNoticeEntryKeys.isEmpty {
for (view, pipe) in self.noticeEntryViews.copyItems() {
if view.replay(accountManagerImpl: self, updatedKeys: self.currentUpdatedNoticeEntryKeys) {
pipe.putNext(NoticeEntryView(view))
}
}
}
if let data = self.currentUpdatedAccessChallengeData {
for (view, pipe) in self.accessChallengeDataViews.copyItems() {
if view.replay(updatedData: data) {
pipe.putNext(AccessChallengeDataView(view))
}
}
}
self.currentRecordOperations.removeAll()
self.currentMetadataOperations.removeAll()
self.currentUpdatedSharedDataKeys.removeAll()
self.currentUpdatedNoticeEntryKeys.removeAll()
self.currentUpdatedAccessChallengeData = nil
self.currentAtomicStateUpdated = false
for table in self.tables {
table.beforeCommit()
}
}
fileprivate func accountRecords() -> Signal<AccountRecordsView<Types>, NoError> {
return self.transaction(ignoreDisabled: false, { transaction -> Signal<AccountRecordsView<Types>, NoError> in
return self.accountRecordsInternal(transaction: transaction)
})
|> switchToLatest
}
fileprivate func _internalAccountRecordsSync() -> AccountRecordsView<Types> {
let mutableView = MutableAccountRecordsView<Types>(getRecords: {
return self.currentAtomicState.records.map { $0.1 }
}, currentId: self.currentAtomicState.currentRecordId, currentAuth: self.currentAtomicState.currentAuthRecord)
return AccountRecordsView<Types>(mutableView)
}
fileprivate func sharedData(keys: Set<ValueBoxKey>) -> Signal<AccountSharedDataView<Types>, NoError> {
return self.transaction(ignoreDisabled: false, { transaction -> Signal<AccountSharedDataView<Types>, NoError> in
return self.sharedDataInternal(transaction: transaction, keys: keys)
})
|> switchToLatest
}
fileprivate func noticeEntry(key: NoticeEntryKey) -> Signal<NoticeEntryView<Types>, NoError> {
return self.transaction(ignoreDisabled: false, { transaction -> Signal<NoticeEntryView<Types>, NoError> in
return self.noticeEntryInternal(transaction: transaction, key: key)
})
|> switchToLatest
}
fileprivate func accessChallengeData() -> Signal<AccessChallengeDataView, NoError> {
return self.transaction(ignoreDisabled: false, { transaction -> Signal<AccessChallengeDataView, NoError> in
return self.accessChallengeDataInternal(transaction: transaction)
})
|> switchToLatest
}
private func accountRecordsInternal(transaction: AccountManagerModifier<Types>) -> Signal<AccountRecordsView<Types>, NoError> {
assert(self.queue.isCurrent())
let mutableView = MutableAccountRecordsView<Types>(getRecords: {
return self.currentAtomicState.records.map { $0.1 }
}, currentId: self.currentAtomicState.currentRecordId, currentAuth: self.currentAtomicState.currentAuthRecord)
let pipe = ValuePipe<AccountRecordsView<Types>>()
let index = self.recordsViews.add((mutableView, pipe))
let queue = self.queue
return (.single(AccountRecordsView<Types>(mutableView))
|> then(pipe.signal()))
|> `catch` { _ -> Signal<AccountRecordsView<Types>, NoError> in
}
|> afterDisposed { [weak self] in
queue.async {
if let strongSelf = self {
strongSelf.recordsViews.remove(index)
}
}
}
}
private func sharedDataInternal(transaction: AccountManagerModifier<Types>, keys: Set<ValueBoxKey>) -> Signal<AccountSharedDataView<Types>, NoError> {
let mutableView = MutableAccountSharedDataView<Types>(accountManagerImpl: self, keys: keys)
let pipe = ValuePipe<AccountSharedDataView<Types>>()
let index = self.sharedDataViews.add((mutableView, pipe))
let queue = self.queue
return (.single(AccountSharedDataView<Types>(mutableView))
|> then(pipe.signal()))
|> `catch` { _ -> Signal<AccountSharedDataView<Types>, NoError> in
}
|> afterDisposed { [weak self] in
queue.async {
if let strongSelf = self {
strongSelf.sharedDataViews.remove(index)
}
}
}
}
private func noticeEntryInternal(transaction: AccountManagerModifier<Types>, key: NoticeEntryKey) -> Signal<NoticeEntryView<Types>, NoError> {
let mutableView = MutableNoticeEntryView<Types>(accountManagerImpl: self, key: key)
let pipe = ValuePipe<NoticeEntryView<Types>>()
let index = self.noticeEntryViews.add((mutableView, pipe))
let queue = self.queue
return (.single(NoticeEntryView(mutableView))
|> then(pipe.signal()))
|> `catch` { _ -> Signal<NoticeEntryView<Types>, NoError> in
}
|> afterDisposed { [weak self] in
queue.async {
if let strongSelf = self {
strongSelf.noticeEntryViews.remove(index)
}
}
}
}
private func accessChallengeDataInternal(transaction: AccountManagerModifier<Types>) -> Signal<AccessChallengeDataView, NoError> {
let mutableView = MutableAccessChallengeDataView(data: transaction.getAccessChallengeData())
let pipe = ValuePipe<AccessChallengeDataView>()
let index = self.accessChallengeDataViews.add((mutableView, pipe))
let queue = self.queue
return (.single(AccessChallengeDataView(mutableView))
|> then(pipe.signal()))
|> `catch` { _ -> Signal<AccessChallengeDataView, NoError> in
}
|> afterDisposed { [weak self] in
queue.async {
if let strongSelf = self {
strongSelf.accessChallengeDataViews.remove(index)
}
}
}
}
fileprivate func currentAccountRecord(allocateIfNotExists: Bool) -> Signal<(AccountRecordId, [Types.Attribute])?, NoError> {
return self.transaction(ignoreDisabled: false, { transaction -> Signal<(AccountRecordId, [Types.Attribute])?, NoError> in
let current = transaction.getCurrent()
if let _ = current {
} else if allocateIfNotExists {
let id = generateAccountRecordId()
transaction.setCurrentId(id)
transaction.updateRecord(id, { _ in
return AccountRecord(id: id, attributes: [], temporarySessionId: nil)
})
} else {
return .single(nil)
}
let signal = self.accountRecordsInternal(transaction: transaction)
|> map { view -> (AccountRecordId, [Types.Attribute])? in
if let currentRecord = view.currentRecord {
return (currentRecord.id, currentRecord.attributes)
} else {
return nil
}
}
return signal
})
|> switchToLatest
|> distinctUntilChanged(isEqual: { lhs, rhs in
if let lhs = lhs, let rhs = rhs {
if lhs.0 != rhs.0 {
return false
}
if lhs.1.count != rhs.1.count {
return false
}
for i in 0 ..< lhs.1.count {
if !lhs.1[i].isEqual(to: rhs.1[i]) {
return false
}
}
return true
} else if (lhs != nil) != (rhs != nil) {
return false
} else {
return true
}
})
}
func allocatedTemporaryAccountId() -> Signal<AccountRecordId, NoError> {
let temporarySessionId = self.temporarySessionId
return self.transaction(ignoreDisabled: false, { transaction -> Signal<AccountRecordId, NoError> in
let id = generateAccountRecordId()
transaction.updateRecord(id, { _ in
return AccountRecord(id: id, attributes: [], temporarySessionId: temporarySessionId)
})
return .single(id)
})
|> switchToLatest
|> distinctUntilChanged(isEqual: { lhs, rhs in
return lhs == rhs
})
}
}
private let sharedQueue = Queue()
public final class AccountManager<Types: AccountManagerTypes> {
public let basePath: String
public let mediaBox: MediaBox
private let queue: Queue
private let impl: QueueLocalObject<AccountManagerImpl<Types>>
public let temporarySessionId: Int64
public static func getCurrentRecords(basePath: String) -> (records: [AccountRecord<Types.Attribute>], currentId: AccountRecordId?) {
return AccountManagerImpl<Types>.getCurrentRecords(basePath: basePath)
}
public init(basePath: String, isTemporary: Bool, isReadOnly: Bool, useCaches: Bool, removeDatabaseOnError: Bool) {
self.queue = sharedQueue
self.basePath = basePath
var temporarySessionId: Int64 = 0
arc4random_buf(&temporarySessionId, 8)
self.temporarySessionId = temporarySessionId
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
if let value = AccountManagerImpl<Types>(queue: queue, basePath: basePath, isTemporary: isTemporary, isReadOnly: isReadOnly, useCaches: useCaches, removeDatabaseOnError: removeDatabaseOnError, temporarySessionId: temporarySessionId) {
return value
} else {
postboxLogSync()
preconditionFailure()
}
})
self.mediaBox = MediaBox(basePath: basePath + "/media", isMainProcess: removeDatabaseOnError)
}
public func transaction<T>(ignoreDisabled: Bool = false, _ f: @escaping (AccountManagerModifier<Types>) -> T) -> Signal<T, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.transaction(ignoreDisabled: ignoreDisabled, f).start(next: { next in
subscriber.putNext(next)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func accountRecords() -> Signal<AccountRecordsView<Types>, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.accountRecords().start(next: { next in
subscriber.putNext(next)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func _internalAccountRecordsSync() -> AccountRecordsView<Types> {
var result: AccountRecordsView<Types>?
self.impl.syncWith { impl in
result = impl._internalAccountRecordsSync()
}
return result!
}
public func sharedData(keys: Set<ValueBoxKey>) -> Signal<AccountSharedDataView<Types>, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.sharedData(keys: keys).start(next: { next in
subscriber.putNext(next)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func noticeEntry(key: NoticeEntryKey) -> Signal<NoticeEntryView<Types>, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.noticeEntry(key: key).start(next: { next in
subscriber.putNext(next)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func accessChallengeData() -> Signal<AccessChallengeDataView, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.accessChallengeData().start(next: { next in
subscriber.putNext(next)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func currentAccountRecord(allocateIfNotExists: Bool) -> Signal<(AccountRecordId, [Types.Attribute])?, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.currentAccountRecord(allocateIfNotExists: allocateIfNotExists).start(next: { next in
subscriber.putNext(next)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
public func allocatedTemporaryAccountId() -> Signal<AccountRecordId, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.allocatedTemporaryAccountId().start(next: { next in
subscriber.putNext(next)
}, completed: {
subscriber.putCompletion()
}))
}
return disposable
}
}
}
@@ -0,0 +1,224 @@
import Foundation
import Postbox
public struct AccessChallengeAttempts: Equatable {
public let count: Int32
public var bootTimestamp: Int32
public var uptime: Int32
public init(count: Int32, bootTimestamp: Int32, uptime: Int32) {
self.count = count
self.bootTimestamp = bootTimestamp
self.uptime = uptime
}
}
public enum PostboxAccessChallengeData: PostboxCoding, Equatable, Codable {
enum CodingKeys: String, CodingKey {
case numericalPassword
case plaintextPassword
}
case none
case numericalPassword(value: String)
case plaintextPassword(value: String)
public init(decoder: PostboxDecoder) {
switch decoder.decodeInt32ForKey("r", orElse: 0) {
case 0:
self = .none
case 1:
self = .numericalPassword(value: decoder.decodeStringForKey("t", orElse: ""))
case 2:
self = .plaintextPassword(value: decoder.decodeStringForKey("t", orElse: ""))
default:
assertionFailure()
self = .none
}
}
public func encode(_ encoder: PostboxEncoder) {
switch self {
case .none:
encoder.encodeInt32(0, forKey: "r")
case let .numericalPassword(text):
encoder.encodeInt32(1, forKey: "r")
encoder.encodeString(text, forKey: "t")
case let .plaintextPassword(text):
encoder.encodeInt32(2, forKey: "r")
encoder.encodeString(text, forKey: "t")
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let value = try? container.decode(String.self, forKey: .numericalPassword) {
self = .numericalPassword(value: value)
} else if let value = try? container.decode(String.self, forKey: .plaintextPassword) {
self = .plaintextPassword(value: value)
} else {
self = .none
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .none:
break
case let .numericalPassword(value):
try container.encode(value, forKey: .numericalPassword)
case let .plaintextPassword(value):
try container.encode(value, forKey: .plaintextPassword)
}
}
public var isLockable: Bool {
if case .none = self {
return false
} else {
return true
}
}
public var lockId: String? {
switch self {
case .none:
return nil
case let .numericalPassword(value):
return "numericalPassword:\(value)"
case let .plaintextPassword(value):
return "plaintextPassword:\(value)"
}
}
}
public struct AuthAccountRecord<Attribute: AccountRecordAttribute>: Codable {
enum CodingKeys: String, CodingKey {
case id
case attributes
}
public let id: AccountRecordId
public let attributes: [Attribute]
init(id: AccountRecordId, attributes: [Attribute]) {
self.id = id
self.attributes = attributes
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(AccountRecordId.self, forKey: .id)
if let attributesData = try? container.decode(Array<Data>.self, forKey: .attributes) {
var attributes: [Attribute] = []
for data in attributesData {
if let attribute = try? AdaptedPostboxDecoder().decode(Attribute.self, from: data) {
attributes.append(attribute)
}
}
self.attributes = attributes
} else {
let attributes = try container.decode([Attribute].self, forKey: .attributes)
self.attributes = attributes
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.attributes, forKey: .attributes)
}
}
enum AccountManagerMetadataOperation<Attribute: AccountRecordAttribute> {
case updateCurrentAccountId(AccountRecordId)
case updateCurrentAuthAccountRecord(AuthAccountRecord<Attribute>?)
}
private enum MetadataKey: Int64 {
case currentAccountId = 0
case currentAuthAccount = 1
case accessChallenge = 2
case version = 3
}
final class AccountManagerMetadataTable<Attribute: AccountRecordAttribute>: Table {
static func tableSpec(_ id: Int32) -> ValueBoxTable {
return ValueBoxTable(id: id, keyType: .int64, compactValuesOnCreation: false)
}
private func key(_ key: MetadataKey) -> ValueBoxKey {
let result = ValueBoxKey(length: 8)
result.setInt64(0, value: key.rawValue)
return result
}
func getVersion() -> Int32? {
if let value = self.valueBox.get(self.table, key: self.key(.version)) {
var id: Int32 = 0
value.read(&id, offset: 0, length: 4)
return id
} else {
return 0
}
}
func setVersion(_ version: Int32) {
var value: Int32 = version
self.valueBox.set(self.table, key: self.key(.version), value: MemoryBuffer(memory: &value, capacity: 4, length: 4, freeWhenDone: false))
}
func getCurrentAccountId() -> AccountRecordId? {
if let value = self.valueBox.get(self.table, key: self.key(.currentAccountId)) {
var id: Int64 = 0
value.read(&id, offset: 0, length: 8)
return AccountRecordId(rawValue: id)
} else {
return nil
}
}
func setCurrentAccountId(_ id: AccountRecordId, operations: inout [AccountManagerMetadataOperation<Attribute>]) {
var rawValue = id.rawValue
self.valueBox.set(self.table, key: self.key(.currentAccountId), value: MemoryBuffer(memory: &rawValue, capacity: 8, length: 8, freeWhenDone: false))
operations.append(.updateCurrentAccountId(id))
}
func getCurrentAuthAccount() -> AuthAccountRecord<Attribute>? {
if let value = self.valueBox.get(self.table, key: self.key(.currentAuthAccount)) {
let object = try? AdaptedPostboxDecoder().decode(AuthAccountRecord<Attribute>.self, from: value.makeData())
return object
} else {
return nil
}
}
func setCurrentAuthAccount(_ record: AuthAccountRecord<Attribute>?, operations: inout [AccountManagerMetadataOperation<Attribute>]) {
if let record = record {
let data = try! AdaptedPostboxEncoder().encode(record)
self.valueBox.set(self.table, key: self.key(.currentAuthAccount), value: ReadBuffer(data: data))
} else {
self.valueBox.remove(self.table, key: self.key(.currentAuthAccount), secure: false)
}
operations.append(.updateCurrentAuthAccountRecord(record))
}
func getAccessChallengeData() -> PostboxAccessChallengeData {
if let value = self.valueBox.get(self.table, key: self.key(.accessChallenge)) {
return PostboxAccessChallengeData(decoder: PostboxDecoder(buffer: value))
} else {
return .none
}
}
func setAccessChallengeData(_ data: PostboxAccessChallengeData) {
let encoder = PostboxEncoder()
data.encode(encoder)
withExtendedLifetime(encoder, {
self.valueBox.set(self.table, key: self.key(.accessChallenge), value: encoder.readBufferNoCopy())
})
}
}
@@ -0,0 +1,51 @@
import Foundation
import Postbox
enum AccountManagerRecordOperation<Attribute: AccountRecordAttribute> {
case set(id: AccountRecordId, record: AccountRecord<Attribute>?)
}
final class AccountManagerRecordTable<Attribute: AccountRecordAttribute>: Table {
static func tableSpec(_ id: Int32) -> ValueBoxTable {
return ValueBoxTable(id: id, keyType: .int64, compactValuesOnCreation: false)
}
private func key(_ key: AccountRecordId) -> ValueBoxKey {
let result = ValueBoxKey(length: 8)
result.setInt64(0, value: key.rawValue)
return result
}
func getRecords() -> [AccountRecord<Attribute>] {
var records: [AccountRecord<Attribute>] = []
self.valueBox.scan(self.table, values: { _, value in
if let record = try? AdaptedPostboxDecoder().decode(AccountRecord<Attribute>.self, from: value.makeData()) {
records.append(record)
}
return true
})
return records
}
func getRecord(id: AccountRecordId) -> AccountRecord<Attribute>? {
if let value = self.valueBox.get(self.table, key: self.key(id)) {
if let record = try? AdaptedPostboxDecoder().decode(AccountRecord<Attribute>.self, from: value.makeData()) {
return record
} else {
return nil
}
} else {
return nil
}
}
func setRecord(id: AccountRecordId, record: AccountRecord<Attribute>?, operations: inout [AccountManagerRecordOperation<Attribute>]) {
if let record = record {
let data = try! AdaptedPostboxEncoder().encode(record)
self.valueBox.set(self.table, key: self.key(id), value: ReadBuffer(data: data))
} else {
self.valueBox.remove(self.table, key: self.key(id), secure: false)
}
operations.append(.set(id: id, record: record))
}
}
@@ -0,0 +1,39 @@
import Foundation
import Postbox
final class AccountManagerSharedDataTable: Table {
private var values:[ValueBoxKey : PreferencesEntry] = [:]
static func tableSpec(_ id: Int32) -> ValueBoxTable {
return ValueBoxTable(id: id, keyType: .binary, compactValuesOnCreation: false)
}
func get(key: ValueBoxKey) -> PreferencesEntry? {
if let object = self.values[key] {
return object
} else if let value = self.valueBox.get(self.table, key: key) {
return PreferencesEntry(data: value.makeData())
} else {
return nil
}
}
func set(key: ValueBoxKey, value: PreferencesEntry?, updatedKeys: inout Set<ValueBoxKey>) {
if let value = value {
if let current = self.get(key: key), current == value {
return
}
self.valueBox.set(self.table, key: key, value: ReadBuffer(data: value.data))
updatedKeys.insert(key)
self.values[key] = value
} else if self.get(key: key) != nil {
self.valueBox.remove(self.table, key: key, secure: false)
updatedKeys.insert(key)
self.values.removeValue(forKey: key)
}
}
}
@@ -0,0 +1,106 @@
import Foundation
import Postbox
public protocol AccountRecordAttribute: Codable {
func isEqual(to: AccountRecordAttribute) -> Bool
}
public struct AccountRecordId: Comparable, Hashable, Codable {
let rawValue: Int64
public init(rawValue: Int64) {
self.rawValue = rawValue
}
public var int64: Int64 {
return self.rawValue
}
public static func ==(lhs: AccountRecordId, rhs: AccountRecordId) -> Bool {
return lhs.rawValue == rhs.rawValue
}
public static func <(lhs: AccountRecordId, rhs: AccountRecordId) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}
public func generateAccountRecordId() -> AccountRecordId {
var id: Int64 = 0
arc4random_buf(&id, 8)
return AccountRecordId(rawValue: id)
}
public final class AccountRecord<Attribute: AccountRecordAttribute>: Equatable, Codable {
enum CodingKeys: String, CodingKey {
case id
case attributes
case temporarySessionId
}
public let id: AccountRecordId
public let attributes: [Attribute]
public let temporarySessionId: Int64?
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let idString = try? container.decode(String.self, forKey: .id), let idValue = Int64(idString) {
self.id = AccountRecordId(rawValue: idValue)
} else {
self.id = try container.decode(AccountRecordId.self, forKey: .id)
}
if let attributesData = try? container.decode(Array<Data>.self, forKey: .attributes) {
var attributes: [Attribute] = []
for data in attributesData {
if let attribute = try? AdaptedPostboxDecoder().decode(Attribute.self, from: data) {
attributes.append(attribute)
}
}
self.attributes = attributes
} else {
let attributes = try container.decode([Attribute].self, forKey: .attributes)
self.attributes = attributes
}
if let temporarySessionIdString = try? container.decodeIfPresent(String.self, forKey: .temporarySessionId), let temporarySessionIdValue = Int64(temporarySessionIdString) {
self.temporarySessionId = temporarySessionIdValue
} else if let temporarySessionInt64 = try? container.decodeIfPresent(Int64.self, forKey: .temporarySessionId) {
self.temporarySessionId = temporarySessionInt64
} else {
self.temporarySessionId = nil
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(String("\(self.id.rawValue)"), forKey: .id)
try container.encode(self.attributes, forKey: .attributes)
let temporarySessionIdString: String? = self.temporarySessionId.flatMap({ "\($0)" })
try container.encodeIfPresent(temporarySessionIdString, forKey: .temporarySessionId)
}
public init(id: AccountRecordId, attributes: [Attribute], temporarySessionId: Int64?) {
self.id = id
self.attributes = attributes
self.temporarySessionId = temporarySessionId
}
public static func ==(lhs: AccountRecord, rhs: AccountRecord) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.attributes.count != rhs.attributes.count {
return false
}
for i in 0 ..< lhs.attributes.count {
if !lhs.attributes[i].isEqual(to: rhs.attributes[i]) {
return false
}
}
if lhs.temporarySessionId != rhs.temporarySessionId {
return false
}
return true
}
}
@@ -0,0 +1,85 @@
import Foundation
final class MutableAccountRecordsView<Types: AccountManagerTypes> {
fileprivate var records: [AccountRecord<Types.Attribute>]
fileprivate var currentId: AccountRecordId?
fileprivate var currentAuth: AuthAccountRecord<Types.Attribute>?
init(getRecords: () -> [AccountRecord<Types.Attribute>], currentId: AccountRecordId?, currentAuth: AuthAccountRecord<Types.Attribute>?) {
self.records = getRecords()
self.currentId = currentId
self.currentAuth = currentAuth
}
func replay(operations: [AccountManagerRecordOperation<Types.Attribute>], metadataOperations: [AccountManagerMetadataOperation<Types.Attribute>]) -> Bool {
var updated = false
for operation in operations {
switch operation {
case let .set(id, record):
if let record = record {
updated = true
var found = false
for i in 0 ..< self.records.count {
if self.records[i].id == id {
self.records[i] = record
found = true
break
}
}
if !found {
self.records.append(record)
self.records.sort(by: { lhs, rhs in
return lhs.id < rhs.id
})
}
} else {
for i in 0 ..< self.records.count {
if self.records[i].id == id {
self.records.remove(at: i)
updated = true
break
}
}
}
}
}
for operation in metadataOperations {
switch operation {
case let .updateCurrentAccountId(id):
updated = true
self.currentId = id
case let .updateCurrentAuthAccountRecord(record):
updated = true
self.currentAuth = record
}
}
return updated
}
}
public final class AccountRecordsView<Types: AccountManagerTypes> {
public let records: [AccountRecord<Types.Attribute>]
public let currentRecord: AccountRecord<Types.Attribute>?
public let currentAuthAccount: AuthAccountRecord<Types.Attribute>?
init(_ view: MutableAccountRecordsView<Types>) {
self.records = view.records
if let currentId = view.currentId {
var currentRecord: AccountRecord<Types.Attribute>?
for record in view.records {
if record.id == currentId {
currentRecord = record
break
}
}
self.currentRecord = currentRecord
} else {
self.currentRecord = nil
}
self.currentAuthAccount = view.currentAuth
}
}
@@ -0,0 +1,37 @@
import Foundation
import Postbox
final class MutableAccountSharedDataView<Types: AccountManagerTypes> {
private let keys: Set<ValueBoxKey>
fileprivate var entries: [ValueBoxKey: PreferencesEntry] = [:]
init(accountManagerImpl: AccountManagerImpl<Types>, keys: Set<ValueBoxKey>) {
self.keys = keys
for key in keys {
if let value = accountManagerImpl.sharedDataTable.get(key: key) {
self.entries[key] = value
}
}
}
func replay(accountManagerImpl: AccountManagerImpl<Types>, updatedKeys: Set<ValueBoxKey>) -> Bool {
var updated = false
for key in updatedKeys.intersection(self.keys) {
if let value = accountManagerImpl.sharedDataTable.get(key: key) {
self.entries[key] = value
} else {
self.entries.removeValue(forKey: key)
}
updated = true
}
return updated
}
}
public final class AccountSharedDataView<Types: AccountManagerTypes> {
public let entries: [ValueBoxKey: PreferencesEntry]
init(_ view: MutableAccountSharedDataView<Types>) {
self.entries = view.entries
}
}
@@ -0,0 +1,32 @@
import Foundation
import Postbox
final class MutableNoticeEntryView<Types: AccountManagerTypes> {
private let key: NoticeEntryKey
fileprivate var value: CodableEntry?
init(accountManagerImpl: AccountManagerImpl<Types>, key: NoticeEntryKey) {
self.key = key
self.value = accountManagerImpl.noticeTable.get(key: key)
}
func replay(accountManagerImpl: AccountManagerImpl<Types>, updatedKeys: Set<NoticeEntryKey>) -> Bool {
if updatedKeys.contains(self.key) {
self.value = accountManagerImpl.noticeTable.get(key: self.key)
return true
}
return false
}
func immutableView() -> NoticeEntryView<Types> {
return NoticeEntryView(self)
}
}
public final class NoticeEntryView<Types: AccountManagerTypes> {
public let value: CodableEntry?
init(_ view: MutableNoticeEntryView<Types>) {
self.value = view.value
}
}
@@ -0,0 +1,41 @@
import Foundation
import Postbox
public final class AdMessageAttribute: MessageAttribute {
public enum MessageType {
case sponsored
case recommended
}
public let opaqueId: Data
public let messageType: MessageType
public let url: String
public let buttonText: String
public let sponsorInfo: String?
public let additionalInfo: String?
public let canReport: Bool
public let hasContentMedia: Bool
public let minDisplayDuration: Int32?
public let maxDisplayDuration: Int32?
public init(opaqueId: Data, messageType: MessageType, url: String, buttonText: String, sponsorInfo: String?, additionalInfo: String?, canReport: Bool, hasContentMedia: Bool, minDisplayDuration: Int32?, maxDisplayDuration: Int32?) {
self.opaqueId = opaqueId
self.messageType = messageType
self.url = url
self.buttonText = buttonText
self.sponsorInfo = sponsorInfo
self.additionalInfo = additionalInfo
self.canReport = canReport
self.hasContentMedia = hasContentMedia
self.minDisplayDuration = minDisplayDuration
self.maxDisplayDuration = maxDisplayDuration
}
public init(decoder: PostboxDecoder) {
preconditionFailure()
}
public func encode(_ encoder: PostboxEncoder) {
preconditionFailure()
}
}
@@ -0,0 +1,360 @@
import Foundation
import Postbox
import TelegramApi
func imageRepresentationsForApiChatPhoto(_ photo: Api.ChatPhoto) -> [TelegramMediaImageRepresentation] {
var representations: [TelegramMediaImageRepresentation] = []
switch photo {
case let .chatPhoto(chatPhotoData):
let (flags, photoId, strippedThumb, dcId) = (chatPhotoData.flags, chatPhotoData.photoId, chatPhotoData.strippedThumb, chatPhotoData.dcId)
let hasVideo = (flags & (1 << 0)) != 0
let smallResource: TelegramMediaResource
let fullSizeResource: TelegramMediaResource
smallResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: photoId, sizeSpec: .small, volumeId: nil, localId: nil)
fullSizeResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: photoId, sizeSpec: .fullSize, volumeId: nil, localId: nil)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 80, height: 80), resource: smallResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData(), hasVideo: hasVideo, isPersonal: false))
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: fullSizeResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData(), hasVideo: hasVideo, isPersonal: false))
case .chatPhotoEmpty:
break
}
return representations
}
func parseTelegramGroupOrChannel(chat: Api.Chat) -> Peer? {
switch chat {
case let .chat(chatData):
let (flags, id, title, photo, participantsCount, date, version, migratedTo, adminRights, defaultBannedRights) = (chatData.flags, chatData.id, chatData.title, chatData.photo, chatData.participantsCount, chatData.date, chatData.version, chatData.migratedTo, chatData.adminRights, chatData.defaultBannedRights)
let left = (flags & ((1 << 1) | (1 << 2))) != 0
var migrationReference: TelegramGroupToChannelMigrationReference?
if let migratedTo = migratedTo {
switch migratedTo {
case let .inputChannel(inputChannelData):
let (channelId, accessHash) = (inputChannelData.channelId, inputChannelData.accessHash)
migrationReference = TelegramGroupToChannelMigrationReference(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), accessHash: accessHash)
case .inputChannelEmpty:
break
case .inputChannelFromMessage:
break
}
}
var groupFlags = TelegramGroupFlags()
var role: TelegramGroupRole = .member
if (flags & (1 << 0)) != 0 {
role = .creator(rank: nil)
} else if let adminRights = adminRights {
role = .admin(TelegramChatAdminRights(apiAdminRights: adminRights) ?? TelegramChatAdminRights(rights: []), rank: nil)
}
if (flags & (1 << 5)) != 0 {
groupFlags.insert(.deactivated)
}
if (flags & Int32(1 << 23)) != 0 {
groupFlags.insert(.hasVoiceChat)
}
if (flags & Int32(1 << 24)) != 0 {
groupFlags.insert(.hasActiveVoiceChat)
}
if (flags & Int32(1 << 25)) != 0 {
groupFlags.insert(.copyProtectionEnabled)
}
return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: title, photo: imageRepresentationsForApiChatPhoto(photo), participantCount: Int(participantsCount), role: role, membership: left ? .Left : .Member, flags: groupFlags, defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init(apiBannedRights:)), migrationReference: migrationReference, creationDate: date, version: Int(version))
case let .chatEmpty(chatEmptyData):
let id = chatEmptyData.id
return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: "", photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0)
case let .chatForbidden(chatForbiddenData):
let (id, title) = (chatForbiddenData.id, chatForbiddenData.title)
return TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id)), title: title, photo: [], participantCount: 0, role: .member, membership: .Removed, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0)
case let .channel(channelData):
let (flags, flags2, id, accessHash, title, username, photo, date, restrictionReason, adminRights, bannedRights, defaultBannedRights, usernames, color, profileColor, emojiStatus, boostLevel, subscriptionUntilDate, verificationIconFileId, sendPaidMessageStars, linkedMonoforumId) = (channelData.flags, channelData.flags2, channelData.id, channelData.accessHash, channelData.title, channelData.username, channelData.photo, channelData.date, channelData.restrictionReason, channelData.adminRights, channelData.bannedRights, channelData.defaultBannedRights, channelData.usernames, channelData.color, channelData.profileColor, channelData.emojiStatus, channelData.level, channelData.subscriptionUntilDate, channelData.botVerificationIcon, channelData.sendPaidMessagesStars, channelData.linkedMonoforumId)
let isMin = (flags & (1 << 12)) != 0
let participationStatus: TelegramChannelParticipationStatus
if (flags & Int32(1 << 1)) != 0 {
participationStatus = .kicked
} else if (flags & Int32(1 << 2)) != 0 {
participationStatus = .left
} else {
participationStatus = .member
}
let info: TelegramChannelInfo
if (flags & Int32(1 << 8)) != 0 {
var infoFlags = TelegramChannelGroupFlags()
if (flags & Int32(1 << 22)) != 0 {
infoFlags.insert(.slowModeEnabled)
}
info = .group(TelegramChannelGroupInfo(flags: infoFlags))
} else {
var infoFlags = TelegramChannelBroadcastFlags()
if (flags & Int32(1 << 11)) != 0 {
infoFlags.insert(.messagesShouldHaveSignatures)
}
if (flags2 & Int32(1 << 12)) != 0 {
infoFlags.insert(.messagesShouldHaveProfiles)
}
if (flags & Int32(1 << 20)) != 0 {
infoFlags.insert(.hasDiscussionGroup)
}
if (flags2 & Int32(1 << 16)) != 0 {
infoFlags.insert(.hasMonoforum)
}
info = .broadcast(TelegramChannelBroadcastInfo(flags: infoFlags))
}
var channelFlags = TelegramChannelFlags()
if (flags & Int32(1 << 0)) != 0 {
channelFlags.insert(.isCreator)
}
if (flags & Int32(1 << 7)) != 0 {
channelFlags.insert(.isVerified)
}
if (flags & Int32(1 << 19)) != 0 {
channelFlags.insert(.isScam)
}
if (flags & Int32(1 << 21)) != 0 {
channelFlags.insert(.hasGeo)
}
if (flags & Int32(1 << 23)) != 0 {
channelFlags.insert(.hasVoiceChat)
}
if (flags & Int32(1 << 24)) != 0 {
channelFlags.insert(.hasActiveVoiceChat)
}
if (flags & Int32(1 << 25)) != 0 {
channelFlags.insert(.isFake)
}
if (flags & Int32(1 << 26)) != 0 {
channelFlags.insert(.isGigagroup)
}
if (flags & Int32(1 << 27)) != 0 {
channelFlags.insert(.copyProtectionEnabled)
}
if (flags & Int32(1 << 28)) != 0 {
channelFlags.insert(.joinToSend)
}
if (flags & Int32(1 << 29)) != 0 {
channelFlags.insert(.requestToJoin)
}
if (flags & Int32(1 << 30)) != 0 {
channelFlags.insert(.isForum)
}
if (flags2 & Int32(1 << 15)) != 0 {
channelFlags.insert(.autoTranslateEnabled)
}
if (flags2 & Int32(1 << 17)) != 0 {
channelFlags.insert(.isMonoforum)
}
if (flags2 & Int32(1 << 19)) != 0 {
channelFlags.insert(.displayForumAsTabs)
}
var storiesHidden: Bool?
if flags2 & (1 << 2) == 0 { // stories_hidden_min
if flags2 & (1 << 1) != 0 {
storiesHidden = true
} else if !isMin {
storiesHidden = false
}
}
let restrictionInfo: PeerAccessRestrictionInfo?
if let restrictionReason = restrictionReason {
restrictionInfo = PeerAccessRestrictionInfo(apiReasons: restrictionReason)
} else {
restrictionInfo = nil
}
let accessHashValue = accessHash.flatMap { value -> TelegramPeerAccessHash in
if isMin {
return .genericPublic(value)
} else {
return .personal(value)
}
}
var nameColorIndex: Int32?
var backgroundEmojiId: Int64?
if let color = color {
switch color {
case let .peerColor(peerColorData):
let (_, color, backgroundEmojiIdValue) = (peerColorData.flags, peerColorData.color, peerColorData.backgroundEmojiId)
nameColorIndex = color
backgroundEmojiId = backgroundEmojiIdValue
case .peerColorCollectible:
break
case .inputPeerColorCollectible:
break
}
}
var profileColorIndex: Int32?
var profileBackgroundEmojiId: Int64?
if let profileColor = profileColor {
switch profileColor {
case let .peerColor(peerColorData):
let (_, color, backgroundEmojiIdValue) = (peerColorData.flags, peerColorData.color, peerColorData.backgroundEmojiId)
profileColorIndex = color
profileBackgroundEmojiId = backgroundEmojiIdValue
default:
break
}
}
return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: accessHashValue, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: date, version: 0, participationStatus: participationStatus, info: info, flags: channelFlags, restrictionInfo: restrictionInfo, adminRights: adminRights.flatMap(TelegramChatAdminRights.init), bannedRights: bannedRights.flatMap(TelegramChatBannedRights.init), defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), approximateBoostLevel: boostLevel, subscriptionUntilDate: subscriptionUntilDate, verificationIconFileId: verificationIconFileId, sendPaidMessageStars: sendPaidMessageStars.flatMap { StarsAmount(value: $0, nanos: 0) }, linkedMonoforumId: linkedMonoforumId.flatMap { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) })
case let .channelForbidden(channelForbiddenData):
let (flags, id, accessHash, title, untilDate) = (channelForbiddenData.flags, channelForbiddenData.id, channelForbiddenData.accessHash, channelForbiddenData.title, channelForbiddenData.untilDate)
let info: TelegramChannelInfo
if (flags & Int32(1 << 8)) != 0 {
info = .group(TelegramChannelGroupInfo(flags: []))
} else {
info = .broadcast(TelegramChannelBroadcastInfo(flags: []))
}
var channelFlags = TelegramChannelFlags()
if (flags & (1 << 10)) != 0 {
channelFlags.insert(.isMonoforum)
}
return TelegramChannel(id: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id)), accessHash: .personal(accessHash), title: title, username: nil, photo: [], creationDate: 0, version: 0, participationStatus: .kicked, info: info, flags: channelFlags, restrictionInfo: nil, adminRights: nil, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: untilDate ?? Int32.max), defaultBannedRights: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, emojiStatus: nil, approximateBoostLevel: nil, subscriptionUntilDate: nil, verificationIconFileId: nil, sendPaidMessageStars: nil, linkedMonoforumId: nil)
}
}
func mergeGroupOrChannel(lhs: Peer?, rhs: Api.Chat) -> Peer? {
switch rhs {
case .chat, .chatEmpty, .chatForbidden, .channelForbidden:
return parseTelegramGroupOrChannel(chat: rhs)
case let .channel(channelData):
let (flags, flags2, accessHash, title, username, photo, defaultBannedRights, usernames, color, profileColor, emojiStatus, boostLevel, subscriptionUntilDate, verificationIconFileId, sendPaidMessageStars, linkedMonoforumIdValue) = (channelData.flags, channelData.flags2, channelData.accessHash, channelData.title, channelData.username, channelData.photo, channelData.defaultBannedRights, channelData.usernames, channelData.color, channelData.profileColor, channelData.emojiStatus, channelData.level, channelData.subscriptionUntilDate, channelData.botVerificationIcon, channelData.sendPaidMessagesStars, channelData.linkedMonoforumId)
let isMin = (flags & (1 << 12)) != 0
if accessHash != nil && !isMin {
return parseTelegramGroupOrChannel(chat: rhs)
} else if let lhs = lhs as? TelegramChannel {
var channelFlags = lhs.flags
if (flags & Int32(1 << 7)) != 0 {
channelFlags.insert(.isVerified)
} else {
let _ = channelFlags.remove(.isVerified)
}
if (flags & Int32(1 << 23)) != 0 {
channelFlags.insert(.hasVoiceChat)
} else {
let _ = channelFlags.remove(.hasVoiceChat)
}
if (flags & Int32(1 << 24)) != 0 {
channelFlags.insert(.hasActiveVoiceChat)
} else {
let _ = channelFlags.remove(.hasActiveVoiceChat)
}
var info = lhs.info
switch info {
case .broadcast:
break
case .group:
var infoFlags = TelegramChannelGroupFlags()
if (flags & Int32(1 << 22)) != 0 {
infoFlags.insert(.slowModeEnabled)
}
info = .group(TelegramChannelGroupInfo(flags: infoFlags))
}
var storiesHidden: Bool? = lhs.storiesHidden
if flags2 & (1 << 2) == 0 { // stories_hidden_min
if flags2 & (1 << 1) != 0 {
storiesHidden = true
} else if !isMin {
storiesHidden = false
}
}
var nameColorIndex: Int32?
var backgroundEmojiId: Int64?
if let color = color {
switch color {
case let .peerColor(peerColorData):
let (_, color, backgroundEmojiIdValue) = (peerColorData.flags, peerColorData.color, peerColorData.backgroundEmojiId)
nameColorIndex = color
backgroundEmojiId = backgroundEmojiIdValue
case .peerColorCollectible:
break
case .inputPeerColorCollectible:
break
}
}
var profileColorIndex: Int32?
var profileBackgroundEmojiId: Int64?
if let profileColor = profileColor {
switch profileColor {
case let .peerColor(peerColorData):
let (_, color, backgroundEmojiIdValue) = (peerColorData.flags, peerColorData.color, peerColorData.backgroundEmojiId)
profileColorIndex = color
profileBackgroundEmojiId = backgroundEmojiIdValue
default:
break
}
}
let parsedEmojiStatus = emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:))
let linkedMonoforumId = linkedMonoforumIdValue.flatMap({ PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }) ?? lhs.linkedMonoforumId
return TelegramChannel(id: lhs.id, accessHash: lhs.accessHash, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: lhs.creationDate, version: lhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: lhs.restrictionInfo, adminRights: lhs.adminRights, bannedRights: lhs.bannedRights, defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColorIndex.flatMap { PeerNameColor(rawValue: $0) }, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, emojiStatus: parsedEmojiStatus, approximateBoostLevel: boostLevel, subscriptionUntilDate: subscriptionUntilDate, verificationIconFileId: verificationIconFileId, sendPaidMessageStars: sendPaidMessageStars.flatMap { StarsAmount(value: $0, nanos: 0) } ?? lhs.sendPaidMessageStars, linkedMonoforumId: linkedMonoforumId)
} else {
return parseTelegramGroupOrChannel(chat: rhs)
}
}
}
func mergeChannel(lhs: TelegramChannel?, rhs: TelegramChannel) -> TelegramChannel {
guard let lhs = lhs else {
return rhs
}
if case .personal? = rhs.accessHash {
let storiesHidden: Bool? = rhs.storiesHidden ?? lhs.storiesHidden
return rhs.withUpdatedStoriesHidden(storiesHidden)
}
var channelFlags = lhs.flags
if rhs.flags.contains(.isGigagroup) {
channelFlags.insert(.isGigagroup)
}
if rhs.flags.contains(.isVerified) {
channelFlags.insert(.isVerified)
} else {
let _ = channelFlags.remove(.isVerified)
}
if rhs.flags.contains(.hasVoiceChat) {
channelFlags.insert(.hasVoiceChat)
} else {
let _ = channelFlags.remove(.hasVoiceChat)
}
if rhs.flags.contains(.hasActiveVoiceChat) {
channelFlags.insert(.hasActiveVoiceChat)
} else {
let _ = channelFlags.remove(.hasActiveVoiceChat)
}
var info = lhs.info
switch info {
case .broadcast:
break
case .group:
let infoFlags = TelegramChannelGroupFlags()
info = .group(TelegramChannelGroupInfo(flags: infoFlags))
}
let accessHash: TelegramPeerAccessHash?
if let rhsAccessHashValue = lhs.accessHash, case .personal = rhsAccessHashValue {
accessHash = rhsAccessHashValue
} else {
accessHash = rhs.accessHash ?? lhs.accessHash
}
let storiesHidden: Bool? = rhs.storiesHidden ?? lhs.storiesHidden
let linkedMonoforumId = rhs.linkedMonoforumId ?? lhs.linkedMonoforumId
return TelegramChannel(id: lhs.id, accessHash: accessHash, title: rhs.title, username: rhs.username, photo: rhs.photo, creationDate: rhs.creationDate, version: rhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: rhs.restrictionInfo, adminRights: rhs.adminRights, bannedRights: rhs.bannedRights, defaultBannedRights: rhs.defaultBannedRights, usernames: rhs.usernames, storiesHidden: storiesHidden, nameColor: rhs.nameColor, backgroundEmojiId: rhs.backgroundEmojiId, profileColor: rhs.profileColor, profileBackgroundEmojiId: rhs.profileBackgroundEmojiId, emojiStatus: rhs.emojiStatus, approximateBoostLevel: rhs.approximateBoostLevel, subscriptionUntilDate: rhs.subscriptionUntilDate, verificationIconFileId: rhs.verificationIconFileId, sendPaidMessageStars: rhs.sendPaidMessageStars, linkedMonoforumId: linkedMonoforumId)
}
@@ -0,0 +1,111 @@
import Foundation
import Postbox
import TelegramApi
public extension PeerReference {
var id: PeerId {
switch self {
case let .user(id, _):
return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))
case let .group(id):
return PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(id))
case let .channel(id, _):
return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id))
}
}
}
extension PeerReference {
var inputPeer: Api.InputPeer {
switch self {
case let .user(id, accessHash):
return .inputPeerUser(.init(userId: id, accessHash: accessHash))
case let .group(id):
return .inputPeerChat(.init(chatId: id))
case let .channel(id, accessHash):
return .inputPeerChannel(.init(channelId: id, accessHash: accessHash))
}
}
var inputUser: Api.InputUser? {
if case let .user(id, accessHash) = self {
return .inputUser(.init(userId: id, accessHash: accessHash))
} else {
return nil
}
}
var inputChannel: Api.InputChannel? {
if case let .channel(id, accessHash) = self {
return .inputChannel(.init(channelId: id, accessHash: accessHash))
} else {
return nil
}
}
}
func forceApiInputPeer(_ peer: Peer) -> Api.InputPeer? {
switch peer {
case let user as TelegramUser:
return Api.InputPeer.inputPeerUser(.init(userId: user.id.id._internalGetInt64Value(), accessHash: user.accessHash?.value ?? 0))
case let group as TelegramGroup:
return Api.InputPeer.inputPeerChat(.init(chatId: group.id.id._internalGetInt64Value()))
case let channel as TelegramChannel:
if let accessHash = channel.accessHash {
return Api.InputPeer.inputPeerChannel(.init(channelId: channel.id.id._internalGetInt64Value(), accessHash: accessHash.value))
} else {
return nil
}
default:
return nil
}
}
func apiInputPeer(_ peer: Peer) -> Api.InputPeer? {
switch peer {
case let user as TelegramUser where user.accessHash != nil:
return Api.InputPeer.inputPeerUser(.init(userId: user.id.id._internalGetInt64Value(), accessHash: user.accessHash!.value))
case let group as TelegramGroup:
return Api.InputPeer.inputPeerChat(.init(chatId: group.id.id._internalGetInt64Value()))
case let channel as TelegramChannel:
if let accessHash = channel.accessHash {
return Api.InputPeer.inputPeerChannel(.init(channelId: channel.id.id._internalGetInt64Value(), accessHash: accessHash.value))
} else {
return nil
}
default:
return nil
}
}
func apiInputPeerOrSelf(_ peer: Peer, accountPeerId: PeerId) -> Api.InputPeer? {
if peer.id == accountPeerId {
return .inputPeerSelf
}
return apiInputPeer(peer)
}
func apiInputChannel(_ peer: Peer) -> Api.InputChannel? {
if let channel = peer as? TelegramChannel, let accessHash = channel.accessHash {
return Api.InputChannel.inputChannel(.init(channelId: channel.id.id._internalGetInt64Value(), accessHash: accessHash.value))
} else {
return nil
}
}
func apiInputUser(_ peer: Peer) -> Api.InputUser? {
if let user = peer as? TelegramUser, let accessHash = user.accessHash {
return Api.InputUser.inputUser(.init(userId: user.id.id._internalGetInt64Value(), accessHash: accessHash.value))
} else {
return nil
}
}
func apiInputSecretChat(_ peer: Peer) -> Api.InputEncryptedChat? {
if let chat = peer as? TelegramSecretChat {
return Api.InputEncryptedChat.inputEncryptedChat(.init(chatId: Int32(peer.id.id._internalGetInt64Value()), accessHash: chat.accessHash))
} else {
return nil
}
}
@@ -0,0 +1,72 @@
import Foundation
import Postbox
import TelegramApi
extension BotMenuButton {
init(apiBotMenuButton: Api.BotMenuButton) {
switch apiBotMenuButton {
case .botMenuButtonCommands, .botMenuButtonDefault:
self = .commands
case let .botMenuButton(botMenuButtonData):
let (text, url) = (botMenuButtonData.text, botMenuButtonData.url)
self = .webView(text: text, url: url)
}
}
}
extension BotAppSettings {
init(apiBotAppSettings: Api.BotAppSettings) {
switch apiBotAppSettings {
case let .botAppSettings(botAppSettingsData):
let (_, placeholder, backgroundColor, backgroundDarkColor, headerColor, headerDarkColor) = (botAppSettingsData.flags, botAppSettingsData.placeholderPath, botAppSettingsData.backgroundColor, botAppSettingsData.backgroundDarkColor, botAppSettingsData.headerColor, botAppSettingsData.headerDarkColor)
self.init(
placeholderData: placeholder.flatMap { $0.makeData() },
backgroundColor: backgroundColor,
backgroundDarkColor: backgroundDarkColor,
headerColor: headerColor,
headerDarkColor: headerDarkColor
)
}
}
}
extension BotVerifierSettings {
init(apiBotVerifierSettings: Api.BotVerifierSettings) {
switch apiBotVerifierSettings {
case let .botVerifierSettings(botVerifierSettingsData):
let (flags, iconFileId, companyName, customDescription) = (botVerifierSettingsData.flags, botVerifierSettingsData.icon, botVerifierSettingsData.company, botVerifierSettingsData.customDescription)
self.init(
iconFileId: iconFileId,
companyName: companyName,
customDescription: customDescription,
canModifyDescription: (flags & (1 << 1)) != 0
)
}
}
}
extension BotInfo {
convenience init(apiBotInfo: Api.BotInfo) {
switch apiBotInfo {
case let .botInfo(botInfoData):
let (_, _, description, descriptionPhoto, descriptionDocument, apiCommands, apiMenuButton, privacyPolicyUrl, appSettings, verifierSettings) = (botInfoData.flags, botInfoData.userId, botInfoData.description, botInfoData.descriptionPhoto, botInfoData.descriptionDocument, botInfoData.commands, botInfoData.menuButton, botInfoData.privacyPolicyUrl, botInfoData.appSettings, botInfoData.verifierSettings)
let photo: TelegramMediaImage? = descriptionPhoto.flatMap(telegramMediaImageFromApiPhoto)
let video: TelegramMediaFile? = descriptionDocument.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }
var commands: [BotCommand] = []
if let apiCommands = apiCommands {
commands = apiCommands.map { command in
switch command {
case let .botCommand(botCommandData):
let (command, description) = (botCommandData.command, botCommandData.description)
return BotCommand(text: command, description: description)
}
}
}
var menuButton: BotMenuButton = .commands
if let apiMenuButton = apiMenuButton {
menuButton = BotMenuButton(apiBotMenuButton: apiMenuButton)
}
self.init(description: description ?? "", photo: photo, video: video, commands: commands, menuButton: menuButton, privacyPolicyUrl: privacyPolicyUrl, appSettings: appSettings.flatMap { BotAppSettings(apiBotAppSettings: $0) }, verifierSettings: verifierSettings.flatMap { BotVerifierSettings(apiBotVerifierSettings: $0) })
}
}
}
@@ -0,0 +1,245 @@
import Foundation
import Postbox
import TelegramApi
private enum ChannelParticipantValue: Int32 {
case member = 0
case creator = 1
case editor = 2
case moderator = 3
}
public struct ChannelParticipantAdminInfo: PostboxCoding, Equatable {
public let rights: TelegramChatAdminRights
public let promotedBy: PeerId
public let canBeEditedByAccountPeer: Bool
public init(rights: TelegramChatAdminRights, promotedBy: PeerId, canBeEditedByAccountPeer: Bool) {
self.rights = rights
self.promotedBy = promotedBy
self.canBeEditedByAccountPeer = canBeEditedByAccountPeer
}
public init(decoder: PostboxDecoder) {
self.rights = decoder.decodeObjectForKey("r", decoder: { TelegramChatAdminRights(decoder: $0) }) as! TelegramChatAdminRights
self.promotedBy = PeerId(decoder.decodeInt64ForKey("p", orElse: 0))
self.canBeEditedByAccountPeer = decoder.decodeInt32ForKey("e", orElse: 0) != 0
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeObject(self.rights, forKey: "r")
encoder.encodeInt64(self.promotedBy.toInt64(), forKey: "p")
encoder.encodeInt32(self.canBeEditedByAccountPeer ? 1 : 0, forKey: "e")
}
public static func ==(lhs: ChannelParticipantAdminInfo, rhs: ChannelParticipantAdminInfo) -> Bool {
return lhs.rights == rhs.rights && lhs.promotedBy == rhs.promotedBy && lhs.canBeEditedByAccountPeer == rhs.canBeEditedByAccountPeer
}
}
public struct ChannelParticipantBannedInfo: PostboxCoding, Equatable {
public let rights: TelegramChatBannedRights
public let restrictedBy: PeerId
public let timestamp: Int32
public let isMember: Bool
public init(rights: TelegramChatBannedRights, restrictedBy: PeerId, timestamp: Int32, isMember: Bool) {
self.rights = rights
self.restrictedBy = restrictedBy
self.timestamp = timestamp
self.isMember = isMember
}
public init(decoder: PostboxDecoder) {
self.rights = decoder.decodeObjectForKey("r", decoder: { TelegramChatBannedRights(decoder: $0) }) as! TelegramChatBannedRights
self.restrictedBy = PeerId(decoder.decodeInt64ForKey("p", orElse: 0))
self.timestamp = decoder.decodeInt32ForKey("t", orElse: 0)
self.isMember = decoder.decodeInt32ForKey("m", orElse: 0) != 0
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeObject(self.rights, forKey: "r")
encoder.encodeInt64(self.restrictedBy.toInt64(), forKey: "p")
encoder.encodeInt32(self.timestamp, forKey: "t")
encoder.encodeInt32(self.isMember ? 1 : 0, forKey: "m")
}
public static func ==(lhs: ChannelParticipantBannedInfo, rhs: ChannelParticipantBannedInfo) -> Bool {
return lhs.rights == rhs.rights && lhs.restrictedBy == rhs.restrictedBy && lhs.timestamp == rhs.timestamp && lhs.isMember == rhs.isMember
}
}
public enum ChannelParticipant: PostboxCoding, Equatable {
case creator(id: PeerId, adminInfo: ChannelParticipantAdminInfo?, rank: String?)
case member(id: PeerId, invitedAt: Int32, adminInfo: ChannelParticipantAdminInfo?, banInfo: ChannelParticipantBannedInfo?, rank: String?, subscriptionUntilDate: Int32?)
public var peerId: PeerId {
switch self {
case let .creator(id, _, _):
return id
case let .member(id, _, _, _, _, _):
return id
}
}
public var rank: String? {
switch self {
case let .creator(_, _, rank):
return rank
case let .member(_, _, _, _, rank, _):
return rank
}
}
public func withUpdated(rank: String?) -> ChannelParticipant {
switch self {
case let .creator(id, adminInfo, _):
return .creator(id: id, adminInfo: adminInfo, rank: rank)
case let .member(id, invitedAt, adminInfo, banInfo, _, subscriptionUntilDate):
return .member(id: id, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: banInfo, rank: rank, subscriptionUntilDate: subscriptionUntilDate)
}
}
public static func ==(lhs: ChannelParticipant, rhs: ChannelParticipant) -> Bool {
switch lhs {
case let .member(lhsId, lhsInvitedAt, lhsAdminInfo, lhsBanInfo, lhsRank, lhsSubscriptionUntilDate):
if case let .member(rhsId, rhsInvitedAt, rhsAdminInfo, rhsBanInfo, rhsRank, rhsSubscriptionUntilDate) = rhs {
if lhsId != rhsId {
return false
}
if lhsInvitedAt != rhsInvitedAt {
return false
}
if lhsAdminInfo != rhsAdminInfo {
return false
}
if lhsBanInfo != rhsBanInfo {
return false
}
if lhsRank != rhsRank {
return false
}
if lhsSubscriptionUntilDate != rhsSubscriptionUntilDate {
return false
}
return true
} else {
return false
}
case let .creator(id, adminInfo, rank):
if case .creator(id, adminInfo, rank) = rhs {
return true
} else {
return false
}
}
}
public init(decoder: PostboxDecoder) {
switch decoder.decodeInt32ForKey("r", orElse: 0) {
case ChannelParticipantValue.member.rawValue:
self = .member(id: PeerId(decoder.decodeInt64ForKey("i", orElse: 0)), invitedAt: decoder.decodeInt32ForKey("t", orElse: 0), adminInfo: decoder.decodeObjectForKey("ai", decoder: { ChannelParticipantAdminInfo(decoder: $0) }) as? ChannelParticipantAdminInfo, banInfo: decoder.decodeObjectForKey("bi", decoder: { ChannelParticipantBannedInfo(decoder: $0) }) as? ChannelParticipantBannedInfo, rank: decoder.decodeOptionalStringForKey("rank"), subscriptionUntilDate: decoder.decodeOptionalInt32ForKey("subscriptionUntilDate"))
case ChannelParticipantValue.creator.rawValue:
self = .creator(id: PeerId(decoder.decodeInt64ForKey("i", orElse: 0)), adminInfo: decoder.decodeObjectForKey("ai", decoder: { ChannelParticipantAdminInfo(decoder: $0) }) as? ChannelParticipantAdminInfo, rank: decoder.decodeOptionalStringForKey("rank"))
default:
self = .member(id: PeerId(decoder.decodeInt64ForKey("i", orElse: 0)), invitedAt: decoder.decodeInt32ForKey("t", orElse: 0), adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil)
}
}
public func encode(_ encoder: PostboxEncoder) {
switch self {
case let .member(id, invitedAt, adminInfo, banInfo, rank, subscriptionUntilDate):
encoder.encodeInt32(ChannelParticipantValue.member.rawValue, forKey: "r")
encoder.encodeInt64(id.toInt64(), forKey: "i")
encoder.encodeInt32(invitedAt, forKey: "t")
if let adminInfo = adminInfo {
encoder.encodeObject(adminInfo, forKey: "ai")
} else {
encoder.encodeNil(forKey: "ai")
}
if let banInfo = banInfo {
encoder.encodeObject(banInfo, forKey: "bi")
} else {
encoder.encodeNil(forKey: "bi")
}
if let rank = rank {
encoder.encodeString(rank, forKey: "rank")
} else {
encoder.encodeNil(forKey: "rank")
}
if let subscriptionUntilDate = subscriptionUntilDate {
encoder.encodeInt32(subscriptionUntilDate, forKey: "subscriptionUntilDate")
} else {
encoder.encodeNil(forKey: "subscriptionUntilDate")
}
case let .creator(id, adminInfo, rank):
encoder.encodeInt32(ChannelParticipantValue.creator.rawValue, forKey: "r")
encoder.encodeInt64(id.toInt64(), forKey: "i")
if let adminInfo = adminInfo {
encoder.encodeObject(adminInfo, forKey: "ai")
} else {
encoder.encodeNil(forKey: "ai")
}
if let rank = rank {
encoder.encodeString(rank, forKey: "rank")
} else {
encoder.encodeNil(forKey: "rank")
}
}
}
}
public final class CachedChannelParticipants: PostboxCoding, Equatable {
public let participants: [ChannelParticipant]
init(participants: [ChannelParticipant]) {
self.participants = participants
}
public init(decoder: PostboxDecoder) {
self.participants = decoder.decodeObjectArrayWithDecoderForKey("p")
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeObjectArray(self.participants, forKey: "p")
}
public static func ==(lhs: CachedChannelParticipants, rhs: CachedChannelParticipants) -> Bool {
return lhs.participants == rhs.participants
}
}
extension ChannelParticipant {
init(apiParticipant: Api.ChannelParticipant) {
switch apiParticipant {
case let .channelParticipant(channelParticipantData):
let (_, userId, date, subscriptionUntilDate, rank) = (channelParticipantData.flags, channelParticipantData.userId, channelParticipantData.date, channelParticipantData.subscriptionUntilDate, channelParticipantData.rank)
self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: nil, banInfo: nil, rank: rank, subscriptionUntilDate: subscriptionUntilDate)
case let .channelParticipantCreator(channelParticipantCreatorData):
let (_, userId, adminRights, rank) = (channelParticipantCreatorData.flags, channelParticipantCreatorData.userId, channelParticipantCreatorData.adminRights, channelParticipantCreatorData.rank)
self = .creator(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(apiAdminRights: adminRights) ?? TelegramChatAdminRights(rights: []), promotedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), canBeEditedByAccountPeer: true), rank: rank)
case let .channelParticipantBanned(channelParticipantBannedData):
let (flags, userId, restrictedBy, date, bannedRights) = (channelParticipantBannedData.flags, channelParticipantBannedData.peer, channelParticipantBannedData.kickedBy, channelParticipantBannedData.date, channelParticipantBannedData.bannedRights)
let hasLeft = (flags & (1 << 0)) != 0
let banInfo = ChannelParticipantBannedInfo(rights: TelegramChatBannedRights(apiBannedRights: bannedRights), restrictedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(restrictedBy)), timestamp: date, isMember: !hasLeft)
self = .member(id: userId.peerId, invitedAt: date, adminInfo: nil, banInfo: banInfo, rank: channelParticipantBannedData.rank, subscriptionUntilDate: nil)
case let .channelParticipantAdmin(channelParticipantAdminData):
let (flags, userId, _, promotedBy, date, adminRights, rank) = (channelParticipantAdminData.flags, channelParticipantAdminData.userId, channelParticipantAdminData.inviterId, channelParticipantAdminData.promotedBy, channelParticipantAdminData.date, channelParticipantAdminData.adminRights, channelParticipantAdminData.rank)
self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(apiAdminRights: adminRights) ?? TelegramChatAdminRights(rights: []), promotedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(promotedBy)), canBeEditedByAccountPeer: (flags & (1 << 0)) != 0), banInfo: nil, rank: rank, subscriptionUntilDate: nil)
case let .channelParticipantSelf(channelParticipantSelfData):
let (_, userId, _, date, subscriptionUntilDate, rank) = (channelParticipantSelfData.flags, channelParticipantSelfData.userId, channelParticipantSelfData.inviterId, channelParticipantSelfData.date, channelParticipantSelfData.subscriptionUntilDate, channelParticipantSelfData.rank)
self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedAt: date, adminInfo: nil, banInfo: nil, rank: rank, subscriptionUntilDate: subscriptionUntilDate)
case let .channelParticipantLeft(channelParticipantLeftData):
let (userId) = (channelParticipantLeftData.peer)
self = .member(id: userId.peerId, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil)
}
}
}
extension CachedChannelParticipants {
convenience init(apiParticipants: [Api.ChannelParticipant]) {
self.init(participants: apiParticipants.map(ChannelParticipant.init))
}
}
@@ -0,0 +1,32 @@
import Foundation
import Postbox
import TelegramApi
extension GroupParticipant {
init(apiParticipant: Api.ChatParticipant) {
switch apiParticipant {
case let .chatParticipantCreator(chatParticipantCreatorData):
let (userId, rank) = (chatParticipantCreatorData.userId, chatParticipantCreatorData.rank)
self = .creator(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), rank: rank)
case let .chatParticipantAdmin(chatParticipantAdminData):
let (userId, inviterId, date, rank) = (chatParticipantAdminData.userId, chatParticipantAdminData.inviterId, chatParticipantAdminData.date, chatParticipantAdminData.rank)
self = .admin(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(inviterId)), invitedAt: date, rank: rank)
case let .chatParticipant(chatParticipantData):
let (userId, inviterId, date, rank) = (chatParticipantData.userId, chatParticipantData.inviterId, chatParticipantData.date, chatParticipantData.rank)
self = .member(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), invitedBy: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(inviterId)), invitedAt: date, rank: rank)
}
}
}
extension CachedGroupParticipants {
convenience init?(apiParticipants: Api.ChatParticipants) {
switch apiParticipants {
case let .chatParticipants(chatParticipantsData):
let (participants, version) = (chatParticipantsData.participants, chatParticipantsData.version)
self.init(participants: participants.map { GroupParticipant(apiParticipant: $0) }, version: version)
case .chatParticipantsForbidden:
return nil
}
}
}
@@ -0,0 +1,711 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum ChatContextResultMessageDecodingError: Error {
case generic
}
public enum ChatContextResultMessage: PostboxCoding, Equatable, Codable {
enum CodingKeys: String, CodingKey {
case data
}
case auto(caption: String, entities: TextEntitiesMessageAttribute?, replyMarkup: ReplyMarkupMessageAttribute?)
case text(text: String, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, previewParameters: WebpagePreviewMessageAttribute?, replyMarkup: ReplyMarkupMessageAttribute?)
case mapLocation(media: TelegramMediaMap, replyMarkup: ReplyMarkupMessageAttribute?)
case contact(media: TelegramMediaContact, replyMarkup: ReplyMarkupMessageAttribute?)
case invoice(media: TelegramMediaInvoice, replyMarkup: ReplyMarkupMessageAttribute?)
case webpage(text: String, entities: TextEntitiesMessageAttribute?, url: String, previewParameters: WebpagePreviewMessageAttribute?, replyMarkup: ReplyMarkupMessageAttribute?)
public init(decoder: PostboxDecoder) {
switch decoder.decodeInt32ForKey("_v", orElse: 0) {
case 0:
self = .auto(caption: decoder.decodeStringForKey("c", orElse: ""), entities: decoder.decodeObjectForKey("e") as? TextEntitiesMessageAttribute, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute)
case 1:
self = .text(text: decoder.decodeStringForKey("t", orElse: ""), entities: decoder.decodeObjectForKey("e") as? TextEntitiesMessageAttribute, disableUrlPreview: decoder.decodeInt32ForKey("du", orElse: 0) != 0, previewParameters: decoder.decodeObjectForKey("prp") as? WebpagePreviewMessageAttribute, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute)
case 2:
self = .mapLocation(media: decoder.decodeObjectForKey("l") as! TelegramMediaMap, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute)
case 3:
self = .contact(media: decoder.decodeObjectForKey("c") as! TelegramMediaContact, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute)
case 4:
self = .invoice(media: decoder.decodeObjectForKey("i") as! TelegramMediaInvoice, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute)
case 5:
self = .webpage(text: decoder.decodeStringForKey("t", orElse: ""), entities: decoder.decodeObjectForKey("e") as? TextEntitiesMessageAttribute, url: decoder.decodeStringForKey("url", orElse: ""), previewParameters: decoder.decodeObjectForKey("prp") as? WebpagePreviewMessageAttribute, replyMarkup: decoder.decodeObjectForKey("m") as? ReplyMarkupMessageAttribute)
default:
self = .auto(caption: "", entities: nil, replyMarkup: nil)
}
}
public func encode(_ encoder: PostboxEncoder) {
switch self {
case let .auto(caption, entities, replyMarkup):
encoder.encodeInt32(0, forKey: "_v")
encoder.encodeString(caption, forKey: "c")
if let entities = entities {
encoder.encodeObject(entities, forKey: "e")
} else {
encoder.encodeNil(forKey: "e")
}
if let replyMarkup = replyMarkup {
encoder.encodeObject(replyMarkup, forKey: "m")
} else {
encoder.encodeNil(forKey: "m")
}
case let .text(text, entities, disableUrlPreview, previewParameters, replyMarkup):
encoder.encodeInt32(1, forKey: "_v")
encoder.encodeString(text, forKey: "t")
if let entities = entities {
encoder.encodeObject(entities, forKey: "e")
} else {
encoder.encodeNil(forKey: "e")
}
encoder.encodeInt32(disableUrlPreview ? 1 : 0, forKey: "du")
if let previewParameters = previewParameters {
encoder.encodeObject(previewParameters, forKey: "prp")
} else {
encoder.encodeNil(forKey: "prp")
}
if let replyMarkup = replyMarkup {
encoder.encodeObject(replyMarkup, forKey: "m")
} else {
encoder.encodeNil(forKey: "m")
}
case let .mapLocation(media, replyMarkup):
encoder.encodeInt32(2, forKey: "_v")
encoder.encodeObject(media, forKey: "l")
if let replyMarkup = replyMarkup {
encoder.encodeObject(replyMarkup, forKey: "m")
} else {
encoder.encodeNil(forKey: "m")
}
case let .contact(media, replyMarkup):
encoder.encodeInt32(3, forKey: "_v")
encoder.encodeObject(media, forKey: "c")
if let replyMarkup = replyMarkup {
encoder.encodeObject(replyMarkup, forKey: "m")
} else {
encoder.encodeNil(forKey: "m")
}
case let .invoice(media, replyMarkup):
encoder.encodeInt32(4, forKey: "_v")
encoder.encodeObject(media, forKey: "i")
if let replyMarkup = replyMarkup {
encoder.encodeObject(replyMarkup, forKey: "m")
} else {
encoder.encodeNil(forKey: "m")
}
case let .webpage(text, entities, url, previewParameters, replyMarkup):
encoder.encodeInt32(5, forKey: "_v")
encoder.encodeString(text, forKey: "t")
if let entities = entities {
encoder.encodeObject(entities, forKey: "e")
} else {
encoder.encodeNil(forKey: "e")
}
encoder.encodeString(url, forKey: "url")
if let previewParameters = previewParameters {
encoder.encodeObject(previewParameters, forKey: "prp")
} else {
encoder.encodeNil(forKey: "prp")
}
if let replyMarkup = replyMarkup {
encoder.encodeObject(replyMarkup, forKey: "m")
} else {
encoder.encodeNil(forKey: "m")
}
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let data = try container.decode(Data.self, forKey: .data)
let postboxDecoder = PostboxDecoder(buffer: MemoryBuffer(data: data))
self = ChatContextResultMessage(decoder: postboxDecoder)
}
public func encode(to encoder: Encoder) throws {
let postboxEncoder = PostboxEncoder()
self.encode(postboxEncoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(postboxEncoder.makeData(), forKey: .data)
}
public static func ==(lhs: ChatContextResultMessage, rhs: ChatContextResultMessage) -> Bool {
switch lhs {
case let .auto(lhsCaption, lhsEntities, lhsReplyMarkup):
if case let .auto(rhsCaption, rhsEntities, rhsReplyMarkup) = rhs {
if lhsCaption != rhsCaption {
return false
}
if lhsEntities != rhsEntities {
return false
}
if lhsReplyMarkup != rhsReplyMarkup {
return false
}
return true
} else {
return false
}
case let .text(lhsText, lhsEntities, lhsDisableUrlPreview, lhsPreviewParameters, lhsReplyMarkup):
if case let .text(rhsText, rhsEntities, rhsDisableUrlPreview, rhsPreviewParameters, rhsReplyMarkup) = rhs {
if lhsText != rhsText {
return false
}
if lhsEntities != rhsEntities {
return false
}
if lhsDisableUrlPreview != rhsDisableUrlPreview {
return false
}
if lhsPreviewParameters != rhsPreviewParameters {
return false
}
if lhsReplyMarkup != rhsReplyMarkup {
return false
}
return true
} else {
return false
}
case let .mapLocation(lhsMedia, lhsReplyMarkup):
if case let .mapLocation(rhsMedia, rhsReplyMarkup) = rhs {
if !lhsMedia.isEqual(to: rhsMedia) {
return false
}
if lhsReplyMarkup != rhsReplyMarkup {
return false
}
return true
} else {
return false
}
case let .contact(lhsMedia, lhsReplyMarkup):
if case let .contact(rhsMedia, rhsReplyMarkup) = rhs {
if !lhsMedia.isEqual(to: rhsMedia) {
return false
}
if lhsReplyMarkup != rhsReplyMarkup {
return false
}
return true
} else {
return false
}
case let .invoice(lhsMedia, lhsReplyMarkup):
if case let .invoice(rhsMedia, rhsReplyMarkup) = rhs {
if !lhsMedia.isEqual(to: rhsMedia) {
return false
}
if lhsReplyMarkup != rhsReplyMarkup {
return false
}
return true
} else {
return false
}
case let .webpage(lhsText, lhsEntities, lhsUrl, lhsPreviewParameters, lhsReplyMarkup):
if case let .webpage(rhsText, rhsEntities, rhsUrl, rhsPreviewParameters, rhsReplyMarkup) = rhs {
if lhsText != rhsText {
return false
}
if lhsEntities != rhsEntities {
return false
}
if lhsUrl != rhsUrl {
return false
}
if lhsPreviewParameters != rhsPreviewParameters {
return false
}
if lhsReplyMarkup != rhsReplyMarkup {
return false
}
return true
} else {
return false
}
}
}
}
public enum ChatContextResultDecodingError: Error {
case generic
}
public enum ChatContextResult: Equatable, Codable {
enum CodingKeys: String, CodingKey {
case externalReference
case internalReference
}
public struct ExternalReference: Equatable, Codable {
public let queryId: Int64
public let id: String
public let type: String
public let title: String?
public let description: String?
public let url: String?
public let content: TelegramMediaWebFile?
public let thumbnail: TelegramMediaWebFile?
public let message: ChatContextResultMessage
public init(
queryId: Int64,
id: String,
type: String,
title: String?,
description: String?,
url: String?,
content: TelegramMediaWebFile?,
thumbnail: TelegramMediaWebFile?,
message: ChatContextResultMessage
) {
self.queryId = queryId
self.id = id
self.type = type
self.title = title
self.description = description
self.url = url
self.content = content
self.thumbnail = thumbnail
self.message = message
}
}
public struct InternalReference: Equatable, Codable {
public let queryId: Int64
public let id: String
public let type: String
public let title: String?
public let description: String?
public let image: TelegramMediaImage?
public let file: TelegramMediaFile?
public let message: ChatContextResultMessage
public init(
queryId: Int64,
id: String,
type: String,
title: String?,
description: String?,
image: TelegramMediaImage?,
file: TelegramMediaFile?,
message: ChatContextResultMessage
) {
self.queryId = queryId
self.id = id
self.type = type
self.title = title
self.description = description
self.image = image
self.file = file
self.message = message
}
}
case externalReference(ExternalReference)
case internalReference(InternalReference)
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let externalReference = try? container.decode(ExternalReference.self, forKey: .externalReference) {
self = .externalReference(externalReference)
} else if let internalReference = try? container.decode(InternalReference.self, forKey: .internalReference) {
self = .internalReference(internalReference)
} else {
throw ChatContextResultDecodingError.generic
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .internalReference(internalReference):
try container.encode(internalReference, forKey: .internalReference)
case let .externalReference(externalReference):
try container.encode(externalReference, forKey: .externalReference)
}
}
public var queryId: Int64 {
switch self {
case let .externalReference(externalReference):
return externalReference.queryId
case let .internalReference(internalReference):
return internalReference.queryId
}
}
public var id: String {
switch self {
case let .externalReference(externalReference):
return externalReference.id
case let .internalReference(internalReference):
return internalReference.id
}
}
public var type: String {
switch self {
case let .externalReference(externalReference):
return externalReference.type
case let .internalReference(internalReference):
return internalReference.type
}
}
public var title: String? {
switch self {
case let .externalReference(externalReference):
return externalReference.title
case let .internalReference(internalReference):
return internalReference.title
}
}
public var description: String? {
switch self {
case let .externalReference(externalReference):
return externalReference.description
case let .internalReference(internalReference):
return internalReference.description
}
}
public var message: ChatContextResultMessage {
switch self {
case let .externalReference(externalReference):
return externalReference.message
case let .internalReference(internalReference):
return internalReference.message
}
}
}
public enum ChatContextResultCollectionPresentation: Int32, Codable {
case media
case list
}
public struct ChatContextResultSwitchPeer: Equatable, Codable {
public let text: String
public let startParam: String
public static func ==(lhs: ChatContextResultSwitchPeer, rhs: ChatContextResultSwitchPeer) -> Bool {
return lhs.text == rhs.text && lhs.startParam == rhs.startParam
}
}
public struct ChatContextResultWebView: Equatable, Codable {
public let text: String
public let url: String
public static func ==(lhs: ChatContextResultWebView, rhs: ChatContextResultWebView) -> Bool {
return lhs.text == rhs.text && lhs.url == rhs.url
}
}
public final class ChatContextResultCollection: Equatable, Codable {
public struct GeoPoint: Equatable, Codable {
public let latitude: Double
public let longitude: Double
public init(latitude: Double, longitude: Double) {
self.latitude = latitude
self.longitude = longitude
}
}
public let botId: PeerId
public let peerId: PeerId
public let query: String
public let geoPoint: ChatContextResultCollection.GeoPoint?
public let queryId: Int64
public let nextOffset: String?
public let presentation: ChatContextResultCollectionPresentation
public let switchPeer: ChatContextResultSwitchPeer?
public let webView: ChatContextResultWebView?
public let results: [ChatContextResult]
public let cacheTimeout: Int32
public init(botId: PeerId, peerId: PeerId, query: String, geoPoint: ChatContextResultCollection.GeoPoint?, queryId: Int64, nextOffset: String?, presentation: ChatContextResultCollectionPresentation, switchPeer: ChatContextResultSwitchPeer?, webView: ChatContextResultWebView?, results: [ChatContextResult], cacheTimeout: Int32) {
self.botId = botId
self.peerId = peerId
self.query = query
self.geoPoint = geoPoint
self.queryId = queryId
self.nextOffset = nextOffset
self.presentation = presentation
self.switchPeer = switchPeer
self.webView = webView
self.results = results
self.cacheTimeout = cacheTimeout
}
public static func ==(lhs: ChatContextResultCollection, rhs: ChatContextResultCollection) -> Bool {
if lhs.botId != rhs.botId {
return false
}
if lhs.peerId != rhs.peerId {
return false
}
if lhs.queryId != rhs.queryId {
return false
}
if lhs.query != rhs.query {
return false
}
if lhs.geoPoint != rhs.geoPoint {
return false
}
if lhs.nextOffset != rhs.nextOffset {
return false
}
if lhs.presentation != rhs.presentation {
return false
}
if lhs.switchPeer != rhs.switchPeer {
return false
}
if lhs.webView != rhs.webView {
return false
}
if lhs.results != rhs.results {
return false
}
if lhs.cacheTimeout != rhs.cacheTimeout {
return false
}
return true
}
}
extension ChatContextResultMessage {
init(apiMessage: Api.BotInlineMessage) {
switch apiMessage {
case let .botInlineMessageMediaAuto(botInlineMessageMediaAutoData):
let (_, message, entities, replyMarkup) = (botInlineMessageMediaAutoData.flags, botInlineMessageMediaAutoData.message, botInlineMessageMediaAutoData.entities, botInlineMessageMediaAutoData.replyMarkup)
var parsedEntities: TextEntitiesMessageAttribute?
if let entities = entities, !entities.isEmpty {
parsedEntities = TextEntitiesMessageAttribute(entities: messageTextEntitiesFromApiEntities(entities))
}
var parsedReplyMarkup: ReplyMarkupMessageAttribute?
if let replyMarkup = replyMarkup {
parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup)
}
self = .auto(caption: message, entities: parsedEntities, replyMarkup: parsedReplyMarkup)
case let .botInlineMessageText(botInlineMessageTextData):
let (flags, message, entities, replyMarkup) = (botInlineMessageTextData.flags, botInlineMessageTextData.message, botInlineMessageTextData.entities, botInlineMessageTextData.replyMarkup)
var parsedEntities: TextEntitiesMessageAttribute?
if let entities = entities, !entities.isEmpty {
parsedEntities = TextEntitiesMessageAttribute(entities: messageTextEntitiesFromApiEntities(entities))
}
var parsedReplyMarkup: ReplyMarkupMessageAttribute?
if let replyMarkup = replyMarkup {
parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup)
}
let leadingPreview = (flags & (1 << 3)) != 0
self = .text(text: message, entities: parsedEntities, disableUrlPreview: (flags & (1 << 0)) != 0, previewParameters: WebpagePreviewMessageAttribute(
leadingPreview: leadingPreview,
forceLargeMedia: nil,
isManuallyAdded: false,
isSafe: false
), replyMarkup: parsedReplyMarkup)
case let .botInlineMessageMediaGeo(botInlineMessageMediaGeoData):
let (_, geo, heading, period, proximityNotificationRadius, replyMarkup) = (botInlineMessageMediaGeoData.flags, botInlineMessageMediaGeoData.geo, botInlineMessageMediaGeoData.heading, botInlineMessageMediaGeoData.period, botInlineMessageMediaGeoData.proximityNotificationRadius, botInlineMessageMediaGeoData.replyMarkup)
let media = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: period, liveProximityNotificationRadius: proximityNotificationRadius, heading: heading)
var parsedReplyMarkup: ReplyMarkupMessageAttribute?
if let replyMarkup = replyMarkup {
parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup)
}
self = .mapLocation(media: media, replyMarkup: parsedReplyMarkup)
case let .botInlineMessageMediaVenue(botInlineMessageMediaVenueData):
let (_, geo, title, address, provider, venueId, venueType, replyMarkup) = (botInlineMessageMediaVenueData.flags, botInlineMessageMediaVenueData.geo, botInlineMessageMediaVenueData.title, botInlineMessageMediaVenueData.address, botInlineMessageMediaVenueData.provider, botInlineMessageMediaVenueData.venueId, botInlineMessageMediaVenueData.venueType, botInlineMessageMediaVenueData.replyMarkup)
let media = telegramMediaMapFromApiGeoPoint(geo, title: title, address: address, provider: provider, venueId: venueId, venueType: venueType, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil, heading: nil)
var parsedReplyMarkup: ReplyMarkupMessageAttribute?
if let replyMarkup = replyMarkup {
parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup)
}
self = .mapLocation(media: media, replyMarkup: parsedReplyMarkup)
case let .botInlineMessageMediaContact(botInlineMessageMediaContactData):
let (_, phoneNumber, firstName, lastName, vcard, replyMarkup) = (botInlineMessageMediaContactData.flags, botInlineMessageMediaContactData.phoneNumber, botInlineMessageMediaContactData.firstName, botInlineMessageMediaContactData.lastName, botInlineMessageMediaContactData.vcard, botInlineMessageMediaContactData.replyMarkup)
let media = TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: nil, vCardData: vcard.isEmpty ? nil : vcard)
var parsedReplyMarkup: ReplyMarkupMessageAttribute?
if let replyMarkup = replyMarkup {
parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup)
}
self = .contact(media: media, replyMarkup: parsedReplyMarkup)
case let .botInlineMessageMediaInvoice(botInlineMessageMediaInvoiceData):
let (flags, title, description, photo, currency, totalAmount, replyMarkup) = (botInlineMessageMediaInvoiceData.flags, botInlineMessageMediaInvoiceData.title, botInlineMessageMediaInvoiceData.description, botInlineMessageMediaInvoiceData.photo, botInlineMessageMediaInvoiceData.currency, botInlineMessageMediaInvoiceData.totalAmount, botInlineMessageMediaInvoiceData.replyMarkup)
var parsedFlags = TelegramMediaInvoiceFlags()
if (flags & (1 << 3)) != 0 {
parsedFlags.insert(.isTest)
}
if (flags & (1 << 1)) != 0 {
parsedFlags.insert(.shippingAddressRequested)
}
var parsedReplyMarkup: ReplyMarkupMessageAttribute?
if let replyMarkup = replyMarkup {
parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup)
}
self = .invoice(media: TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: nil, currency: currency, totalAmount: totalAmount, startParam: "", extendedMedia: nil, subscriptionPeriod: nil, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), replyMarkup: parsedReplyMarkup)
case let .botInlineMessageMediaWebPage(botInlineMessageMediaWebPageData):
let (flags, message, entities, url, replyMarkup) = (botInlineMessageMediaWebPageData.flags, botInlineMessageMediaWebPageData.message, botInlineMessageMediaWebPageData.entities, botInlineMessageMediaWebPageData.url, botInlineMessageMediaWebPageData.replyMarkup)
var parsedReplyMarkup: ReplyMarkupMessageAttribute?
if let replyMarkup = replyMarkup {
parsedReplyMarkup = ReplyMarkupMessageAttribute(apiMarkup: replyMarkup)
}
let leadingPreview = (flags & (1 << 3)) != 0
var forceLargeMedia: Bool?
if (flags & (1 << 4)) != 0 {
forceLargeMedia = true
} else if (flags & (1 << 5)) != 0 {
forceLargeMedia = false
}
let isManuallyAdded = (flags & (1 << 7)) != 0
let isSafe = (flags & (1 << 8)) != 0
var parsedEntities: TextEntitiesMessageAttribute?
if let entities = entities, !entities.isEmpty {
parsedEntities = TextEntitiesMessageAttribute(entities: messageTextEntitiesFromApiEntities(entities))
}
self = .webpage(
text: message,
entities: parsedEntities,
url: url,
previewParameters: WebpagePreviewMessageAttribute(
leadingPreview: leadingPreview,
forceLargeMedia: forceLargeMedia,
isManuallyAdded: isManuallyAdded,
isSafe: isSafe
),
replyMarkup: parsedReplyMarkup
)
}
}
}
extension ChatContextResult {
init(apiResult: Api.BotInlineResult, queryId: Int64) {
switch apiResult {
case let .botInlineResult(botInlineResultData):
let (_, id, type, title, description, url, thumb, content, sendMessage) = (botInlineResultData.flags, botInlineResultData.id, botInlineResultData.type, botInlineResultData.title, botInlineResultData.description, botInlineResultData.url, botInlineResultData.thumb, botInlineResultData.content, botInlineResultData.sendMessage)
self = .externalReference(ChatContextResult.ExternalReference(queryId: queryId, id: id, type: type, title: title, description: description, url: url, content: content.flatMap(TelegramMediaWebFile.init), thumbnail: thumb.flatMap(TelegramMediaWebFile.init), message: ChatContextResultMessage(apiMessage: sendMessage)))
case let .botInlineMediaResult(botInlineMediaResultData):
let (_, id, type, photo, document, title, description, sendMessage) = (botInlineMediaResultData.flags, botInlineMediaResultData.id, botInlineMediaResultData.type, botInlineMediaResultData.photo, botInlineMediaResultData.document, botInlineMediaResultData.title, botInlineMediaResultData.description, botInlineMediaResultData.sendMessage)
var image: TelegramMediaImage?
var file: TelegramMediaFile?
if let photo = photo, let parsedImage = telegramMediaImageFromApiPhoto(photo) {
image = parsedImage
}
if let document = document, let parsedFile = telegramMediaFileFromApiDocument(document, altDocuments: []) {
file = parsedFile
}
self = .internalReference(ChatContextResult.InternalReference(queryId: queryId, id: id, type: type, title: title, description: description, image: image, file: file, message: ChatContextResultMessage(apiMessage: sendMessage)))
}
}
}
extension ChatContextResultSwitchPeer {
init(apiSwitchPeer: Api.InlineBotSwitchPM) {
switch apiSwitchPeer {
case let .inlineBotSwitchPM(inlineBotSwitchPMData):
let (text, startParam) = (inlineBotSwitchPMData.text, inlineBotSwitchPMData.startParam)
self.init(text: text, startParam: startParam)
}
}
}
extension ChatContextResultWebView {
init(apiSwitchWebView: Api.InlineBotWebView) {
switch apiSwitchWebView {
case let .inlineBotWebView(inlineBotWebViewData):
let (text, url) = (inlineBotWebViewData.text, inlineBotWebViewData.url)
self.init(text: text, url: url)
}
}
}
extension ChatContextResultCollection {
convenience init(apiResults: Api.messages.BotResults, botId: PeerId, peerId: PeerId, query: String, geoPoint: (Double, Double)?) {
switch apiResults {
case let .botResults(botResultsData):
let (flags, queryId, nextOffset, switchPm, switchWebView, results, cacheTime, _) = (botResultsData.flags, botResultsData.queryId, botResultsData.nextOffset, botResultsData.switchPm, botResultsData.switchWebview, botResultsData.results, botResultsData.cacheTime, botResultsData.users)
var switchPeer: ChatContextResultSwitchPeer?
if let switchPm = switchPm {
switchPeer = ChatContextResultSwitchPeer(apiSwitchPeer: switchPm)
}
var webView: ChatContextResultWebView?
if let switchWebView = switchWebView {
webView = ChatContextResultWebView(apiSwitchWebView: switchWebView)
}
let parsedResults = results.map({ ChatContextResult(apiResult: $0, queryId: queryId) })
/*.filter({ result in
switch result {
case .internalReference:
return false
default:
return true
}
})*/
let mappedGeoPoint = geoPoint.flatMap { geoPoint -> ChatContextResultCollection.GeoPoint in
return ChatContextResultCollection.GeoPoint(latitude: geoPoint.0, longitude: geoPoint.1)
}
self.init(botId: botId, peerId: peerId, query: query, geoPoint: mappedGeoPoint, queryId: queryId, nextOffset: nextOffset, presentation: (flags & (1 << 0) != 0) ? .media : .list, switchPeer: switchPeer, webView: webView, results: parsedResults, cacheTimeout: cacheTime)
}
}
}
public func requestContextResults(engine: TelegramEngine, botId: EnginePeer.Id, query: String, peerId: EnginePeer.Id, offset: String = "", existingResults: ChatContextResultCollection? = nil, incompleteResults: Bool = false, staleCachedResults: Bool = false, limit: Int = 60) -> Signal<RequestChatContextResultsResult?, NoError> {
return engine.messages.requestChatContextResults(botId: botId, peerId: peerId, query: query, offset: offset, incompleteResults: incompleteResults, staleCachedResults: staleCachedResults)
|> `catch` { error -> Signal<RequestChatContextResultsResult?, NoError> in
return .single(nil)
}
|> mapToSignal { resultsStruct -> Signal<RequestChatContextResultsResult?, NoError> in
let results = resultsStruct?.results
var collection = existingResults
var updated: Bool = false
if let existingResults = existingResults, let results = results {
var newResults: [ChatContextResult] = []
var existingIds = Set<String>()
for result in existingResults.results {
newResults.append(result)
existingIds.insert(result.id)
}
for result in results.results {
if !existingIds.contains(result.id) {
newResults.append(result)
existingIds.insert(result.id)
updated = true
}
}
collection = ChatContextResultCollection(botId: existingResults.botId, peerId: existingResults.peerId, query: existingResults.query, geoPoint: existingResults.geoPoint, queryId: results.queryId, nextOffset: results.nextOffset, presentation: existingResults.presentation, switchPeer: existingResults.switchPeer, webView: existingResults.webView, results: newResults, cacheTimeout: existingResults.cacheTimeout)
} else {
collection = results
updated = true
}
if let collection = collection, collection.results.count < limit, let nextOffset = collection.nextOffset, updated {
let nextResults = requestContextResults(engine: engine, botId: botId, query: query, peerId: peerId, offset: nextOffset, existingResults: collection, limit: limit)
if collection.results.count > 10 {
return .single(RequestChatContextResultsResult(results: collection, isStale: resultsStruct?.isStale ?? false))
|> then(nextResults)
} else {
return nextResults
}
} else if let collection = collection {
return .single(RequestChatContextResultsResult(results: collection, isStale: resultsStruct?.isStale ?? false))
} else {
return .single(nil)
}
}
}
@@ -0,0 +1,84 @@
import Foundation
import Postbox
import TelegramApi
protocol TelegramCloudMediaResource: TelegramMediaResource {
func apiInputLocation(fileReference: Data?) -> Api.InputFileLocation?
}
public func extractMediaResourceDebugInfo(resource: MediaResource) -> String? {
if let resource = resource as? TelegramCloudMediaResource {
guard let inputLocation = resource.apiInputLocation(fileReference: nil) else {
return nil
}
return String(describing: inputLocation)
} else {
return nil
}
}
public protocol TelegramMultipartFetchableResource: TelegramMediaResource {
var datacenterId: Int { get }
}
public protocol TelegramCloudMediaResourceWithFileReference {
var fileReference: Data? { get }
}
extension CloudFileMediaResource: TelegramCloudMediaResource, TelegramMultipartFetchableResource, TelegramCloudMediaResourceWithFileReference {
func apiInputLocation(fileReference: Data?) -> Api.InputFileLocation? {
return Api.InputFileLocation.inputFileLocation(.init(volumeId: self.volumeId, localId: self.localId, secret: self.secret, fileReference: Buffer(data: fileReference ?? Data())))
}
}
extension CloudPhotoSizeMediaResource: TelegramCloudMediaResource, TelegramMultipartFetchableResource, TelegramCloudMediaResourceWithFileReference {
func apiInputLocation(fileReference: Data?) -> Api.InputFileLocation? {
return Api.InputFileLocation.inputPhotoFileLocation(.init(id: self.photoId, accessHash: self.accessHash, fileReference: Buffer(data: fileReference ?? Data()), thumbSize: self.sizeSpec))
}
}
extension CloudDocumentSizeMediaResource: TelegramCloudMediaResource, TelegramMultipartFetchableResource, TelegramCloudMediaResourceWithFileReference {
func apiInputLocation(fileReference: Data?) -> Api.InputFileLocation? {
return Api.InputFileLocation.inputDocumentFileLocation(.init(id: self.documentId, accessHash: self.accessHash, fileReference: Buffer(data: fileReference ?? Data()), thumbSize: self.sizeSpec))
}
}
extension CloudPeerPhotoSizeMediaResource: TelegramMultipartFetchableResource {
func apiInputLocation(peerReference: PeerReference) -> Api.InputFileLocation? {
let flags: Int32
switch self.sizeSpec {
case .small:
flags = 0
case .fullSize:
flags = 1 << 0
}
if let photoId = self.photoId {
return Api.InputFileLocation.inputPeerPhotoFileLocation(.init(flags: flags, peer: peerReference.inputPeer, photoId: photoId))
} else {
return nil
}
}
}
extension CloudStickerPackThumbnailMediaResource: TelegramMultipartFetchableResource {
func apiInputLocation(packReference: StickerPackReference) -> Api.InputFileLocation? {
if let thumbVersion = self.thumbVersion {
return Api.InputFileLocation.inputStickerSetThumb(.init(stickerset: packReference.apiInputStickerSet, thumbVersion: thumbVersion))
} else {
return nil
}
}
}
extension CloudDocumentMediaResource: TelegramCloudMediaResource, TelegramMultipartFetchableResource, TelegramCloudMediaResourceWithFileReference {
func apiInputLocation(fileReference: Data?) -> Api.InputFileLocation? {
return Api.InputFileLocation.inputDocumentFileLocation(.init(id: self.fileId, accessHash: self.accessHash, fileReference: Buffer(data: fileReference ?? Data()), thumbSize: ""))
}
}
extension SecretFileMediaResource: TelegramCloudMediaResource, TelegramMultipartFetchableResource {
func apiInputLocation(fileReference: Data?) -> Api.InputFileLocation? {
return .inputEncryptedFileLocation(.init(id: self.fileId, accessHash: self.accessHash))
}
}
@@ -0,0 +1,8 @@
import Foundation
public enum CloudMediaResourceLocation: Equatable {
case photo(id: Int64, accessHash: Int64, fileReference: Data, thumbSize: String)
case file(id: Int64, accessHash: Int64, fileReference: Data, thumbSize: String)
case peerPhoto(peer: PeerReference, fullSize: Bool, volumeId: Int64, localId: Int64)
case stickerPackThumbnail(packReference: StickerPackReference, volumeId: Int64, localId: Int64)
}
@@ -0,0 +1,5 @@
import Foundation
public protocol EncryptedMediaResource {
func decrypt(data: Data, params: Any) -> Data?
}
@@ -0,0 +1,64 @@
import Foundation
import Postbox
import TelegramApi
extension ExportedInvitation {
init(apiExportedInvite: Api.ExportedChatInvite) {
switch apiExportedInvite {
case let .chatInviteExported(chatInviteExportedData):
let (flags, link, adminId, date, startDate, expireDate, usageLimit, usage, requested, subscriptionExpired, title, pricing) = (chatInviteExportedData.flags, chatInviteExportedData.link, chatInviteExportedData.adminId, chatInviteExportedData.date, chatInviteExportedData.startDate, chatInviteExportedData.expireDate, chatInviteExportedData.usageLimit, chatInviteExportedData.usage, chatInviteExportedData.requested, chatInviteExportedData.subscriptionExpired, chatInviteExportedData.title, chatInviteExportedData.subscriptionPricing)
let _ = subscriptionExpired
self = .link(link: link, title: title, isPermanent: (flags & (1 << 5)) != 0, requestApproval: (flags & (1 << 6)) != 0, isRevoked: (flags & (1 << 0)) != 0, adminId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(adminId)), date: date, startDate: startDate, expireDate: expireDate, usageLimit: usageLimit, count: usage, requestedCount: requested, pricing: pricing.flatMap { StarsSubscriptionPricing(apiStarsSubscriptionPricing: $0) })
case .chatInvitePublicJoinRequests:
self = .publicJoinRequest
}
}
}
public extension ExportedInvitation {
var link: String? {
switch self {
case let .link(link, _, _, _, _, _, _, _, _, _, _, _, _):
return link
case .publicJoinRequest:
return nil
}
}
var date: Int32? {
switch self {
case let .link(_, _, _, _, _, _, date, _, _, _, _, _, _):
return date
case .publicJoinRequest:
return nil
}
}
var isPermanent: Bool {
switch self {
case let .link(_, _, isPermanent, _, _, _, _, _, _, _, _, _, _):
return isPermanent
case .publicJoinRequest:
return false
}
}
var isRevoked: Bool {
switch self {
case let .link(_, _, _, _, isRevoked, _, _, _, _, _, _, _, _):
return isRevoked
case .publicJoinRequest:
return false
}
}
var pricing: StarsSubscriptionPricing? {
switch self {
case let .link(_, _, _, _, _, _, _, _, _, _, _, _, pricing):
return pricing
case .publicJoinRequest:
return nil
}
}
}
@@ -0,0 +1,22 @@
import Foundation
public struct ImageRepresentationWithReference: Equatable {
public let representation: TelegramMediaImageRepresentation
public let reference: MediaResourceReference
public init(representation: TelegramMediaImageRepresentation, reference: MediaResourceReference) {
self.representation = representation
self.reference = reference
}
}
public struct VideoRepresentationWithReference: Equatable {
public let representation: TelegramMediaImage.VideoRepresentation
public let reference: MediaResourceReference
public init(representation: TelegramMediaImage.VideoRepresentation, reference: MediaResourceReference) {
self.representation = representation
self.reference = reference
}
}
@@ -0,0 +1,238 @@
import Foundation
import Postbox
import TelegramApi
extension InstantPageCaption {
convenience init(apiCaption: Api.PageCaption) {
switch apiCaption {
case let .pageCaption(pageCaptionData):
let (text, credit) = (pageCaptionData.text, pageCaptionData.credit)
self.init(text: RichText(apiText: text), credit: RichText(apiText: credit))
}
}
}
public extension InstantPageListItem {
var num: String? {
switch self {
case let .text(_, num):
return num
case let .blocks(_, num):
return num
default:
return nil
}
}
}
extension InstantPageListItem {
init(apiListItem: Api.PageListItem) {
switch apiListItem {
case let .pageListItemText(pageListItemTextData):
let text = pageListItemTextData.text
self = .text(RichText(apiText: text), nil)
case let .pageListItemBlocks(pageListItemBlocksData):
let blocks = pageListItemBlocksData.blocks
self = .blocks(blocks.map({ InstantPageBlock(apiBlock: $0) }), nil)
}
}
init(apiListOrderedItem: Api.PageListOrderedItem) {
switch apiListOrderedItem {
case let .pageListOrderedItemText(pageListOrderedItemTextData):
let (num, text) = (pageListOrderedItemTextData.num, pageListOrderedItemTextData.text)
self = .text(RichText(apiText: text), num)
case let .pageListOrderedItemBlocks(pageListOrderedItemBlocksData):
let (num, blocks) = (pageListOrderedItemBlocksData.num, pageListOrderedItemBlocksData.blocks)
self = .blocks(blocks.map({ InstantPageBlock(apiBlock: $0) }), num)
}
}
}
extension InstantPageTableCell {
convenience init(apiTableCell: Api.PageTableCell) {
switch apiTableCell {
case let .pageTableCell(pageTableCellData):
let (flags, text, colspan, rowspan) = (pageTableCellData.flags, pageTableCellData.text, pageTableCellData.colspan, pageTableCellData.rowspan)
var alignment = TableHorizontalAlignment.left
if (flags & (1 << 3)) != 0 {
alignment = .center
} else if (flags & (1 << 4)) != 0 {
alignment = .right
}
var verticalAlignment = TableVerticalAlignment.top
if (flags & (1 << 5)) != 0 {
verticalAlignment = .middle
} else if (flags & (1 << 6)) != 0 {
verticalAlignment = .bottom
}
self.init(text: text != nil ? RichText(apiText: text!) : nil, header: (flags & (1 << 0)) != 0, alignment: alignment, verticalAlignment: verticalAlignment, colspan: colspan ?? 0, rowspan: rowspan ?? 0)
}
}
}
extension InstantPageTableRow {
convenience init(apiTableRow: Api.PageTableRow) {
switch apiTableRow {
case let .pageTableRow(pageTableRowData):
let cells = pageTableRowData.cells
self.init(cells: cells.map({ InstantPageTableCell(apiTableCell: $0) }))
}
}
}
extension InstantPageRelatedArticle {
convenience init(apiRelatedArticle: Api.PageRelatedArticle) {
switch apiRelatedArticle {
case let .pageRelatedArticle(pageRelatedArticleData):
let (url, webpageId, title, description, photoId, author, publishedDate) = (pageRelatedArticleData.url, pageRelatedArticleData.webpageId, pageRelatedArticleData.title, pageRelatedArticleData.description, pageRelatedArticleData.photoId, pageRelatedArticleData.author, pageRelatedArticleData.publishedDate)
var posterPhotoId: MediaId?
if let photoId = photoId {
posterPhotoId = MediaId(namespace: Namespaces.Media.CloudImage, id: photoId)
}
self.init(url: url, webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: webpageId), title: title, description: description, photoId: posterPhotoId, author: author, date: publishedDate)
}
}
}
extension InstantPageBlock {
init(apiBlock: Api.PageBlock) {
switch apiBlock {
case .pageBlockUnsupported:
self = .unsupported
case let .pageBlockTitle(pageBlockTitleData):
let text = pageBlockTitleData.text
self = .title(RichText(apiText: text))
case let .pageBlockSubtitle(pageBlockSubtitleData):
let text = pageBlockSubtitleData.text
self = .subtitle(RichText(apiText: text))
case let .pageBlockAuthorDate(pageBlockAuthorDateData):
let (author, publishedDate) = (pageBlockAuthorDateData.author, pageBlockAuthorDateData.publishedDate)
self = .authorDate(author: RichText(apiText: author), date: publishedDate)
case let .pageBlockHeader(pageBlockHeaderData):
let text = pageBlockHeaderData.text
self = .header(RichText(apiText: text))
case let .pageBlockSubheader(pageBlockSubheaderData):
let text = pageBlockSubheaderData.text
self = .subheader(RichText(apiText: text))
case let .pageBlockParagraph(pageBlockParagraphData):
let text = pageBlockParagraphData.text
self = .paragraph(RichText(apiText: text))
case let .pageBlockPreformatted(pageBlockPreformattedData):
let text = pageBlockPreformattedData.text
self = .preformatted(RichText(apiText: text))
case let .pageBlockFooter(pageBlockFooterData):
let text = pageBlockFooterData.text
self = .footer(RichText(apiText: text))
case .pageBlockDivider:
self = .divider
case let .pageBlockAnchor(pageBlockAnchorData):
let name = pageBlockAnchorData.name
self = .anchor(name)
case let .pageBlockBlockquote(pageBlockBlockquoteData):
let (text, caption) = (pageBlockBlockquoteData.text, pageBlockBlockquoteData.caption)
self = .blockQuote(text: RichText(apiText: text), caption: RichText(apiText: caption))
case let .pageBlockPullquote(pageBlockPullquoteData):
let (text, caption) = (pageBlockPullquoteData.text, pageBlockPullquoteData.caption)
self = .pullQuote(text: RichText(apiText: text), caption: RichText(apiText: caption))
case let .pageBlockPhoto(pageBlockPhotoData):
let (flags, photoId, caption, url, webpageId) = (pageBlockPhotoData.flags, pageBlockPhotoData.photoId, pageBlockPhotoData.caption, pageBlockPhotoData.url, pageBlockPhotoData.webpageId)
var webpageMediaId: MediaId?
if (flags & (1 << 0)) != 0, let webpageId = webpageId, webpageId != 0 {
webpageMediaId = MediaId(namespace: Namespaces.Media.CloudWebpage, id: webpageId)
}
self = .image(id: MediaId(namespace: Namespaces.Media.CloudImage, id: photoId), caption: InstantPageCaption(apiCaption: caption), url: url, webpageId: webpageMediaId)
case let .pageBlockVideo(pageBlockVideoData):
let (flags, videoId, caption) = (pageBlockVideoData.flags, pageBlockVideoData.videoId, pageBlockVideoData.caption)
self = .video(id: MediaId(namespace: Namespaces.Media.CloudFile, id: videoId), caption: InstantPageCaption(apiCaption: caption), autoplay: (flags & (1 << 0)) != 0, loop: (flags & (1 << 1)) != 0)
case let .pageBlockCover(pageBlockCoverData):
let cover = pageBlockCoverData.cover
self = .cover(InstantPageBlock(apiBlock: cover))
case let .pageBlockEmbed(pageBlockEmbedData):
let (flags, url, html, posterPhotoId, w, h, caption) = (pageBlockEmbedData.flags, pageBlockEmbedData.url, pageBlockEmbedData.html, pageBlockEmbedData.posterPhotoId, pageBlockEmbedData.w, pageBlockEmbedData.h, pageBlockEmbedData.caption)
var dimensions: PixelDimensions?
if let w = w, let h = h {
dimensions = PixelDimensions(width: w, height: h)
}
self = .webEmbed(url: url, html: html, dimensions: dimensions, caption: InstantPageCaption(apiCaption: caption), stretchToWidth: (flags & (1 << 0)) != 0, allowScrolling: (flags & (1 << 3)) != 0, coverId: posterPhotoId.flatMap { MediaId(namespace: Namespaces.Media.CloudImage, id: $0) })
case let .pageBlockEmbedPost(pageBlockEmbedPostData):
let (url, webpageId, authorPhotoId, author, date, blocks, caption) = (pageBlockEmbedPostData.url, pageBlockEmbedPostData.webpageId, pageBlockEmbedPostData.authorPhotoId, pageBlockEmbedPostData.author, pageBlockEmbedPostData.date, pageBlockEmbedPostData.blocks, pageBlockEmbedPostData.caption)
self = .postEmbed(url: url, webpageId: webpageId == 0 ? nil : MediaId(namespace: Namespaces.Media.CloudWebpage, id: webpageId), avatarId: authorPhotoId == 0 ? nil : MediaId(namespace: Namespaces.Media.CloudImage, id: authorPhotoId), author: author, date: date, blocks: blocks.map({ InstantPageBlock(apiBlock: $0) }), caption: InstantPageCaption(apiCaption: caption))
case let .pageBlockCollage(pageBlockCollageData):
let (items, caption) = (pageBlockCollageData.items, pageBlockCollageData.caption)
self = .collage(items: items.map({ InstantPageBlock(apiBlock: $0) }), caption: InstantPageCaption(apiCaption: caption))
case let .pageBlockSlideshow(pageBlockSlideshowData):
let (items, caption) = (pageBlockSlideshowData.items, pageBlockSlideshowData.caption)
self = .slideshow(items: items.map({ InstantPageBlock(apiBlock: $0) }), caption: InstantPageCaption(apiCaption: caption))
case let .pageBlockChannel(pageBlockChannelData):
let apiChat = pageBlockChannelData.channel
self = .channelBanner(parseTelegramGroupOrChannel(chat: apiChat) as? TelegramChannel)
case let .pageBlockAudio(pageBlockAudioData):
let (audioId, caption) = (pageBlockAudioData.audioId, pageBlockAudioData.caption)
self = .audio(id: MediaId(namespace: Namespaces.Media.CloudFile, id: audioId), caption: InstantPageCaption(apiCaption: caption))
case let .pageBlockKicker(pageBlockKickerData):
let text = pageBlockKickerData.text
self = .kicker(RichText(apiText: text))
case let .pageBlockTable(pageBlockTableData):
let (flags, title, rows) = (pageBlockTableData.flags, pageBlockTableData.title, pageBlockTableData.rows)
self = .table(title: RichText(apiText: title), rows: rows.map({ InstantPageTableRow(apiTableRow: $0) }), bordered: (flags & (1 << 0)) != 0, striped: (flags & (1 << 1)) != 0)
case let .pageBlockList(pageBlockListData):
let items = pageBlockListData.items
self = .list(items: items.map({ InstantPageListItem(apiListItem: $0) }), ordered: false)
case let .pageBlockOrderedList(pageBlockOrderedListData):
let items = pageBlockOrderedListData.items
self = .list(items: items.map({ InstantPageListItem(apiListOrderedItem: $0) }), ordered: true)
case let .pageBlockDetails(pageBlockDetailsData):
let (flags, blocks, title) = (pageBlockDetailsData.flags, pageBlockDetailsData.blocks, pageBlockDetailsData.title)
self = .details(title: RichText(apiText: title), blocks: blocks.map({ InstantPageBlock(apiBlock: $0) }), expanded: (flags & (1 << 0)) != 0)
case let .pageBlockRelatedArticles(pageBlockRelatedArticlesData):
let (title, articles) = (pageBlockRelatedArticlesData.title, pageBlockRelatedArticlesData.articles)
self = .relatedArticles(title: RichText(apiText: title), articles: articles.map({ InstantPageRelatedArticle(apiRelatedArticle: $0) }))
case let .pageBlockMap(pageBlockMapData):
let (geo, zoom, w, h, caption) = (pageBlockMapData.geo, pageBlockMapData.zoom, pageBlockMapData.w, pageBlockMapData.h, pageBlockMapData.caption)
switch geo {
case let .geoPoint(geoPointData):
let (long, lat) = (geoPointData.long, geoPointData.lat)
self = .map(latitude: lat, longitude: long, zoom: zoom, dimensions: PixelDimensions(width: w, height: h), caption: InstantPageCaption(apiCaption: caption))
default:
self = .unsupported
}
}
}
}
extension InstantPage {
convenience init(apiPage: Api.Page) {
let blocks: [Api.PageBlock]
let photos: [Api.Photo]
let files: [Api.Document]
let isComplete: Bool
let rtl: Bool
let url: String
let views: Int32?
switch apiPage {
case let .page(pageData):
let (flags, pageUrl, pageBlocks, pagePhotos, pageDocuments, pageViews) = (pageData.flags, pageData.url, pageData.blocks, pageData.photos, pageData.documents, pageData.views)
url = pageUrl
blocks = pageBlocks
photos = pagePhotos
files = pageDocuments
isComplete = (flags & (1 << 0)) == 0
rtl = (flags & (1 << 1)) != 0
views = pageViews
}
var media: [MediaId: Media] = [:]
for photo in photos {
if let image = telegramMediaImageFromApiPhoto(photo), let id = image.id {
media[id] = image
}
}
for file in files {
if let file = telegramMediaFileFromApiDocument(file, altDocuments: []), let id = file.id {
media[id] = file
}
}
self.init(blocks: blocks.map({ InstantPageBlock(apiBlock: $0) }), media: media, isComplete: isComplete, rtl: rtl, url: url, views: views)
}
}
@@ -0,0 +1,7 @@
import Foundation
extension SecretChatFileReference {
func resource(key: SecretFileEncryptionKey, decryptedSize: Int64) -> SecretFileMediaResource {
return SecretFileMediaResource(fileId: self.id, accessHash: self.accessHash, containerSize: self.size, decryptedSize: decryptedSize, datacenterId: Int(self.datacenterId), key: key)
}
}
@@ -0,0 +1,20 @@
import Foundation
import Postbox
import TelegramApi
extension RestrictionRule {
convenience init(apiReason: Api.RestrictionReason) {
switch apiReason {
case let .restrictionReason(restrictionReasonData):
let (platform, reason, text) = (restrictionReasonData.platform, restrictionReasonData.reason, restrictionReasonData.text)
self.init(platform: platform, reason: reason, text: text)
}
}
}
extension PeerAccessRestrictionInfo {
convenience init(apiReasons: [Api.RestrictionReason]) {
self.init(rules: apiReasons.map(RestrictionRule.init(apiReason:)))
}
}
@@ -0,0 +1,21 @@
import Foundation
import Postbox
import TelegramApi
extension PeerGeoLocation {
init?(apiLocation: Api.ChannelLocation) {
switch apiLocation {
case let .channelLocation(channelLocationData):
let (geopoint, address) = (channelLocationData.geoPoint, channelLocationData.address)
if case let .geoPoint(geoPointData) = geopoint {
let (longitude, latitude) = (geoPointData.long, geoPointData.lat)
self.init(latitude: latitude, longitude: longitude, address: address)
} else {
return nil
}
default:
return nil
}
}
}
@@ -0,0 +1,325 @@
import Foundation
import Postbox
import TelegramApi
extension ReactionsMessageAttribute {
func withUpdatedResults(_ reactions: Api.MessageReactions) -> ReactionsMessageAttribute {
switch reactions {
case let .messageReactions(messageReactionsData):
let (flags, results, recentReactions, topReactors) = (messageReactionsData.flags, messageReactionsData.results, messageReactionsData.recentReactions, messageReactionsData.topReactors)
let min = (flags & (1 << 0)) != 0
let canViewList = (flags & (1 << 2)) != 0
let isTags = (flags & (1 << 3)) != 0
var reactions = results.compactMap { result -> MessageReaction? in
switch result {
case let .reactionCount(reactionCountData):
let (_, chosenOrder, reaction, count) = (reactionCountData.flags, reactionCountData.chosenOrder, reactionCountData.reaction, reactionCountData.count)
if let reaction = MessageReaction.Reaction(apiReaction: reaction) {
return MessageReaction(value: reaction, count: count, chosenOrder: chosenOrder.flatMap(Int.init))
} else {
return nil
}
}
}
let parsedRecentReactions: [ReactionsMessageAttribute.RecentPeer]
if let recentReactions = recentReactions {
parsedRecentReactions = recentReactions.compactMap { recentReaction -> ReactionsMessageAttribute.RecentPeer? in
switch recentReaction {
case let .messagePeerReaction(messagePeerReactionData):
let (flags, peerId, date, reaction) = (messagePeerReactionData.flags, messagePeerReactionData.peerId, messagePeerReactionData.date, messagePeerReactionData.reaction)
let isLarge = (flags & (1 << 0)) != 0
let isUnseen = (flags & (1 << 1)) != 0
let isMy = (flags & (1 << 2)) != 0
if let reaction = MessageReaction.Reaction(apiReaction: reaction) {
return ReactionsMessageAttribute.RecentPeer(value: reaction, isLarge: isLarge, isUnseen: isUnseen, isMy: isMy, peerId: peerId.peerId, timestamp: date)
} else {
return nil
}
}
}
} else {
parsedRecentReactions = []
}
if min {
var currentSelectedReactions: [MessageReaction.Reaction: Int] = [:]
for reaction in self.reactions {
if let chosenOrder = reaction.chosenOrder {
currentSelectedReactions[reaction.value] = chosenOrder
break
}
}
if !currentSelectedReactions.isEmpty {
for i in 0 ..< reactions.count {
if let chosenOrder = currentSelectedReactions[reactions[i].value] {
reactions[i].chosenOrder = chosenOrder
}
}
}
}
var topPeers: [ReactionsMessageAttribute.TopPeer] = []
if let topReactors {
for item in topReactors {
switch item {
case let .messageReactor(messageReactorData):
let (flags, peerId, count) = (messageReactorData.flags, messageReactorData.peerId, messageReactorData.count)
topPeers.append(ReactionsMessageAttribute.TopPeer(
peerId: peerId?.peerId,
count: count,
isTop: (flags & (1 << 0)) != 0,
isMy: (flags & (1 << 1)) != 0,
isAnonymous: (flags & (1 << 2)) != 0
))
}
}
}
return ReactionsMessageAttribute(canViewList: canViewList, isTags: isTags, reactions: reactions, recentPeers: parsedRecentReactions, topPeers: topPeers)
}
}
}
public func mergedMessageReactionsAndPeers(accountPeerId: EnginePeer.Id, accountPeer: EnginePeer?, message: Message) -> (reactions: [MessageReaction], peers: [(MessageReaction.Reaction, EnginePeer)]) {
guard let attribute = mergedMessageReactions(attributes: message.attributes, isTags: message.areReactionsTags(accountPeerId: accountPeerId)) else {
return ([], [])
}
var recentPeers: [(MessageReaction.Reaction, EnginePeer)] = []
if message.id.peerId.namespace == Namespaces.Peer.CloudUser {
for reaction in attribute.reactions {
var selfCount: Int32 = 0
if reaction.isSelected {
selfCount += 1
if let accountPeer = accountPeer {
recentPeers.append((reaction.value, accountPeer))
}
}
if reaction.count >= selfCount + 1 {
if let peer = message.peers[message.id.peerId] {
recentPeers.append((reaction.value, EnginePeer(peer)))
}
}
}
} else {
recentPeers = attribute.recentPeers.compactMap { recentPeer -> (MessageReaction.Reaction, EnginePeer)? in
if let peer = message.peers[recentPeer.peerId] {
return (recentPeer.value, EnginePeer(peer))
} else {
return nil
}
}
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
recentPeers.removeAll()
}
}
let reactions = attribute.reactions
return (reactions, recentPeers)
}
private func mergeReactions(reactions: [MessageReaction], recentPeers: [ReactionsMessageAttribute.RecentPeer], pending: [PendingReactionsMessageAttribute.PendingReaction], accountPeerId: PeerId) -> ([MessageReaction], [ReactionsMessageAttribute.RecentPeer]) {
var result = reactions
var recentPeers = recentPeers
var pendingIndex: Int = Int(Int32.max - 100)
for pendingReaction in pending {
if let index = result.firstIndex(where: { $0.value == pendingReaction.value }) {
var merged = result[index]
if merged.chosenOrder == nil {
merged.chosenOrder = pendingIndex
pendingIndex += 1
merged.count += 1
}
result[index] = merged
} else {
result.append(MessageReaction(value: pendingReaction.value, count: 1, chosenOrder: pendingIndex))
pendingIndex += 1
}
let pendingReactionSendAsPeerId = pendingReaction.sendAsPeerId ?? accountPeerId
if let index = recentPeers.firstIndex(where: {
$0.value == pendingReaction.value && ($0.peerId == pendingReactionSendAsPeerId || $0.isMy)
}) {
recentPeers.remove(at: index)
}
recentPeers.append(ReactionsMessageAttribute.RecentPeer(value: pendingReaction.value, isLarge: false, isUnseen: false, isMy: true, peerId: pendingReactionSendAsPeerId, timestamp: Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)))
}
for i in (0 ..< result.count).reversed() {
if result[i].chosenOrder != nil {
if !pending.contains(where: { $0.value == result[i].value }), result[i].value != .stars {
if let index = recentPeers.firstIndex(where: { $0.value == result[i].value && ($0.peerId == accountPeerId || $0.isMy) }) {
recentPeers.remove(at: index)
}
if result[i].count <= 1 {
result.remove(at: i)
} else {
result[i].count -= 1
result[i].chosenOrder = nil
}
}
}
}
if recentPeers.count > 3 {
recentPeers.removeFirst(recentPeers.count - 3)
}
return (result, recentPeers)
}
public func mergedMessageReactions(attributes: [MessageAttribute], isTags: Bool) -> ReactionsMessageAttribute? {
var current: ReactionsMessageAttribute?
var pending: PendingReactionsMessageAttribute?
var pendingStars: PendingStarsReactionsMessageAttribute?
for attribute in attributes {
if let attribute = attribute as? ReactionsMessageAttribute {
current = attribute
} else if let attribute = attribute as? PendingReactionsMessageAttribute {
pending = attribute
} else if let attribute = attribute as? PendingStarsReactionsMessageAttribute {
pendingStars = attribute
}
}
let result: ReactionsMessageAttribute?
if let pending = pending, let accountPeerId = pending.accountPeerId {
var reactions = current?.reactions ?? []
var recentPeers = current?.recentPeers ?? []
let (updatedReactions, updatedRecentPeers) = mergeReactions(reactions: reactions, recentPeers: recentPeers, pending: pending.reactions, accountPeerId: accountPeerId)
reactions = updatedReactions
recentPeers = updatedRecentPeers
if !reactions.isEmpty {
result = ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: reactions, recentPeers: recentPeers, topPeers: current?.topPeers ?? [])
} else {
result = nil
}
} else if let current {
result = current
} else {
result = nil
}
if let pendingStars {
if let result {
var reactions = result.reactions
var updatedCount: Int32 = pendingStars.count
if let index = reactions.firstIndex(where: { $0.value == .stars }) {
updatedCount += reactions[index].count
reactions.remove(at: index)
}
var topPeers = result.topPeers
if let index = topPeers.firstIndex(where: { $0.isMy }) {
topPeers[index].count += pendingStars.count
} else {
let isAnonymous: Bool
let topPeerId: PeerId?
switch pendingStars.privacy {
case .anonymous:
isAnonymous = true
topPeerId = pendingStars.accountPeerId
case .default:
isAnonymous = false
topPeerId = pendingStars.accountPeerId
case let .peer(peerId):
isAnonymous = false
topPeerId = peerId
}
topPeers.append(ReactionsMessageAttribute.TopPeer(peerId: topPeerId, count: pendingStars.count, isTop: false, isMy: true, isAnonymous: isAnonymous))
}
reactions.insert(MessageReaction(value: .stars, count: updatedCount, chosenOrder: -1), at: 0)
return ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: reactions, recentPeers: result.recentPeers, topPeers: topPeers)
} else {
let isAnonymous: Bool
let topPeerId: PeerId?
switch pendingStars.privacy {
case .anonymous:
isAnonymous = true
topPeerId = pendingStars.accountPeerId
case .default:
isAnonymous = false
topPeerId = pendingStars.accountPeerId
case let .peer(peerId):
isAnonymous = false
topPeerId = peerId
}
return ReactionsMessageAttribute(canViewList: current?.canViewList ?? false, isTags: current?.isTags ?? isTags, reactions: [MessageReaction(value: .stars, count: pendingStars.count, chosenOrder: -1)], recentPeers: [], topPeers: [ReactionsMessageAttribute.TopPeer(peerId: topPeerId, count: pendingStars.count, isTop: false, isMy: true, isAnonymous: isAnonymous)])
}
} else {
return result
}
}
extension ReactionsMessageAttribute {
convenience init(apiReactions: Api.MessageReactions) {
switch apiReactions {
case let .messageReactions(messageReactionsData):
let (flags, results, recentReactions, topReactors) = (messageReactionsData.flags, messageReactionsData.results, messageReactionsData.recentReactions, messageReactionsData.topReactors)
let canViewList = (flags & (1 << 2)) != 0
let isTags = (flags & (1 << 3)) != 0
let parsedRecentReactions: [ReactionsMessageAttribute.RecentPeer]
if let recentReactions = recentReactions {
parsedRecentReactions = recentReactions.compactMap { recentReaction -> ReactionsMessageAttribute.RecentPeer? in
switch recentReaction {
case let .messagePeerReaction(messagePeerReactionData):
let (flags, peerId, date, reaction) = (messagePeerReactionData.flags, messagePeerReactionData.peerId, messagePeerReactionData.date, messagePeerReactionData.reaction)
let isLarge = (flags & (1 << 0)) != 0
let isUnseen = (flags & (1 << 1)) != 0
let isMy = (flags & (1 << 2)) != 0
if let reaction = MessageReaction.Reaction(apiReaction: reaction) {
return ReactionsMessageAttribute.RecentPeer(value: reaction, isLarge: isLarge, isUnseen: isUnseen, isMy: isMy, peerId: peerId.peerId, timestamp: date)
} else {
return nil
}
}
}
} else {
parsedRecentReactions = []
}
var topPeers: [ReactionsMessageAttribute.TopPeer] = []
if let topReactors {
for item in topReactors {
switch item {
case let .messageReactor(messageReactorData):
let (flags, peerId, count) = (messageReactorData.flags, messageReactorData.peerId, messageReactorData.count)
topPeers.append(ReactionsMessageAttribute.TopPeer(
peerId: peerId?.peerId,
count: count,
isTop: (flags & (1 << 0)) != 0,
isMy: (flags & (1 << 1)) != 0,
isAnonymous: (flags & (1 << 2)) != 0
))
}
}
}
self.init(
canViewList: canViewList,
isTags: isTags,
reactions: results.compactMap { result -> MessageReaction? in
switch result {
case let .reactionCount(reactionCountData):
let (_, chosenOrder, reaction, count) = (reactionCountData.flags, reactionCountData.chosenOrder, reactionCountData.reaction, reactionCountData.count)
if let reaction = MessageReaction.Reaction(apiReaction: reaction) {
return MessageReaction(value: reaction, count: count, chosenOrder: chosenOrder.flatMap(Int.init))
} else {
return nil
}
}
},
recentPeers: parsedRecentReactions,
topPeers: topPeers
)
}
}
}
@@ -0,0 +1,27 @@
import Foundation
import Postbox
import SwiftSignalKit
func currentWebDocumentsHostDatacenterId(postbox: Postbox, isTestingEnvironment: Bool) -> Signal<Int32, NoError> {
return postbox.transaction { transaction -> Int32 in
if let entry = transaction.getPreferencesEntry(key: PreferencesKeys.remoteStorageConfiguration)?.get(RemoteStorageConfiguration.self) {
return entry.webDocumentsHostDatacenterId
} else {
if isTestingEnvironment {
return 2
} else {
return 4
}
}
}
}
func updateRemoteStorageConfiguration(transaction: Transaction, configuration: RemoteStorageConfiguration) {
let current = transaction.getPreferencesEntry(key: PreferencesKeys.remoteStorageConfiguration)?.get(RemoteStorageConfiguration.self)
if let current = current, current == configuration {
return
}
transaction.setPreferencesEntry(key: PreferencesKeys.remoteStorageConfiguration, value: PreferencesEntry(configuration))
}
@@ -0,0 +1,222 @@
import Foundation
import Postbox
import TelegramApi
extension ReplyMarkupButtonAction.PeerTypes {
init(apiType: [Api.InlineQueryPeerType]) {
var rawValue: Int32 = 0
for type in apiType {
switch type {
case .inlineQueryPeerTypePM:
rawValue |= ReplyMarkupButtonAction.PeerTypes.users.rawValue
case .inlineQueryPeerTypeBotPM:
rawValue |= ReplyMarkupButtonAction.PeerTypes.bots.rawValue
case .inlineQueryPeerTypeBroadcast:
rawValue |= ReplyMarkupButtonAction.PeerTypes.channels.rawValue
case .inlineQueryPeerTypeChat, .inlineQueryPeerTypeMegagroup:
rawValue |= ReplyMarkupButtonAction.PeerTypes.groups.rawValue
case .inlineQueryPeerTypeSameBotPM:
break
}
}
self.init(rawValue: rawValue)
}
}
extension ReplyMarkupButton {
init(apiButton: Api.KeyboardButton) {
switch apiButton {
case let .keyboardButton(keyboardButtonData):
let text = keyboardButtonData.text
self.init(title: text, titleWhenForwarded: nil, action: .text, style: keyboardButtonData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .keyboardButtonCallback(keyboardButtonCallbackData):
let (flags, text, data) = (keyboardButtonCallbackData.flags, keyboardButtonCallbackData.text, keyboardButtonCallbackData.data)
let memory = malloc(data.size)!
memcpy(memory, data.data, data.size)
let dataBuffer = MemoryBuffer(memory: memory, capacity: data.size, length: data.size, freeWhenDone: true)
self.init(title: text, titleWhenForwarded: nil, action: .callback(requiresPassword: (flags & (1 << 0)) != 0, data: dataBuffer), style: keyboardButtonCallbackData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .keyboardButtonRequestGeoLocation(keyboardButtonRequestGeoLocationData):
let text = keyboardButtonRequestGeoLocationData.text
self.init(title: text, titleWhenForwarded: nil, action: .requestMap, style: keyboardButtonRequestGeoLocationData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .keyboardButtonRequestPhone(keyboardButtonRequestPhoneData):
let text = keyboardButtonRequestPhoneData.text
self.init(title: text, titleWhenForwarded: nil, action: .requestPhone, style: keyboardButtonRequestPhoneData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .keyboardButtonSwitchInline(keyboardButtonSwitchInlineData):
let (flags, text, query, types) = (keyboardButtonSwitchInlineData.flags, keyboardButtonSwitchInlineData.text, keyboardButtonSwitchInlineData.query, keyboardButtonSwitchInlineData.peerTypes)
var peerTypes = ReplyMarkupButtonAction.PeerTypes()
if let types = types {
for type in types {
switch type {
case .inlineQueryPeerTypePM:
peerTypes.insert(.users)
case .inlineQueryPeerTypeBotPM:
peerTypes.insert(.bots)
case .inlineQueryPeerTypeBroadcast:
peerTypes.insert(.channels)
case .inlineQueryPeerTypeChat, .inlineQueryPeerTypeMegagroup:
peerTypes.insert(.groups)
case .inlineQueryPeerTypeSameBotPM:
break
}
}
}
self.init(title: text, titleWhenForwarded: nil, action: .switchInline(samePeer: (flags & (1 << 0)) != 0, query: query, peerTypes: peerTypes), style: keyboardButtonSwitchInlineData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .keyboardButtonUrl(keyboardButtonUrlData):
let (text, url) = (keyboardButtonUrlData.text, keyboardButtonUrlData.url)
self.init(title: text, titleWhenForwarded: nil, action: .url(url), style: keyboardButtonUrlData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .keyboardButtonGame(keyboardButtonGameData):
let text = keyboardButtonGameData.text
self.init(title: text, titleWhenForwarded: nil, action: .openWebApp, style: keyboardButtonGameData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .keyboardButtonBuy(keyboardButtonBuyData):
let text = keyboardButtonBuyData.text
self.init(title: text, titleWhenForwarded: nil, action: .payment, style: keyboardButtonBuyData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .keyboardButtonUrlAuth(keyboardButtonUrlAuthData):
let (text, fwdText, url, buttonId) = (keyboardButtonUrlAuthData.text, keyboardButtonUrlAuthData.fwdText, keyboardButtonUrlAuthData.url, keyboardButtonUrlAuthData.buttonId)
self.init(title: text, titleWhenForwarded: fwdText, action: .urlAuth(url: url, buttonId: buttonId), style: keyboardButtonUrlAuthData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .inputKeyboardButtonUrlAuth(inputKeyboardButtonUrlAuthData):
let (text, fwdText, url) = (inputKeyboardButtonUrlAuthData.text, inputKeyboardButtonUrlAuthData.fwdText, inputKeyboardButtonUrlAuthData.url)
self.init(title: text, titleWhenForwarded: fwdText, action: .urlAuth(url: url, buttonId: 0), style: inputKeyboardButtonUrlAuthData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .keyboardButtonRequestPoll(keyboardButtonRequestPollData):
let (quiz, text) = (keyboardButtonRequestPollData.quiz, keyboardButtonRequestPollData.text)
let isQuiz: Bool? = quiz.flatMap { quiz in
if case .boolTrue = quiz {
return true
} else {
return false
}
}
self.init(title: text, titleWhenForwarded: nil, action: .setupPoll(isQuiz: isQuiz), style: keyboardButtonRequestPollData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .keyboardButtonUserProfile(keyboardButtonUserProfileData):
let (text, userId) = (keyboardButtonUserProfileData.text, keyboardButtonUserProfileData.userId)
self.init(title: text, titleWhenForwarded: nil, action: .openUserProfile(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))), style: keyboardButtonUserProfileData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .inputKeyboardButtonUserProfile(inputKeyboardButtonUserProfileData):
let text = inputKeyboardButtonUserProfileData.text
self.init(title: text, titleWhenForwarded: nil, action: .openUserProfile(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0))), style: inputKeyboardButtonUserProfileData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .keyboardButtonWebView(keyboardButtonWebViewData):
let (text, url) = (keyboardButtonWebViewData.text, keyboardButtonWebViewData.url)
self.init(title: text, titleWhenForwarded: nil, action: .openWebView(url: url, simple: false), style: keyboardButtonWebViewData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .keyboardButtonSimpleWebView(keyboardButtonSimpleWebViewData):
let (text, url) = (keyboardButtonSimpleWebViewData.text, keyboardButtonSimpleWebViewData.url)
self.init(title: text, titleWhenForwarded: nil, action: .openWebView(url: url, simple: true), style: keyboardButtonSimpleWebViewData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .keyboardButtonRequestPeer(keyboardButtonRequestPeerData):
let (text, buttonId, peerType, maxQuantity) = (keyboardButtonRequestPeerData.text, keyboardButtonRequestPeerData.buttonId, keyboardButtonRequestPeerData.peerType, keyboardButtonRequestPeerData.maxQuantity)
let mappedPeerType: ReplyMarkupButtonRequestPeerType
switch peerType {
case let .requestPeerTypeUser(requestPeerTypeUserData):
let (bot, premium) = (requestPeerTypeUserData.bot, requestPeerTypeUserData.premium)
mappedPeerType = .user(ReplyMarkupButtonRequestPeerType.User(
isBot: bot.flatMap({ $0 == .boolTrue }),
isPremium: premium.flatMap({ $0 == .boolTrue })
))
case let .requestPeerTypeChat(requestPeerTypeChatData):
let (flags, hasUsername, forum, userAdminRights, botAdminRights) = (requestPeerTypeChatData.flags, requestPeerTypeChatData.hasUsername, requestPeerTypeChatData.forum, requestPeerTypeChatData.userAdminRights, requestPeerTypeChatData.botAdminRights)
mappedPeerType = .group(ReplyMarkupButtonRequestPeerType.Group(
isCreator: (flags & (1 << 0)) != 0,
hasUsername: hasUsername.flatMap({ $0 == .boolTrue }),
isForum: forum.flatMap({ $0 == .boolTrue }),
botParticipant: (flags & (1 << 5)) != 0,
userAdminRights: userAdminRights.flatMap(TelegramChatAdminRights.init(apiAdminRights:)),
botAdminRights: botAdminRights.flatMap(TelegramChatAdminRights.init(apiAdminRights:))
))
case let .requestPeerTypeBroadcast(requestPeerTypeBroadcastData):
let (flags, hasUsername, userAdminRights, botAdminRights) = (requestPeerTypeBroadcastData.flags, requestPeerTypeBroadcastData.hasUsername, requestPeerTypeBroadcastData.userAdminRights, requestPeerTypeBroadcastData.botAdminRights)
mappedPeerType = .channel(ReplyMarkupButtonRequestPeerType.Channel(
isCreator: (flags & (1 << 0)) != 0,
hasUsername: hasUsername.flatMap({ $0 == .boolTrue }),
userAdminRights: userAdminRights.flatMap(TelegramChatAdminRights.init(apiAdminRights:)),
botAdminRights: botAdminRights.flatMap(TelegramChatAdminRights.init(apiAdminRights:))
))
}
self.init(title: text, titleWhenForwarded: nil, action: .requestPeer(peerType: mappedPeerType, buttonId: buttonId, maxQuantity: maxQuantity), style: keyboardButtonRequestPeerData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .inputKeyboardButtonRequestPeer(inputKeyboardButtonRequestPeerData):
let (text, buttonId, peerType, maxQuantity) = (inputKeyboardButtonRequestPeerData.text, inputKeyboardButtonRequestPeerData.buttonId, inputKeyboardButtonRequestPeerData.peerType, inputKeyboardButtonRequestPeerData.maxQuantity)
let mappedPeerType: ReplyMarkupButtonRequestPeerType
switch peerType {
case let .requestPeerTypeUser(requestPeerTypeUserData):
let (bot, premium) = (requestPeerTypeUserData.bot, requestPeerTypeUserData.premium)
mappedPeerType = .user(ReplyMarkupButtonRequestPeerType.User(
isBot: bot.flatMap({ $0 == .boolTrue }),
isPremium: premium.flatMap({ $0 == .boolTrue })
))
case let .requestPeerTypeChat(requestPeerTypeChatData):
let (flags, hasUsername, forum, userAdminRights, botAdminRights) = (requestPeerTypeChatData.flags, requestPeerTypeChatData.hasUsername, requestPeerTypeChatData.forum, requestPeerTypeChatData.userAdminRights, requestPeerTypeChatData.botAdminRights)
mappedPeerType = .group(ReplyMarkupButtonRequestPeerType.Group(
isCreator: (flags & (1 << 0)) != 0,
hasUsername: hasUsername.flatMap({ $0 == .boolTrue }),
isForum: forum.flatMap({ $0 == .boolTrue }),
botParticipant: (flags & (1 << 5)) != 0,
userAdminRights: userAdminRights.flatMap(TelegramChatAdminRights.init(apiAdminRights:)),
botAdminRights: botAdminRights.flatMap(TelegramChatAdminRights.init(apiAdminRights:))
))
case let .requestPeerTypeBroadcast(requestPeerTypeBroadcastData):
let (flags, hasUsername, userAdminRights, botAdminRights) = (requestPeerTypeBroadcastData.flags, requestPeerTypeBroadcastData.hasUsername, requestPeerTypeBroadcastData.userAdminRights, requestPeerTypeBroadcastData.botAdminRights)
mappedPeerType = .channel(ReplyMarkupButtonRequestPeerType.Channel(
isCreator: (flags & (1 << 0)) != 0,
hasUsername: hasUsername.flatMap({ $0 == .boolTrue }),
userAdminRights: userAdminRights.flatMap(TelegramChatAdminRights.init(apiAdminRights:)),
botAdminRights: botAdminRights.flatMap(TelegramChatAdminRights.init(apiAdminRights:))
))
}
self.init(title: text, titleWhenForwarded: nil, action: .requestPeer(peerType: mappedPeerType, buttonId: buttonId, maxQuantity: maxQuantity), style: inputKeyboardButtonRequestPeerData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
case let .keyboardButtonCopy(keyboardButtonCopyData):
let (text, payload) = (keyboardButtonCopyData.text, keyboardButtonCopyData.copyText)
self.init(title: text, titleWhenForwarded: nil, action: .copyText(payload: payload), style: keyboardButtonCopyData.style.flatMap(ReplyMarkupButton.Style.init(apiStyle:)))
}
}
}
extension ReplyMarkupRow {
init(apiRow: Api.KeyboardButtonRow) {
switch apiRow {
case let .keyboardButtonRow(keyboardButtonRowData):
let buttons = keyboardButtonRowData.buttons
self.init(buttons: buttons.map { ReplyMarkupButton(apiButton: $0) })
}
}
}
extension ReplyMarkupMessageAttribute {
convenience init(apiMarkup: Api.ReplyMarkup) {
var rows: [ReplyMarkupRow] = []
var flags = ReplyMarkupMessageFlags()
var placeholder: String?
switch apiMarkup {
case let .replyKeyboardMarkup(replyKeyboardMarkupData):
let (markupFlags, apiRows, apiPlaceholder) = (replyKeyboardMarkupData.flags, replyKeyboardMarkupData.rows, replyKeyboardMarkupData.placeholder)
rows = apiRows.map { ReplyMarkupRow(apiRow: $0) }
if (markupFlags & (1 << 0)) != 0 {
flags.insert(.fit)
}
if (markupFlags & (1 << 1)) != 0 {
flags.insert(.once)
}
if (markupFlags & (1 << 2)) != 0 {
flags.insert(.personal)
}
if (markupFlags & (1 << 4)) != 0 {
flags.insert(.persistent)
}
placeholder = apiPlaceholder
case let .replyInlineMarkup(replyInlineMarkupData):
let apiRows = replyInlineMarkupData.rows
rows = apiRows.map { ReplyMarkupRow(apiRow: $0) }
flags.insert(.inline)
case let .replyKeyboardForceReply(replyKeyboardForceReplyData):
let (forceReplyFlags, apiPlaceholder) = (replyKeyboardForceReplyData.flags, replyKeyboardForceReplyData.placeholder)
if (forceReplyFlags & (1 << 1)) != 0 {
flags.insert(.once)
}
if (forceReplyFlags & (1 << 2)) != 0 {
flags.insert(.personal)
}
flags.insert(.setupReply)
placeholder = apiPlaceholder
case let .replyKeyboardHide(replyKeyboardHideData):
let hideFlags = replyKeyboardHideData.flags
if (hideFlags & (1 << 2)) != 0 {
flags.insert(.personal)
}
}
self.init(rows: rows, flags: flags, placeholder: placeholder)
}
}
@@ -0,0 +1,58 @@
import Foundation
import Postbox
import TelegramApi
extension RichText {
init(apiText: Api.RichText) {
switch apiText {
case .textEmpty:
self = .empty
case let .textPlain(textPlainData):
let text = textPlainData.text
self = .plain(text)
case let .textBold(textBoldData):
let text = textBoldData.text
self = .bold(RichText(apiText: text))
case let .textItalic(textItalicData):
let text = textItalicData.text
self = .italic(RichText(apiText: text))
case let .textUnderline(textUnderlineData):
let text = textUnderlineData.text
self = .underline(RichText(apiText: text))
case let .textStrike(textStrikeData):
let text = textStrikeData.text
self = .strikethrough(RichText(apiText: text))
case let .textFixed(textFixedData):
let text = textFixedData.text
self = .fixed(RichText(apiText: text))
case let .textUrl(textUrlData):
let (text, url, webpageId) = (textUrlData.text, textUrlData.url, textUrlData.webpageId)
self = .url(text: RichText(apiText: text), url: url, webpageId: webpageId == 0 ? nil : MediaId(namespace: Namespaces.Media.CloudWebpage, id: webpageId))
case let .textEmail(textEmailData):
let (text, email) = (textEmailData.text, textEmailData.email)
self = .email(text: RichText(apiText: text), email: email)
case let .textConcat(textConcatData):
let texts = textConcatData.texts
self = .concat(texts.map({ RichText(apiText: $0) }))
case let .textSubscript(textSubscriptData):
let text = textSubscriptData.text
self = .subscript(RichText(apiText: text))
case let .textSuperscript(textSuperscriptData):
let text = textSuperscriptData.text
self = .superscript(RichText(apiText: text))
case let .textMarked(textMarkedData):
let text = textMarkedData.text
self = .marked(RichText(apiText: text))
case let .textPhone(textPhoneData):
let (text, phone) = (textPhoneData.text, textPhoneData.phone)
self = .phone(text: RichText(apiText: text), phone: phone)
case let .textImage(textImageData):
let (documentId, w, h) = (textImageData.documentId, textImageData.w, textImageData.h)
self = .image(id: MediaId(namespace: Namespaces.Media.CloudFile, id: documentId), dimensions: PixelDimensions(width: w, height: h))
case let .textAnchor(textAnchorData):
let (text, name) = (textAnchorData.text, textAnchorData.name)
self = .anchor(text: RichText(apiText: text), name: name)
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,315 @@
import Foundation
import Postbox
public enum TelegramChannelPermission {
case sendText
case sendPhoto
case sendVideo
case sendSomething
case pinMessages
case manageTopics
case createTopics
case inviteMembers
case editAllMessages
case deleteAllMessages
case banMembers
case addAdmins
case changeInfo
case canBeAnonymous
case manageCalls
case postStories
case editStories
case deleteStories
case manageDirect
case editRank
case manageRanks
}
public extension TelegramChannel {
func hasPermission(_ permission: TelegramChannelPermission, ignoreDefault: Bool = false) -> Bool {
if self.flags.contains(.isCreator) {
if case .canBeAnonymous = permission {
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canBeAnonymous)
} else {
return false
}
}
return true
}
switch permission {
case .sendText:
if case .broadcast = self.info {
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canPostMessages)
} else {
return false
}
} else {
if let _ = self.adminRights {
return true
}
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendText) {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendText) && !ignoreDefault {
return false
}
return true
}
case .sendPhoto:
if case .broadcast = self.info {
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canPostMessages)
} else {
return false
}
} else {
if let _ = self.adminRights {
return true
}
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendPhotos) {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendPhotos) && !ignoreDefault {
return false
}
return true
}
case .sendVideo:
if case .broadcast = self.info {
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canPostMessages)
} else {
return false
}
} else {
if let _ = self.adminRights {
return true
}
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banSendVideos) {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banSendVideos) && !ignoreDefault {
return false
}
return true
}
case .sendSomething:
if case .broadcast = self.info {
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canPostMessages)
} else {
return false
}
} else {
if let _ = self.adminRights {
return true
}
let flags: TelegramChatBannedRightsFlags = [
.banSendText,
.banSendInstantVideos,
.banSendVoice,
.banSendPhotos,
.banSendVideos,
.banSendStickers,
.banSendPolls,
.banSendFiles,
.banSendInline,
.banSendMusic
]
if let bannedRights = self.bannedRights, bannedRights.flags.intersection(flags) == flags {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.intersection(flags) == flags && !ignoreDefault {
return false
}
return true
}
case .pinMessages:
if case .broadcast = self.info {
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canPinMessages) || adminRights.rights.contains(.canEditMessages)
} else {
return false
}
} else {
if let adminRights = self.adminRights, adminRights.rights.contains(.canPinMessages) {
return true
}
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banPinMessages) {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banPinMessages) {
return false
}
return true
}
case .manageTopics:
if self.flags.contains(.isCreator) {
return true
}
if self.adminRights == nil {
return false
}
if let adminRights = self.adminRights, adminRights.rights.contains(.canManageTopics) {
return true
}
return false
case .createTopics:
if self.flags.contains(.isCreator) {
return true
}
if let adminRights = self.adminRights, adminRights.rights.contains(.canManageTopics) {
return true
}
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banManageTopics) {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banManageTopics) {
return false
}
return true
case .inviteMembers:
if case .broadcast = self.info {
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canInviteUsers)
} else {
return false
}
} else {
if let adminRights = self.adminRights, adminRights.rights.contains(.canInviteUsers) {
return true
}
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banAddMembers) {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banAddMembers) {
return false
}
return true
}
case .editAllMessages:
if let adminRights = self.adminRights, adminRights.rights.contains(.canEditMessages) {
return true
}
return false
case .deleteAllMessages:
if let adminRights = self.adminRights, adminRights.rights.contains(.canDeleteMessages) {
return true
}
return false
case .banMembers:
if let adminRights = self.adminRights, adminRights.rights.contains(.canBanUsers) {
return true
}
return false
case .changeInfo:
if case .broadcast = self.info {
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canChangeInfo)
} else {
return false
}
} else {
if let adminRights = self.adminRights, adminRights.rights.contains(.canChangeInfo) {
return true
}
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banChangeInfo) {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banChangeInfo) {
return false
}
return true
}
case .addAdmins:
if let adminRights = self.adminRights, adminRights.rights.contains(.canAddAdmins) {
return true
}
return false
case .manageDirect:
if let adminRights = self.adminRights, adminRights.rights.contains(.canManageDirect) {
return true
}
return false
case .manageCalls:
if let adminRights = self.adminRights, adminRights.rights.contains(.canManageCalls) {
return true
}
return false
case .canBeAnonymous:
if let adminRights = self.adminRights, adminRights.rights.contains(.canBeAnonymous) {
return true
}
return false
case .postStories:
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canPostStories)
} else {
return false
}
case .editStories:
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canEditStories)
} else {
return false
}
case .deleteStories:
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canDeleteStories)
} else {
return false
}
case .editRank:
if let adminRights = self.adminRights, adminRights.rights.contains(.canManageRanks) {
return true
}
if let bannedRights = self.bannedRights, bannedRights.flags.contains(.banEditRank) {
return false
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(.banEditRank) {
return false
}
return true
case .manageRanks:
if let adminRights = self.adminRights {
return adminRights.rights.contains(.canManageRanks)
} else {
return false
}
}
}
func hasBannedPermission(_ rights: TelegramChatBannedRightsFlags, ignoreDefault: Bool = false) -> (Int32, Bool)? {
if self.flags.contains(.isCreator) {
return nil
}
if let _ = self.adminRights {
return nil
}
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.contains(rights) && !ignoreDefault {
return (Int32.max, false)
}
if let bannedRights = self.bannedRights, bannedRights.flags.contains(rights) {
return (bannedRights.untilDate, true)
}
return nil
}
var isRestrictedBySlowmode: Bool {
if self.flags.contains(.isCreator) {
return false
}
if let _ = self.adminRights {
return false
}
if case let .group(group) = self.info {
return group.flags.contains(.slowModeEnabled)
} else {
return false
}
}
}
@@ -0,0 +1,24 @@
import Foundation
import Postbox
import TelegramApi
extension TelegramChatAdminRights {
init?(apiAdminRights: Api.ChatAdminRights) {
switch apiAdminRights {
case let .chatAdminRights(chatAdminRightsData):
let flags = chatAdminRightsData.flags
if flags == 0 {
return nil
}
let filteredFlags = flags & (~(1 << 12))
self.init(rights: TelegramChatAdminRightsFlags(rawValue: filteredFlags))
}
}
var apiAdminRights: Api.ChatAdminRights {
var filteredFlags = self.rights.rawValue
filteredFlags |= 1 << 12
return .chatAdminRights(Api.ChatAdminRights.Cons_chatAdminRights(flags: filteredFlags))
}
}
@@ -0,0 +1,24 @@
import Foundation
import Postbox
import TelegramApi
extension TelegramChatBannedRights {
init(apiBannedRights: Api.ChatBannedRights) {
switch apiBannedRights {
case let .chatBannedRights(chatBannedRightsData):
let (flags, untilDate) = (chatBannedRightsData.flags, chatBannedRightsData.untilDate)
var effectiveFlags = TelegramChatBannedRightsFlags(rawValue: flags)
effectiveFlags.remove(.banSendMedia)
effectiveFlags.remove(TelegramChatBannedRightsFlags(rawValue: 1 << 1))
self.init(flags: effectiveFlags, untilDate: untilDate)
}
}
var apiBannedRights: Api.ChatBannedRights {
var effectiveFlags = self.flags
effectiveFlags.remove(.banSendMedia)
effectiveFlags.remove(TelegramChatBannedRightsFlags(rawValue: 1 << 1))
return .chatBannedRights(.init(flags: effectiveFlags.rawValue, untilDate: self.untilDate))
}
}
@@ -0,0 +1,29 @@
import Foundation
import Postbox
import TelegramApi
extension TelegramExtendedMedia {
init?(apiExtendedMedia: Api.MessageExtendedMedia, peerId: PeerId) {
switch apiExtendedMedia {
case let .messageExtendedMediaPreview(messageExtendedMediaPreviewData):
let (width, height, thumb, videoDuration) = (messageExtendedMediaPreviewData.w, messageExtendedMediaPreviewData.h, messageExtendedMediaPreviewData.thumb, messageExtendedMediaPreviewData.videoDuration)
var dimensions: PixelDimensions?
if let width = width, let height = height {
dimensions = PixelDimensions(width: width, height: height)
}
var immediateThumbnailData: Data?
if let thumb = thumb, case let .photoStrippedSize(photoStrippedSizeData) = thumb {
let bytes = photoStrippedSizeData.bytes
immediateThumbnailData = bytes.makeData()
}
self = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration)
case let .messageExtendedMedia(messageExtendedMediaData):
let apiMedia = messageExtendedMediaData.media
if let media = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId).media {
self = .full(media: media)
} else {
return nil
}
}
}
}
@@ -0,0 +1,55 @@
import Foundation
import Postbox
// Incuding at least one Objective-C class in a swift file ensures that it doesn't get stripped by the linker
private final class LinkHelperClass: NSObject {
}
public extension TelegramGroup {
enum Permission {
case sendSomething
}
func hasPermission(_ permission: Permission) -> Bool {
switch permission {
case .sendSomething:
switch self.role {
case .creator, .admin:
return true
default:
break
}
let flags: TelegramChatBannedRightsFlags = [
.banSendText,
.banSendInstantVideos,
.banSendVoice,
.banSendPhotos,
.banSendVideos,
.banSendStickers,
.banSendPolls,
.banSendFiles,
.banSendInline,
.banSendMusic
]
if let defaultBannedRights = self.defaultBannedRights, defaultBannedRights.flags.intersection(flags) == flags {
return false
}
return true
}
}
func hasBannedPermission(_ rights: TelegramChatBannedRightsFlags) -> Bool {
switch self.role {
case .creator, .admin:
return false
default:
if let bannedRights = self.defaultBannedRights {
return bannedRights.flags.contains(rights)
} else {
return false
}
}
}
}
@@ -0,0 +1,23 @@
import Foundation
import Postbox
import TelegramApi
public class InvertMediaMessageAttribute: MessageAttribute, Equatable {
public let associatedPeerIds: [PeerId] = []
public let associatedMediaIds: [MediaId] = []
public init() {
}
required public init(decoder: PostboxDecoder) {
}
public func encode(_ encoder: PostboxEncoder) {
}
public static func ==(lhs: InvertMediaMessageAttribute, rhs: InvertMediaMessageAttribute) -> Bool {
return true
}
}
@@ -0,0 +1,417 @@
import Foundation
import Postbox
import TelegramApi
func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMediaAction? {
switch action {
case let .messageActionChannelCreate(messageActionChannelCreateData):
let title = messageActionChannelCreateData.title
return TelegramMediaAction(action: .groupCreated(title: title))
case let .messageActionChannelMigrateFrom(messageActionChannelMigrateFromData):
let (title, chatId) = (messageActionChannelMigrateFromData.title, messageActionChannelMigrateFromData.chatId)
return TelegramMediaAction(action: .channelMigratedFromGroup(title: title, groupId: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatId))))
case let .messageActionChatAddUser(messageActionChatAddUserData):
let users = messageActionChatAddUserData.users
return TelegramMediaAction(action: .addedMembers(peerIds: users.map({ PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) })))
case let .messageActionChatCreate(messageActionChatCreateData):
let title = messageActionChatCreateData.title
return TelegramMediaAction(action: .groupCreated(title: title))
case .messageActionChatDeletePhoto:
return TelegramMediaAction(action: .photoUpdated(image: nil))
case let .messageActionChatDeleteUser(messageActionChatDeleteUserData):
let userId = messageActionChatDeleteUserData.userId
return TelegramMediaAction(action: .removedMembers(peerIds: [PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))]))
case let .messageActionChatEditPhoto(messageActionChatEditPhotoData):
let photo = messageActionChatEditPhotoData.photo
return TelegramMediaAction(action: .photoUpdated(image: telegramMediaImageFromApiPhoto(photo)))
case let .messageActionChatEditTitle(messageActionChatEditTitleData):
let title = messageActionChatEditTitleData.title
return TelegramMediaAction(action: .titleUpdated(title: title))
case let .messageActionChatJoinedByLink(messageActionChatJoinedByLinkData):
let inviterId = messageActionChatJoinedByLinkData.inviterId
return TelegramMediaAction(action: .joinedByLink(inviter: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(inviterId))))
case let .messageActionChatMigrateTo(messageActionChatMigrateToData):
let channelId = messageActionChatMigrateToData.channelId
return TelegramMediaAction(action: .groupMigratedToChannel(channelId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId))))
case .messageActionHistoryClear:
return TelegramMediaAction(action: .historyCleared)
case .messageActionPinMessage:
return TelegramMediaAction(action: .pinnedMessageUpdated)
case let .messageActionGameScore(messageActionGameScoreData):
let (gameId, score) = (messageActionGameScoreData.gameId, messageActionGameScoreData.score)
return TelegramMediaAction(action: .gameScore(gameId: gameId, score: score))
case let .messageActionPhoneCall(messageActionPhoneCallData):
let (flags, callId, reason, duration) = (messageActionPhoneCallData.flags, messageActionPhoneCallData.callId, messageActionPhoneCallData.reason, messageActionPhoneCallData.duration)
var discardReason: PhoneCallDiscardReason?
if let reason = reason {
discardReason = PhoneCallDiscardReason(apiReason: reason)
}
let isVideo = (flags & (1 << 2)) != 0
return TelegramMediaAction(action: .phoneCall(callId: callId, discardReason: discardReason, duration: duration, isVideo: isVideo))
case .messageActionEmpty:
return nil
case let .messageActionPaymentSent(messageActionPaymentSentData):
let (flags, currency, totalAmount, invoiceSlug, subscriptionUntilDate) = (messageActionPaymentSentData.flags, messageActionPaymentSentData.currency, messageActionPaymentSentData.totalAmount, messageActionPaymentSentData.invoiceSlug, messageActionPaymentSentData.subscriptionUntilDate)
let _ = subscriptionUntilDate
let isRecurringInit = (flags & (1 << 2)) != 0
let isRecurringUsed = (flags & (1 << 3)) != 0
return TelegramMediaAction(action: .paymentSent(currency: currency, totalAmount: totalAmount, invoiceSlug: invoiceSlug, isRecurringInit: isRecurringInit, isRecurringUsed: isRecurringUsed))
case .messageActionPaymentSentMe:
return nil
case .messageActionScreenshotTaken:
return TelegramMediaAction(action: .historyScreenshot)
case let .messageActionCustomAction(messageActionCustomActionData):
let message = messageActionCustomActionData.message
return TelegramMediaAction(action: .customText(text: message, entities: [], additionalAttributes: nil))
case let .messageActionBotAllowed(messageActionBotAllowedData):
let (flags, domain, app) = (messageActionBotAllowedData.flags, messageActionBotAllowedData.domain, messageActionBotAllowedData.app)
if let domain = domain {
return TelegramMediaAction(action: .botDomainAccessGranted(domain: domain))
} else {
var appName: String?
if case let .botApp(botAppData) = app {
let appNameValue = botAppData.title
appName = appNameValue
}
var type: BotSendMessageAccessGrantedType?
if (flags & (1 << 1)) != 0 {
type = .attachMenu
}
if (flags & (1 << 3)) != 0 {
type = .request
}
return TelegramMediaAction(action: .botAppAccessGranted(appName: appName, type: type))
}
case .messageActionSecureValuesSentMe:
return nil
case let .messageActionSecureValuesSent(messageActionSecureValuesSentData):
let types = messageActionSecureValuesSentData.types
return TelegramMediaAction(action: .botSentSecureValues(types: types.map(SentSecureValueType.init)))
case .messageActionContactSignUp:
return TelegramMediaAction(action: .peerJoined)
case let .messageActionGeoProximityReached(messageActionGeoProximityReachedData):
let (fromId, toId, distance) = (messageActionGeoProximityReachedData.fromId, messageActionGeoProximityReachedData.toId, messageActionGeoProximityReachedData.distance)
return TelegramMediaAction(action: .geoProximityReached(from: fromId.peerId, to: toId.peerId, distance: distance))
case let .messageActionGroupCall(messageActionGroupCallData):
let (call, duration) = (messageActionGroupCallData.call, messageActionGroupCallData.duration)
switch call {
case let .inputGroupCall(inputGroupCallData):
let (id, accessHash) = (inputGroupCallData.id, inputGroupCallData.accessHash)
return TelegramMediaAction(action: .groupPhoneCall(callId: id, accessHash: accessHash, scheduleDate: nil, duration: duration))
case .inputGroupCallSlug, .inputGroupCallInviteMessage:
return nil
}
case let .messageActionInviteToGroupCall(messageActionInviteToGroupCallData):
let (call, userIds) = (messageActionInviteToGroupCallData.call, messageActionInviteToGroupCallData.users)
switch call {
case let .inputGroupCall(inputGroupCallData):
let (id, accessHash) = (inputGroupCallData.id, inputGroupCallData.accessHash)
return TelegramMediaAction(action: .inviteToGroupPhoneCall(callId: id, accessHash: accessHash, peerIds: userIds.map { userId in
PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
}))
case .inputGroupCallSlug, .inputGroupCallInviteMessage:
return nil
}
case let .messageActionSetMessagesTTL(messageActionSetMessagesTTLData):
let (period, autoSettingFrom) = (messageActionSetMessagesTTLData.period, messageActionSetMessagesTTLData.autoSettingFrom)
return TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(period: period, autoSettingSource: autoSettingFrom.flatMap { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }))
case let .messageActionGroupCallScheduled(messageActionGroupCallScheduledData):
let (call, scheduleDate) = (messageActionGroupCallScheduledData.call, messageActionGroupCallScheduledData.scheduleDate)
switch call {
case let .inputGroupCall(inputGroupCallData):
let (id, accessHash) = (inputGroupCallData.id, inputGroupCallData.accessHash)
return TelegramMediaAction(action: .groupPhoneCall(callId: id, accessHash: accessHash, scheduleDate: scheduleDate, duration: nil))
case .inputGroupCallSlug, .inputGroupCallInviteMessage:
return nil
}
case let .messageActionSetChatTheme(messageActionSetChatThemeData):
let chatTheme = messageActionSetChatThemeData.theme
if let chatTheme = ChatTheme(apiChatTheme: chatTheme) {
return TelegramMediaAction(action: .setChatTheme(chatTheme: chatTheme))
} else {
return nil
}
case .messageActionChatJoinedByRequest:
return TelegramMediaAction(action: .joinedByRequest)
case let .messageActionWebViewDataSentMe(messageActionWebViewDataSentMeData):
let text = messageActionWebViewDataSentMeData.text
return TelegramMediaAction(action: .webViewData(text))
case let .messageActionWebViewDataSent(messageActionWebViewDataSentData):
let text = messageActionWebViewDataSentData.text
return TelegramMediaAction(action: .webViewData(text))
case let .messageActionGiftPremium(messageActionGiftPremiumData):
let (currency, amount, days, cryptoCurrency, cryptoAmount, message) = (messageActionGiftPremiumData.currency, messageActionGiftPremiumData.amount, messageActionGiftPremiumData.days, messageActionGiftPremiumData.cryptoCurrency, messageActionGiftPremiumData.cryptoAmount, messageActionGiftPremiumData.message)
let text: String?
let entities: [MessageTextEntity]?
switch message {
case let .textWithEntities(textWithEntitiesData):
let (textValue, entitiesValue) = (textWithEntitiesData.text, textWithEntitiesData.entities)
text = textValue
entities = messageTextEntitiesFromApiEntities(entitiesValue)
default:
text = nil
entities = nil
}
return TelegramMediaAction(action: .giftPremium(currency: currency, amount: amount, days: days, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, text: text, entities: entities))
case let .messageActionGiftStars(messageActionGiftStarsData):
let (currency, amount, stars, cryptoCurrency, cryptoAmount, transactionId) = (messageActionGiftStarsData.currency, messageActionGiftStarsData.amount, messageActionGiftStarsData.stars, messageActionGiftStarsData.cryptoCurrency, messageActionGiftStarsData.cryptoAmount, messageActionGiftStarsData.transactionId)
return TelegramMediaAction(action: .giftStars(currency: currency, amount: amount, count: stars, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, transactionId: transactionId))
case let .messageActionTopicCreate(messageActionTopicCreateData):
let (title, iconColor, iconEmojiId) = (messageActionTopicCreateData.title, messageActionTopicCreateData.iconColor, messageActionTopicCreateData.iconEmojiId)
return TelegramMediaAction(action: .topicCreated(title: title, iconColor: iconColor, iconFileId: iconEmojiId))
case let .messageActionTopicEdit(messageActionTopicEditData):
let (flags, title, iconEmojiId, closed, hidden) = (messageActionTopicEditData.flags, messageActionTopicEditData.title, messageActionTopicEditData.iconEmojiId, messageActionTopicEditData.closed, messageActionTopicEditData.hidden)
var components: [TelegramMediaActionType.ForumTopicEditComponent] = []
if let title = title {
components.append(.title(title))
}
if (flags & (1 << 1)) != 0 {
components.append(.iconFileId(iconEmojiId == 0 ? nil : iconEmojiId))
}
if let closed = closed {
components.append(.isClosed(closed == .boolTrue))
}
if let hidden = hidden {
components.append(.isHidden(hidden == .boolTrue))
}
return TelegramMediaAction(action: .topicEdited(components: components))
case let .messageActionSuggestProfilePhoto(messageActionSuggestProfilePhotoData):
let photo = messageActionSuggestProfilePhotoData.photo
return TelegramMediaAction(action: .suggestedProfilePhoto(image: telegramMediaImageFromApiPhoto(photo)))
case let .messageActionRequestedPeer(messageActionRequestedPeerData):
let (buttonId, peers) = (messageActionRequestedPeerData.buttonId, messageActionRequestedPeerData.peers)
return TelegramMediaAction(action: .requestedPeer(buttonId: buttonId, peerIds: peers.map { $0.peerId }))
case let .messageActionRequestedPeerSentMe(messageActionRequestedPeerSentMeData):
let buttonId = messageActionRequestedPeerSentMeData.buttonId
return TelegramMediaAction(action: .requestedPeer(buttonId: buttonId, peerIds: []))
case let .messageActionSetChatWallPaper(messageActionSetChatWallPaperData):
let (flags, wallpaper) = (messageActionSetChatWallPaperData.flags, messageActionSetChatWallPaperData.wallpaper)
if (flags & (1 << 0)) != 0 {
return TelegramMediaAction(action: .setSameChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper)))
} else {
return TelegramMediaAction(action: .setChatWallpaper(wallpaper: TelegramWallpaper(apiWallpaper: wallpaper), forBoth: (flags & (1 << 1)) != 0))
}
case let .messageActionGiftCode(messageActionGiftCodeData):
let (flags, boostPeer, days, slug, currency, amount, cryptoCurrency, cryptoAmount, message) = (messageActionGiftCodeData.flags, messageActionGiftCodeData.boostPeer, messageActionGiftCodeData.days, messageActionGiftCodeData.slug, messageActionGiftCodeData.currency, messageActionGiftCodeData.amount, messageActionGiftCodeData.cryptoCurrency, messageActionGiftCodeData.cryptoAmount, messageActionGiftCodeData.message)
let text: String?
let entities: [MessageTextEntity]?
switch message {
case let .textWithEntities(textWithEntitiesData):
let (textValue, entitiesValue) = (textWithEntitiesData.text, textWithEntitiesData.entities)
text = textValue
entities = messageTextEntitiesFromApiEntities(entitiesValue)
default:
text = nil
entities = nil
}
return TelegramMediaAction(action: .giftCode(slug: slug, fromGiveaway: (flags & (1 << 0)) != 0, isUnclaimed: (flags & (1 << 5)) != 0, boostPeerId: boostPeer?.peerId, months: days, currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, text: text, entities: entities))
case let .messageActionGiveawayLaunch(messageActionGiveawayLaunchData):
return TelegramMediaAction(action: .giveawayLaunched(stars: messageActionGiveawayLaunchData.stars))
case let .messageActionGiveawayResults(messageActionGiveawayResultsData):
let (flags, winners, unclaimed) = (messageActionGiveawayResultsData.flags, messageActionGiveawayResultsData.winnersCount, messageActionGiveawayResultsData.unclaimedCount)
return TelegramMediaAction(action: .giveawayResults(winners: winners, unclaimed: unclaimed, stars: (flags & (1 << 0)) != 0))
case let .messageActionBoostApply(messageActionBoostApplyData):
let boosts = messageActionBoostApplyData.boosts
return TelegramMediaAction(action: .boostsApplied(boosts: boosts))
case let .messageActionPaymentRefunded(messageActionPaymentRefundedData):
let (peer, currency, totalAmount, payload, charge) = (messageActionPaymentRefundedData.peer, messageActionPaymentRefundedData.currency, messageActionPaymentRefundedData.totalAmount, messageActionPaymentRefundedData.payload, messageActionPaymentRefundedData.charge)
let transactionId: String
switch charge {
case let .paymentCharge(paymentChargeData):
let id = paymentChargeData.id
transactionId = id
}
return TelegramMediaAction(action: .paymentRefunded(peerId: peer.peerId, currency: currency, totalAmount: totalAmount, payload: payload?.makeData(), transactionId: transactionId))
case let .messageActionPrizeStars(messageActionPrizeStarsData):
let (flags, stars, transactionId, boostPeer, giveawayMsgId) = (messageActionPrizeStarsData.flags, messageActionPrizeStarsData.stars, messageActionPrizeStarsData.transactionId, messageActionPrizeStarsData.boostPeer, messageActionPrizeStarsData.giveawayMsgId)
return TelegramMediaAction(action: .prizeStars(amount: stars, isUnclaimed: (flags & (1 << 2)) != 0, boostPeerId: boostPeer.peerId, transactionId: transactionId, giveawayMessageId: MessageId(peerId: boostPeer.peerId, namespace: Namespaces.Message.Cloud, id: giveawayMsgId)))
case let .messageActionStarGift(messageActionStarGiftData):
let (flags, apiGift, message, convertStars, upgradeMessageId, upgradeStars, fromId, peer, savedId, prepaidUpgradeHash, giftMessageId, toId, number) = (messageActionStarGiftData.flags, messageActionStarGiftData.gift, messageActionStarGiftData.message, messageActionStarGiftData.convertStars, messageActionStarGiftData.upgradeMsgId, messageActionStarGiftData.upgradeStars, messageActionStarGiftData.fromId, messageActionStarGiftData.peer, messageActionStarGiftData.savedId, messageActionStarGiftData.prepaidUpgradeHash, messageActionStarGiftData.giftMsgId, messageActionStarGiftData.toId, messageActionStarGiftData.giftNum)
let text: String?
let entities: [MessageTextEntity]?
switch message {
case let .textWithEntities(textWithEntitiesData):
let (textValue, entitiesValue) = (textWithEntitiesData.text, textWithEntitiesData.entities)
text = textValue
entities = messageTextEntitiesFromApiEntities(entitiesValue)
default:
text = nil
entities = nil
}
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
return TelegramMediaAction(action: .starGift(gift: gift, convertStars: convertStars, text: text, entities: entities, nameHidden: (flags & (1 << 0)) != 0, savedToProfile: (flags & (1 << 2)) != 0, converted: (flags & (1 << 3)) != 0, upgraded: (flags & (1 << 5)) != 0, canUpgrade: (flags & (1 << 10)) != 0, upgradeStars: upgradeStars, isRefunded: (flags & (1 << 9)) != 0, isPrepaidUpgrade: (flags & (1 << 13)) != 0, upgradeMessageId: upgradeMessageId, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, prepaidUpgradeHash: prepaidUpgradeHash, giftMessageId: giftMessageId, upgradeSeparate: (flags & (1 << 16)) != 0, isAuctionAcquired: (flags & (1 << 17)) != 0, toPeerId: toId?.peerId, number: number))
case let .messageActionStarGiftUnique(messageActionStarGiftUniqueData):
let (flags, apiGift, canExportAt, transferStars, fromId, peer, savedId, resaleAmount, canTransferDate, canResaleDate, dropOriginalDetailsStars, canCraftAt) = (messageActionStarGiftUniqueData.flags, messageActionStarGiftUniqueData.gift, messageActionStarGiftUniqueData.canExportAt, messageActionStarGiftUniqueData.transferStars, messageActionStarGiftUniqueData.fromId, messageActionStarGiftUniqueData.peer, messageActionStarGiftUniqueData.savedId, messageActionStarGiftUniqueData.resaleAmount, messageActionStarGiftUniqueData.canTransferAt, messageActionStarGiftUniqueData.canResellAt, messageActionStarGiftUniqueData.dropOriginalDetailsStars, messageActionStarGiftUniqueData.canCraftAt)
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
return TelegramMediaAction(action: .starGiftUnique(gift: gift, isUpgrade: (flags & (1 << 0)) != 0, isTransferred: (flags & (1 << 1)) != 0, savedToProfile: (flags & (1 << 2)) != 0, canExportDate: canExportAt, transferStars: transferStars, isRefunded: (flags & (1 << 5)) != 0, isPrepaidUpgrade: (flags & (1 << 11)) != 0, peerId: peer?.peerId, senderId: fromId?.peerId, savedId: savedId, resaleAmount: resaleAmount.flatMap { CurrencyAmount(apiAmount: $0) }, canTransferDate: canTransferDate, canResaleDate: canResaleDate, dropOriginalDetailsStars: dropOriginalDetailsStars, assigned: (flags & (1 << 13)) != 0, fromOffer: (flags & (1 << 14)) != 0, canCraftAt: canCraftAt, isCrafted: (flags & (1 << 16)) != 0))
case let .messageActionPaidMessagesRefunded(messageActionPaidMessagesRefundedData):
let (count, stars) = (messageActionPaidMessagesRefundedData.count, messageActionPaidMessagesRefundedData.stars)
return TelegramMediaAction(action: .paidMessagesRefunded(count: count, stars: stars))
case let .messageActionPaidMessagesPrice(messageActionPaidMessagesPriceData):
let (flags, stars) = (messageActionPaidMessagesPriceData.flags, messageActionPaidMessagesPriceData.stars)
let broadcastMessagesAllowed = (flags & (1 << 0)) != 0
return TelegramMediaAction(action: .paidMessagesPriceEdited(stars: stars, broadcastMessagesAllowed: broadcastMessagesAllowed))
case let .messageActionConferenceCall(messageActionConferenceCallData):
let (flags, callId, duration, otherParticipants) = (messageActionConferenceCallData.flags, messageActionConferenceCallData.callId, messageActionConferenceCallData.duration, messageActionConferenceCallData.otherParticipants)
let isMissed = (flags & (1 << 0)) != 0
let isActive = (flags & (1 << 1)) != 0
let isVideo = (flags & (1 << 4)) != 0
var mappedFlags = TelegramMediaActionType.ConferenceCall.Flags()
if isMissed {
mappedFlags.insert(.isMissed)
}
if isActive {
mappedFlags.insert(.isActive)
}
if isVideo {
mappedFlags.insert(.isVideo)
}
return TelegramMediaAction(action: .conferenceCall(TelegramMediaActionType.ConferenceCall(
callId: callId,
duration: duration,
flags: mappedFlags,
otherParticipants: otherParticipants.flatMap({ return $0.map(\.peerId) }) ?? []
)))
case let .messageActionTodoCompletions(messageActionTodoCompletionsData):
let (completed, incompleted) = (messageActionTodoCompletionsData.completed, messageActionTodoCompletionsData.incompleted)
return TelegramMediaAction(action: .todoCompletions(completed: completed, incompleted: incompleted))
case let .messageActionTodoAppendTasks(messageActionTodoAppendTasksData):
let list = messageActionTodoAppendTasksData.list
return TelegramMediaAction(action: .todoAppendTasks(list.map { TelegramMediaTodo.Item(apiItem: $0) }))
case let .messageActionSuggestedPostApproval(messageActionSuggestedPostApprovalData):
let (flags, rejectComment, scheduleDate, starsAmount) = (messageActionSuggestedPostApprovalData.flags, messageActionSuggestedPostApprovalData.rejectComment, messageActionSuggestedPostApprovalData.scheduleDate, messageActionSuggestedPostApprovalData.price)
let status: TelegramMediaActionType.SuggestedPostApprovalStatus
if (flags & (1 << 0)) != 0 {
let reason: TelegramMediaActionType.SuggestedPostApprovalStatus.RejectionReason
if (flags & (1 << 1)) != 0 {
let balanceNeeded: CurrencyAmount
switch starsAmount {
case .none:
balanceNeeded = CurrencyAmount(amount: .zero, currency: .stars)
case let .starsAmount(starsAmountData):
let (amount, nanos) = (starsAmountData.amount, starsAmountData.nanos)
balanceNeeded = CurrencyAmount(amount: StarsAmount(value: amount, nanos: nanos), currency: .stars)
case let .starsTonAmount(starsTonAmountData):
let amount = starsTonAmountData.amount
balanceNeeded = CurrencyAmount(amount: StarsAmount(value: amount, nanos: 0), currency: .ton)
}
reason = .lowBalance(balanceNeeded: balanceNeeded)
} else {
reason = .generic
}
status = .rejected(reason: reason, comment: rejectComment)
} else if (flags & (1 << 1)) != 0 {
let amountValue: CurrencyAmount
switch starsAmount {
case .none:
amountValue = CurrencyAmount(amount: .zero, currency: .stars)
case let .starsAmount(starsAmountData):
let (amount, nanos) = (starsAmountData.amount, starsAmountData.nanos)
amountValue = CurrencyAmount(amount: StarsAmount(value: amount, nanos: nanos), currency: .stars)
case let .starsTonAmount(starsTonAmountData):
let amount = starsTonAmountData.amount
amountValue = CurrencyAmount(amount: StarsAmount(value: amount, nanos: 0), currency: .ton)
}
status = .rejected(reason: .lowBalance(balanceNeeded: amountValue), comment: nil)
} else {
status = .approved(timestamp: scheduleDate, amount: starsAmount.flatMap(CurrencyAmount.init(apiAmount:)))
}
return TelegramMediaAction(action: .suggestedPostApprovalStatus(status: status))
case let .messageActionGiftTon(messageActionGiftTonData):
let (currency, amount, cryptoCurrency, cryptoAmount, transactionId) = (messageActionGiftTonData.currency, messageActionGiftTonData.amount, messageActionGiftTonData.cryptoCurrency, messageActionGiftTonData.cryptoAmount, messageActionGiftTonData.transactionId)
return TelegramMediaAction(action: .giftTon(currency: currency, amount: amount, cryptoCurrency: cryptoCurrency, cryptoAmount: cryptoAmount, transactionId: transactionId))
case let .messageActionSuggestedPostSuccess(messageActionSuggestedPostSuccessData):
let price = messageActionSuggestedPostSuccessData.price
return TelegramMediaAction(action: .suggestedPostSuccess(amount: CurrencyAmount(apiAmount: price)))
case let .messageActionSuggestedPostRefund(messageActionSuggestedPostRefundData):
let flags = messageActionSuggestedPostRefundData.flags
return TelegramMediaAction(action: .suggestedPostRefund(TelegramMediaActionType.SuggestedPostRefund(isUserInitiated: (flags & (1 << 0)) != 0)))
case let .messageActionSuggestBirthday(messageActionSuggestBirthdayData):
let birthday = messageActionSuggestBirthdayData.birthday
return TelegramMediaAction(action: .suggestedBirthday(TelegramBirthday(apiBirthday: birthday)))
case let .messageActionStarGiftPurchaseOffer(messageActionStarGiftPurchaseOfferData):
let (flags, apiGift, price, expiresAt) = (messageActionStarGiftPurchaseOfferData.flags, messageActionStarGiftPurchaseOfferData.gift, messageActionStarGiftPurchaseOfferData.price, messageActionStarGiftPurchaseOfferData.expiresAt)
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
return TelegramMediaAction(action: .starGiftPurchaseOffer(gift: gift, amount: CurrencyAmount(apiAmount: price), expireDate: expiresAt, isAccepted: (flags & (1 << 0)) != 0, isDeclined: (flags & (1 << 1)) != 0))
case let .messageActionStarGiftPurchaseOfferDeclined(messageActionStarGiftPurchaseOfferDeclinedData):
let (flags, apiGift, price) = (messageActionStarGiftPurchaseOfferDeclinedData.flags, messageActionStarGiftPurchaseOfferDeclinedData.gift, messageActionStarGiftPurchaseOfferDeclinedData.price)
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
return TelegramMediaAction(action: .starGiftPurchaseOfferDeclined(gift: gift, amount: CurrencyAmount(apiAmount: price), hasExpired: (flags & (1 << 0)) != 0))
case let .messageActionNewCreatorPending(messageActionNewCreatorPendingData):
return TelegramMediaAction(action: .groupCreatorChange(TelegramMediaActionType.GroupCreatorChange(
kind: .pending,
targetPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(messageActionNewCreatorPendingData.newCreatorId))
)))
case let .messageActionChangeCreator(messageActionChangeCreatorData):
return TelegramMediaAction(action: .groupCreatorChange(TelegramMediaActionType.GroupCreatorChange(
kind: .applied,
targetPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(messageActionChangeCreatorData.newCreatorId))
)))
case let .messageActionNoForwardsToggle(messageActionNoForwardsToggleData):
return TelegramMediaAction(action: .copyProtectionToggle(previousValue: messageActionNoForwardsToggleData.prevValue == .boolTrue, newValue: messageActionNoForwardsToggleData.newValue == .boolTrue))
case let .messageActionNoForwardsRequest(messageActionNoForwardsRequestData):
return TelegramMediaAction(action: .copyProtectionRequest(hasExpired: (messageActionNoForwardsRequestData.flags & (1 << 0)) != 0, previousValue: messageActionNoForwardsRequestData.prevValue == .boolTrue, newValue: messageActionNoForwardsRequestData.newValue == .boolTrue))
}
}
extension PhoneCallDiscardReason {
init(apiReason: Api.PhoneCallDiscardReason) {
switch apiReason {
case .phoneCallDiscardReasonBusy:
self = .busy
case .phoneCallDiscardReasonDisconnect:
self = .disconnect
case .phoneCallDiscardReasonHangup:
self = .hangup
case .phoneCallDiscardReasonMissed:
self = .missed
case .phoneCallDiscardReasonMigrateConferenceCall:
self = .hangup
}
}
}
extension SentSecureValueType {
init(apiType: Api.SecureValueType) {
switch apiType {
case .secureValueTypePersonalDetails:
self = .personalDetails
case .secureValueTypePassport:
self = .passport
case .secureValueTypeDriverLicense:
self = .driversLicense
case .secureValueTypeIdentityCard:
self = .idCard
case .secureValueTypeAddress:
self = .address
case .secureValueTypeBankStatement:
self = .bankStatement
case .secureValueTypeUtilityBill:
self = .utilityBill
case .secureValueTypeRentalAgreement:
self = .rentalAgreement
case .secureValueTypePhone:
self = .phone
case .secureValueTypeEmail:
self = .email
case .secureValueTypeInternalPassport:
self = .internalPassport
case .secureValueTypePassportRegistration:
self = .passportRegistration
case .secureValueTypeTemporaryRegistration:
self = .temporaryRegistration
}
}
}
@@ -0,0 +1,248 @@
import Foundation
import Postbox
import TelegramApi
func dimensionsForFileAttributes(_ attributes: [TelegramMediaFileAttribute]) -> PixelDimensions? {
for attribute in attributes {
switch attribute {
case let .Video(_, size, _, _, _, _):
return size
case let .ImageSize(size):
return size
default:
break
}
}
return nil
}
func durationForFileAttributes(_ attributes: [TelegramMediaFileAttribute]) -> Double? {
for attribute in attributes {
switch attribute {
case let .Video(duration, _, _, _, _, _):
return duration
case let .Audio(_, duration, _, _, _):
return Double(duration)
default:
break
}
}
return nil
}
public extension TelegramMediaFile {
var dimensions: PixelDimensions? {
if let value = dimensionsForFileAttributes(self.attributes) {
return value
} else if self.isAnimatedSticker {
return PixelDimensions(width: 512, height: 512)
} else {
return nil
}
}
var duration: Double? {
return durationForFileAttributes(self.attributes)
}
}
public extension TelegramMediaFile {
func isValidForDisplay(chatPeerId: PeerId) -> Bool {
if chatPeerId.namespace == Namespaces.Peer.SecretChat {
if self.isAnimatedSticker {
if !self.attributes.contains(where: { attribute in
if case .hintIsValidated = attribute {
return true
}
return false
}) {
return false
}
}
}
return true
}
}
extension StickerPackReference {
init?(apiInputSet: Api.InputStickerSet) {
switch apiInputSet {
case .inputStickerSetEmpty:
return nil
case let .inputStickerSetID(inputStickerSetIDData):
let (id, accessHash) = (inputStickerSetIDData.id, inputStickerSetIDData.accessHash)
self = .id(id: id, accessHash: accessHash)
case let .inputStickerSetShortName(inputStickerSetShortNameData):
let shortName = inputStickerSetShortNameData.shortName
self = .name(shortName)
case .inputStickerSetAnimatedEmoji:
self = .animatedEmoji
case let .inputStickerSetDice(inputStickerSetDiceData):
let emoticon = inputStickerSetDiceData.emoticon
self = .dice(emoticon)
case .inputStickerSetAnimatedEmojiAnimations:
self = .animatedEmojiAnimations
case .inputStickerSetPremiumGifts:
self = .premiumGifts
case .inputStickerSetEmojiGenericAnimations:
self = .emojiGenericAnimations
case .inputStickerSetEmojiDefaultStatuses:
self = .iconStatusEmoji
case .inputStickerSetEmojiChannelDefaultStatuses:
self = .iconChannelStatusEmoji
case .inputStickerSetEmojiDefaultTopicIcons:
self = .iconTopicEmoji
case .inputStickerSetTonGifts:
self = .tonGifts
}
}
}
extension StickerMaskCoords {
init(apiMaskCoords: Api.MaskCoords) {
switch apiMaskCoords {
case let .maskCoords(maskCoordsData):
let (n, x, y, zoom) = (maskCoordsData.n, maskCoordsData.x, maskCoordsData.y, maskCoordsData.zoom)
self.init(n: n, x: x, y: y, zoom: zoom)
}
}
}
func telegramMediaFileAttributesFromApiAttributes(_ attributes: [Api.DocumentAttribute], isLivePhoto: Bool = false) -> [TelegramMediaFileAttribute] {
var result: [TelegramMediaFileAttribute] = []
for attribute in attributes {
switch attribute {
case let .documentAttributeFilename(documentAttributeFilenameData):
let fileName = documentAttributeFilenameData.fileName
result.append(.FileName(fileName: fileName))
case let .documentAttributeSticker(documentAttributeStickerData):
let (alt, stickerSet, maskCoords) = (documentAttributeStickerData.alt, documentAttributeStickerData.stickerset, documentAttributeStickerData.maskCoords)
result.append(.Sticker(displayText: alt, packReference: StickerPackReference(apiInputSet: stickerSet), maskData: maskCoords.flatMap(StickerMaskCoords.init)))
case .documentAttributeHasStickers:
result.append(.HasLinkedStickers)
case let .documentAttributeImageSize(documentAttributeImageSizeData):
let (w, h) = (documentAttributeImageSizeData.w, documentAttributeImageSizeData.h)
result.append(.ImageSize(size: PixelDimensions(width: w, height: h)))
case .documentAttributeAnimated:
result.append(.Animated)
case let .documentAttributeVideo(documentAttributeVideoData):
let (flags, duration, w, h, preloadSize, videoStart, videoCodec) = (documentAttributeVideoData.flags, documentAttributeVideoData.duration, documentAttributeVideoData.w, documentAttributeVideoData.h, documentAttributeVideoData.preloadPrefixSize, documentAttributeVideoData.videoStartTs, documentAttributeVideoData.videoCodec)
var videoFlags = TelegramMediaVideoFlags()
if (flags & (1 << 0)) != 0 {
videoFlags.insert(.instantRoundVideo)
}
if (flags & (1 << 1)) != 0 {
videoFlags.insert(.supportsStreaming)
}
if (flags & (1 << 3)) != 0 {
videoFlags.insert(.isSilent)
}
if isLivePhoto {
videoFlags.insert(.isLivePhoto)
}
result.append(.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: preloadSize, coverTime: videoStart, videoCodec: videoCodec))
case let .documentAttributeAudio(documentAttributeAudioData):
let (flags, duration, title, performer, waveform) = (documentAttributeAudioData.flags, documentAttributeAudioData.duration, documentAttributeAudioData.title, documentAttributeAudioData.performer, documentAttributeAudioData.waveform)
let isVoice = (flags & (1 << 10)) != 0
let waveformBuffer: Data? = waveform?.makeData()
result.append(.Audio(isVoice: isVoice, duration: Int(duration), title: title, performer: performer, waveform: waveformBuffer))
case let .documentAttributeCustomEmoji(documentAttributeCustomEmojiData):
let (flags, alt, stickerSet) = (documentAttributeCustomEmojiData.flags, documentAttributeCustomEmojiData.alt, documentAttributeCustomEmojiData.stickerset)
let isFree = (flags & (1 << 0)) != 0
let isSingleColor = (flags & (1 << 1)) != 0
result.append(.CustomEmoji(isPremium: !isFree, isSingleColor: isSingleColor, alt: alt, packReference: StickerPackReference(apiInputSet: stickerSet)))
}
}
return result
}
public func fileNameFromFileAttributes(_ attributes: [TelegramMediaFileAttribute]) -> String? {
for attribute in attributes {
if case let .FileName(value) = attribute {
return value
}
}
return nil
}
func telegramMediaFileThumbnailRepresentationsFromApiSizes(datacenterId: Int32, documentId: Int64, accessHash: Int64, fileReference: Data?, sizes: [Api.PhotoSize]) -> (immediateThumbnail: Data?, representations: [TelegramMediaImageRepresentation]) {
var immediateThumbnailData: Data?
var representations: [TelegramMediaImageRepresentation] = []
for size in sizes {
switch size {
case let .photoCachedSize(photoCachedSizeData):
let (type, w, h, _) = (photoCachedSizeData.type, photoCachedSizeData.w, photoCachedSizeData.h, photoCachedSizeData.bytes)
let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, fileReference: fileReference)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
case let .photoSize(photoSizeData):
let (type, w, h, _) = (photoSizeData.type, photoSizeData.w, photoSizeData.h, photoSizeData.size)
let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, fileReference: fileReference)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
case let .photoSizeProgressive(photoSizeProgressiveData):
let (type, w, h, sizes) = (photoSizeProgressiveData.type, photoSizeProgressiveData.w, photoSizeProgressiveData.h, photoSizeProgressiveData.sizes)
let resource = CloudDocumentSizeMediaResource(datacenterId: datacenterId, documentId: documentId, accessHash: accessHash, sizeSpec: type, fileReference: fileReference)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes, immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
case let .photoPathSize(photoPathSizeData):
let (_, data) = (photoPathSizeData.type, photoPathSizeData.bytes)
immediateThumbnailData = data.makeData()
case let .photoStrippedSize(photoStrippedSizeData):
let (_, data) = (photoStrippedSizeData.type, photoStrippedSizeData.bytes)
immediateThumbnailData = data.makeData()
case .photoSizeEmpty:
break
}
}
return (immediateThumbnailData, representations)
}
func telegramMediaFileFromApiDocument(_ document: Api.Document, altDocuments: [Api.Document]?, videoCover: Api.Photo? = nil, isLivePhoto: Bool = false) -> TelegramMediaFile? {
switch document {
case let .document(documentData):
let (id, accessHash, fileReference, mimeType, size, thumbs, videoThumbs, dcId, attributes) = (documentData.id, documentData.accessHash, documentData.fileReference, documentData.mimeType, documentData.size, documentData.thumbs, documentData.videoThumbs, documentData.dcId, documentData.attributes)
var parsedAttributes = telegramMediaFileAttributesFromApiAttributes(attributes, isLivePhoto: isLivePhoto)
var isSticker = false
var isAnimated = false
for attribute in parsedAttributes {
switch attribute {
case .Sticker:
isSticker = true
case .Animated:
isAnimated = true
default:
break
}
}
if isSticker && isAnimated {
parsedAttributes.append(.hintIsValidated)
}
let (immediateThumbnail, previewRepresentations) = telegramMediaFileThumbnailRepresentationsFromApiSizes(datacenterId: dcId, documentId: id, accessHash: accessHash, fileReference: fileReference.makeData(), sizes: thumbs ?? [])
var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = []
if let videoThumbs = videoThumbs {
for thumb in videoThumbs {
switch thumb {
case let .videoSize(videoSizeData):
let (_, type, w, h, _, _) = (videoSizeData.flags, videoSizeData.type, videoSizeData.w, videoSizeData.h, videoSizeData.size, videoSizeData.videoStartTs)
let resource: TelegramMediaResource
resource = CloudDocumentSizeMediaResource(datacenterId: dcId, documentId: id, accessHash: accessHash, sizeSpec: type, fileReference: fileReference.makeData())
videoThumbnails.append(TelegramMediaFile.VideoThumbnail(
dimensions: PixelDimensions(width: w, height: h),
resource: resource))
case .videoSizeEmojiMarkup, .videoSizeStickerMarkup:
break
}
}
}
var alternativeRepresentations: [TelegramMediaFile] = []
if let altDocuments {
alternativeRepresentations = altDocuments.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }
}
return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: size, fileReference: fileReference.makeData(), fileName: fileNameFromFileAttributes(parsedAttributes)), previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, videoCover: videoCover.flatMap(telegramMediaImageFromApiPhoto), immediateThumbnailData: immediateThumbnail, mimeType: mimeType, size: size, attributes: parsedAttributes, alternativeRepresentations: alternativeRepresentations)
case .documentEmpty:
return nil
}
}
@@ -0,0 +1,18 @@
import Foundation
import Postbox
import TelegramApi
extension TelegramMediaGame {
convenience init(apiGame: Api.Game) {
switch apiGame {
case let .game(gameData):
let (id, accessHash, shortName, title, description, photo, document) = (gameData.id, gameData.accessHash, gameData.shortName, gameData.title, gameData.description, gameData.photo, gameData.document)
var file: TelegramMediaFile?
if let document = document {
file = telegramMediaFileFromApiDocument(document, altDocuments: [])
}
self.init(gameId: id, accessHash: accessHash, name: shortName, title: title, description: description, image: telegramMediaImageFromApiPhoto(photo), file: file)
}
}
}
@@ -0,0 +1,75 @@
import Foundation
import Postbox
import TelegramApi
func telegramMediaImageRepresentationsFromApiSizes(datacenterId: Int32, photoId: Int64, accessHash: Int64, fileReference: Data?, sizes: [Api.PhotoSize]) -> (immediateThumbnail: Data?, representations: [TelegramMediaImageRepresentation]) {
var immediateThumbnailData: Data?
var representations: [TelegramMediaImageRepresentation] = []
for size in sizes {
switch size {
case let .photoCachedSize(photoCachedSizeData):
let (type, w, h) = (photoCachedSizeData.type, photoCachedSizeData.w, photoCachedSizeData.h)
let resource = CloudPhotoSizeMediaResource(datacenterId: datacenterId, photoId: photoId, accessHash: accessHash, sizeSpec: type, size: nil, fileReference: fileReference)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
case let .photoSize(photoSizeData):
let (type, w, h, size) = (photoSizeData.type, photoSizeData.w, photoSizeData.h, photoSizeData.size)
let resource = CloudPhotoSizeMediaResource(datacenterId: datacenterId, photoId: photoId, accessHash: accessHash, sizeSpec: type, size: Int64(size), fileReference: fileReference)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
case let .photoSizeProgressive(photoSizeProgressiveData):
let (type, w, h, sizes) = (photoSizeProgressiveData.type, photoSizeProgressiveData.w, photoSizeProgressiveData.h, photoSizeProgressiveData.sizes)
if !sizes.isEmpty {
let resource = CloudPhotoSizeMediaResource(datacenterId: datacenterId, photoId: photoId, accessHash: accessHash, sizeSpec: type, size: Int64(sizes[sizes.count - 1]), fileReference: fileReference)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: sizes, immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
}
case let .photoStrippedSize(photoStrippedSizeData):
let data = photoStrippedSizeData.bytes
immediateThumbnailData = data.makeData()
case .photoPathSize:
break
case .photoSizeEmpty:
break
}
}
return (immediateThumbnailData, representations)
}
func telegramMediaImageFromApiPhoto(_ photo: Api.Photo) -> TelegramMediaImage? {
switch photo {
case let .photo(photoData):
let (flags, id, accessHash, fileReference, sizes, videoSizes, dcId) = (photoData.flags, photoData.id, photoData.accessHash, photoData.fileReference, photoData.sizes, photoData.videoSizes, photoData.dcId)
let (immediateThumbnailData, representations) = telegramMediaImageRepresentationsFromApiSizes(datacenterId: dcId, photoId: id, accessHash: accessHash, fileReference: fileReference.makeData(), sizes: sizes)
var imageFlags: TelegramMediaImageFlags = []
let hasStickers = (flags & (1 << 0)) != 0
if hasStickers {
imageFlags.insert(.hasStickers)
}
var videoRepresentations: [TelegramMediaImage.VideoRepresentation] = []
var emojiMarkup: TelegramMediaImage.EmojiMarkup?
if let videoSizes = videoSizes {
for size in videoSizes {
switch size {
case let .videoSize(videoSizeData):
let (_, type, w, h, size, videoStartTs) = (videoSizeData.flags, videoSizeData.type, videoSizeData.w, videoSizeData.h, videoSizeData.size, videoSizeData.videoStartTs)
let resource: TelegramMediaResource
resource = CloudPhotoSizeMediaResource(datacenterId: dcId, photoId: id, accessHash: accessHash, sizeSpec: type, size: Int64(size), fileReference: fileReference.makeData())
videoRepresentations.append(TelegramMediaImage.VideoRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, startTimestamp: videoStartTs))
case let .videoSizeEmojiMarkup(videoSizeEmojiMarkupData):
let (fileId, backgroundColors) = (videoSizeEmojiMarkupData.emojiId, videoSizeEmojiMarkupData.backgroundColors)
emojiMarkup = TelegramMediaImage.EmojiMarkup(content: .emoji(fileId: fileId), backgroundColors: backgroundColors)
case let .videoSizeStickerMarkup(videoSizeStickerMarkupData):
let (stickerSet, fileId, backgroundColors) = (videoSizeStickerMarkupData.stickerset, videoSizeStickerMarkupData.stickerId, videoSizeStickerMarkupData.backgroundColors)
if let packReference = StickerPackReference(apiInputSet: stickerSet) {
emojiMarkup = TelegramMediaImage.EmojiMarkup(content: .sticker(packReference: packReference, fileId: fileId), backgroundColors: backgroundColors)
}
}
}
}
return TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudImage, id: id), representations: representations, videoRepresentations: videoRepresentations, immediateThumbnailData: immediateThumbnailData, emojiMarkup: emojiMarkup, reference: .cloud(imageId: id, accessHash: accessHash, fileReference: fileReference.makeData()), partialReference: nil, flags: imageFlags)
case .photoEmpty:
return nil
}
}
@@ -0,0 +1,27 @@
import Foundation
import Postbox
import TelegramApi
func telegramMediaMapFromApiGeoPoint(_ geo: Api.GeoPoint, title: String?, address: String?, provider: String?, venueId: String?, venueType: String?, liveBroadcastingTimeout: Int32?, liveProximityNotificationRadius: Int32?, heading: Int32?) -> TelegramMediaMap {
var venue: MapVenue?
if let title = title {
venue = MapVenue(title: title, address: address, provider: provider, id: venueId, type: venueType)
}
switch geo {
case let .geoPoint(geoPointData):
let (_, long, lat, _, accuracyRadius) = (geoPointData.flags, geoPointData.long, geoPointData.lat, geoPointData.accessHash, geoPointData.accuracyRadius)
return TelegramMediaMap(latitude: lat, longitude: long, heading: heading, accuracyRadius: accuracyRadius.flatMap { Double($0) }, venue: venue, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: liveProximityNotificationRadius)
case .geoPointEmpty:
return TelegramMediaMap(latitude: 0.0, longitude: 0.0, heading: nil, accuracyRadius: nil, venue: venue, liveBroadcastingTimeout: liveBroadcastingTimeout, liveProximityNotificationRadius: liveProximityNotificationRadius)
}
}
func mapGeoAddressFromApiGeoPointAddress(_ geo: Api.GeoPointAddress) -> MapGeoAddress {
switch geo {
case let .geoPointAddress(geoPointAddressData):
let (countryIso2, state, city, street) = (geoPointAddressData.countryIso2, geoPointAddressData.state, geoPointAddressData.city, geoPointAddressData.street)
return MapGeoAddress(country: countryIso2, state: state, city: city, street: street)
}
}
@@ -0,0 +1,54 @@
import Foundation
import Postbox
import TelegramApi
extension TelegramMediaPollOption {
init(apiOption: Api.PollAnswer) {
switch apiOption {
case let .pollAnswer(pollAnswerData):
let (text, option) = (pollAnswerData.text, pollAnswerData.option)
let answerText: String
let answerEntities: [MessageTextEntity]
switch text {
case let .textWithEntities(textWithEntitiesData):
let (text, entities) = (textWithEntitiesData.text, textWithEntitiesData.entities)
answerText = text
answerEntities = messageTextEntitiesFromApiEntities(entities)
}
self.init(text: answerText, entities: answerEntities, opaqueIdentifier: option.makeData())
}
}
var apiOption: Api.PollAnswer {
return .pollAnswer(.init(text: .textWithEntities(.init(text: self.text, entities: apiEntitiesFromMessageTextEntities(self.entities, associatedPeers: SimpleDictionary()))), option: Buffer(data: self.opaqueIdentifier)))
}
}
extension TelegramMediaPollOptionVoters {
init(apiVoters: Api.PollAnswerVoters) {
switch apiVoters {
case let .pollAnswerVoters(pollAnswerVotersData):
let (flags, option, voters) = (pollAnswerVotersData.flags, pollAnswerVotersData.option, pollAnswerVotersData.voters)
self.init(selected: (flags & (1 << 0)) != 0, opaqueIdentifier: option.makeData(), count: voters, isCorrect: (flags & (1 << 1)) != 0)
}
}
}
extension TelegramMediaPollResults {
init(apiResults: Api.PollResults) {
switch apiResults {
case let .pollResults(pollResultsData):
let (results, totalVoters, recentVoters, solution, solutionEntities) = (pollResultsData.results, pollResultsData.totalVoters, pollResultsData.recentVoters, pollResultsData.solution, pollResultsData.solutionEntities)
var parsedSolution: TelegramMediaPollResults.Solution?
if let solution = solution, let solutionEntities = solutionEntities, !solution.isEmpty {
parsedSolution = TelegramMediaPollResults.Solution(text: solution, entities: messageTextEntitiesFromApiEntities(solutionEntities))
}
self.init(voters: results.flatMap({ $0.map(TelegramMediaPollOptionVoters.init(apiVoters:)) }), totalVoters: totalVoters, recentVoters: recentVoters.flatMap { recentVoters in
return recentVoters.map { $0.peerId }
} ?? [], solution: parsedSolution)
}
}
}
@@ -0,0 +1,36 @@
import Foundation
import Postbox
import TelegramApi
extension TelegramMediaTodo.Item {
init(apiItem: Api.TodoItem) {
switch apiItem {
case let .todoItem(todoItemData):
let (id, title) = (todoItemData.id, todoItemData.title)
let itemText: String
let itemEntities: [MessageTextEntity]
switch title {
case let .textWithEntities(textWithEntitiesData):
let (text, entities) = (textWithEntitiesData.text, textWithEntitiesData.entities)
itemText = text
itemEntities = messageTextEntitiesFromApiEntities(entities)
}
self.init(text: itemText, entities: itemEntities, id: id)
}
}
var apiItem: Api.TodoItem {
return .todoItem(.init(id: self.id, title: .textWithEntities(.init(text: self.text, entities: apiEntitiesFromMessageTextEntities(self.entities, associatedPeers: SimpleDictionary())))))
}
}
extension TelegramMediaTodo.Completion {
init(apiCompletion: Api.TodoCompletion) {
switch apiCompletion {
case let .todoCompletion(todoCompletionData):
let (id, completedBy, date) = (todoCompletionData.id, todoCompletionData.completedBy, todoCompletionData.date)
self.init(id: id, date: date, completedBy: completedBy.peerId)
}
}
}
@@ -0,0 +1,17 @@
import Foundation
import Postbox
import TelegramApi
extension TelegramMediaWebFile {
convenience init(_ document: Api.WebDocument) {
switch document {
case let .webDocument(webDocumentData):
let (url, accessHash, size, mimeType, attributes) = (webDocumentData.url, webDocumentData.accessHash, webDocumentData.size, webDocumentData.mimeType, webDocumentData.attributes)
self.init(resource: WebFileReferenceMediaResource(url: url, size: Int64(size), accessHash: accessHash), mimeType: mimeType, size: size, attributes: telegramMediaFileAttributesFromApiAttributes(attributes))
case let .webDocumentNoProxy(webDocumentNoProxyData):
let (url, size, mimeType, attributes) = (webDocumentNoProxyData.url, webDocumentNoProxyData.size, webDocumentNoProxyData.mimeType, webDocumentNoProxyData.attributes)
self.init(resource: HttpReferenceMediaResource(url: url, size: Int64(size)), mimeType: mimeType, size: size, attributes: telegramMediaFileAttributesFromApiAttributes(attributes))
}
}
}
@@ -0,0 +1,151 @@
import Foundation
import Postbox
import TelegramApi
func telegramMediaWebpageAttributeFromApiWebpageAttribute(_ attribute: Api.WebPageAttribute) -> TelegramMediaWebpageAttribute? {
switch attribute {
case let .webPageAttributeTheme(webPageAttributeThemeData):
let (_, documents, settings) = (webPageAttributeThemeData.flags, webPageAttributeThemeData.documents, webPageAttributeThemeData.settings)
var files: [TelegramMediaFile] = []
if let documents = documents {
files = documents.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }
}
return .theme(TelegraMediaWebpageThemeAttribute(files: files, settings: settings.flatMap { TelegramThemeSettings(apiThemeSettings: $0) }))
case let .webPageAttributeStickerSet(webPageAttributeStickerSetData):
let (apiFlags, stickers) = (webPageAttributeStickerSetData.flags, webPageAttributeStickerSetData.stickers)
var flags = TelegramMediaWebpageStickerPackAttribute.Flags()
if (apiFlags & (1 << 0)) != 0 {
flags.insert(.isEmoji)
}
if (apiFlags & (1 << 1)) != 0 {
flags.insert(.isTemplate)
}
var files: [TelegramMediaFile] = []
files = stickers.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }
return .stickerPack(TelegramMediaWebpageStickerPackAttribute(flags: flags, files: files))
case let .webPageAttributeUniqueStarGift(webPageAttributeUniqueStarGiftData):
let gift = webPageAttributeUniqueStarGiftData.gift
if let starGift = StarGift(apiStarGift: gift) {
return .starGift(TelegramMediaWebpageStarGiftAttribute(gift: starGift))
}
return nil
case let .webPageAttributeStarGiftCollection(webPageAttributeStarGiftCollectionData):
let icons = webPageAttributeStarGiftCollectionData.icons
var files: [TelegramMediaFile] = []
files = icons.compactMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }
return .giftCollection(TelegramMediaWebpageGiftCollectionAttribute(files: files))
case let .webPageAttributeStarGiftAuction(webPageAttributeStarGiftAuctionData):
let (apiGift, endDate) = (webPageAttributeStarGiftAuctionData.gift, webPageAttributeStarGiftAuctionData.endDate)
guard let gift = StarGift(apiStarGift: apiGift) else {
return nil
}
return .giftAuction(TelegramMediaWebpageGiftAuctionAttribute(gift: gift, endDate: endDate))
case .webPageAttributeStory:
return nil
}
}
func telegramMediaWebpageFromApiWebpage(_ webpage: Api.WebPage) -> TelegramMediaWebpage? {
switch webpage {
case .webPageNotModified:
return nil
case let .webPagePending(webPagePendingData):
let (flags, id, url, date) = (webPagePendingData.flags, webPagePendingData.id, webPagePendingData.url, webPagePendingData.date)
let _ = flags
return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Pending(date, url))
case let .webPage(webPageData):
let (flags, id, url, displayUrl, hash, type, siteName, title, description, photo, embedUrl, embedType, embedWidth, embedHeight, duration, author, document, cachedPage, attributes) = (webPageData.flags, webPageData.id, webPageData.url, webPageData.displayUrl, webPageData.hash, webPageData.type, webPageData.siteName, webPageData.title, webPageData.description, webPageData.photo, webPageData.embedUrl, webPageData.embedType, webPageData.embedWidth, webPageData.embedHeight, webPageData.duration, webPageData.author, webPageData.document, webPageData.cachedPage, webPageData.attributes)
var embedSize: PixelDimensions?
if let embedWidth = embedWidth, let embedHeight = embedHeight {
embedSize = PixelDimensions(width: embedWidth, height: embedHeight)
}
var webpageDuration: Int?
if let duration = duration {
webpageDuration = Int(duration)
}
var image: TelegramMediaImage?
if let photo = photo {
image = telegramMediaImageFromApiPhoto(photo)
}
var file: TelegramMediaFile?
if let document = document {
file = telegramMediaFileFromApiDocument(document, altDocuments: [])
}
var story: TelegramMediaStory?
var webpageAttributes: [TelegramMediaWebpageAttribute] = []
if let attributes = attributes {
webpageAttributes = attributes.compactMap(telegramMediaWebpageAttributeFromApiWebpageAttribute)
for attribute in attributes {
if case let .webPageAttributeStory(webPageAttributeStoryData) = attribute {
let (_, peerId, id, _) = (webPageAttributeStoryData.flags, webPageAttributeStoryData.peer, webPageAttributeStoryData.id, webPageAttributeStoryData.story)
story = TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: false)
}
}
}
var instantPage: InstantPage?
if let cachedPage = cachedPage {
instantPage = InstantPage(apiPage: cachedPage)
}
let isMediaLargeByDefault = (flags & (1 << 13)) != 0
let imageIsVideoCover = (flags & (1 << 14)) != 0
return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Loaded(TelegramMediaWebpageLoadedContent(url: url, displayUrl: displayUrl, hash: hash, type: type, websiteName: siteName, title: title, text: description, embedUrl: embedUrl, embedType: embedType, embedSize: embedSize, duration: webpageDuration, author: author, isMediaLargeByDefault: isMediaLargeByDefault, imageIsVideoCover: imageIsVideoCover, image: image, file: file, story: story, attributes: webpageAttributes, instantPage: instantPage)))
case .webPageEmpty:
return nil
}
}
public class WebpagePreviewMessageAttribute: MessageAttribute, Equatable {
public let associatedPeerIds: [PeerId] = []
public let associatedMediaIds: [MediaId] = []
public let leadingPreview: Bool
public let forceLargeMedia: Bool?
public let isManuallyAdded: Bool
public let isSafe: Bool
public init(leadingPreview: Bool, forceLargeMedia: Bool?, isManuallyAdded: Bool, isSafe: Bool) {
self.leadingPreview = leadingPreview
self.forceLargeMedia = forceLargeMedia
self.isManuallyAdded = isManuallyAdded
self.isSafe = isSafe
}
required public init(decoder: PostboxDecoder) {
self.leadingPreview = decoder.decodeBoolForKey("lp", orElse: false)
self.forceLargeMedia = decoder.decodeOptionalBoolForKey("lm")
self.isManuallyAdded = decoder.decodeBoolForKey("ma", orElse: false)
self.isSafe = decoder.decodeBoolForKey("sf", orElse: false)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeBool(self.leadingPreview, forKey: "lp")
if let forceLargeMedia = self.forceLargeMedia {
encoder.encodeBool(forceLargeMedia, forKey: "lm")
} else {
encoder.encodeNil(forKey: "lm")
}
encoder.encodeBool(self.isManuallyAdded, forKey: "ma")
encoder.encodeBool(self.isSafe, forKey: "sf")
}
public static func ==(lhs: WebpagePreviewMessageAttribute, rhs: WebpagePreviewMessageAttribute) -> Bool {
if lhs.leadingPreview != rhs.leadingPreview {
return false
}
if lhs.forceLargeMedia != rhs.forceLargeMedia {
return false
}
if lhs.isManuallyAdded != rhs.isManuallyAdded {
return false
}
if lhs.isSafe != rhs.isSafe {
return false
}
return true
}
}
@@ -0,0 +1,121 @@
import Foundation
import Postbox
import TelegramApi
extension TelegramPeerNotificationSettings {
convenience init(apiSettings: Api.PeerNotifySettings) {
switch apiSettings {
case let .peerNotifySettings(peerNotifySettingsData):
let (showPreviews, muteUntil, iosSound, desktopSound, storiesMuted, storiesHideSender, storiesIosSound, storiesDesktopSound) = (peerNotifySettingsData.showPreviews, peerNotifySettingsData.muteUntil, peerNotifySettingsData.iosSound, peerNotifySettingsData.otherSound, peerNotifySettingsData.storiesMuted, peerNotifySettingsData.storiesHideSender, peerNotifySettingsData.storiesIosSound, peerNotifySettingsData.storiesOtherSound)
let sound: Api.NotificationSound?
let storiesSound: Api.NotificationSound?
#if os(iOS)
sound = iosSound
storiesSound = storiesIosSound
#elseif os(macOS)
sound = desktopSound
storiesSound = storiesDesktopSound
#endif
let muteState: PeerMuteState
if let muteUntil = muteUntil {
if muteUntil == 0 {
muteState = .unmuted
} else {
muteState = .muted(until: muteUntil)
}
} else {
muteState = .default
}
let displayPreviews: PeerNotificationDisplayPreviews
if let showPreviews = showPreviews {
if case .boolTrue = showPreviews {
displayPreviews = .show
} else {
displayPreviews = .hide
}
} else {
displayPreviews = .default
}
let storiesMutedValue: PeerStoryNotificationSettings.Mute
if let storiesMuted = storiesMuted {
storiesMutedValue = storiesMuted == .boolTrue ? .muted : .unmuted
} else {
storiesMutedValue = .default
}
var storiesHideSenderValue: PeerStoryNotificationSettings.HideSender
if let storiesHideSender = storiesHideSender {
storiesHideSenderValue = storiesHideSender == .boolTrue ? .hide : .show
} else {
storiesHideSenderValue = .default
}
self.init(muteState: muteState, messageSound: PeerMessageSound(apiSound: sound ?? .notificationSoundDefault), displayPreviews: displayPreviews, storySettings: PeerStoryNotificationSettings(
mute: storiesMutedValue,
hideSender: storiesHideSenderValue,
sound: PeerMessageSound(apiSound: storiesSound ?? .notificationSoundDefault)
))
}
}
}
extension PeerMessageSound {
init(apiSound: Api.NotificationSound) {
switch apiSound {
case .notificationSoundDefault:
self = .default
case .notificationSoundNone:
self = .none
case let .notificationSoundLocal(notificationSoundLocalData):
let (_, data) = (notificationSoundLocalData.title, notificationSoundLocalData.data)
var rawApiSound = data
if let index = rawApiSound.firstIndex(of: ".") {
rawApiSound = String(rawApiSound[..<index])
}
let parsedSound: PeerMessageSound
if rawApiSound == "default" {
parsedSound = .default
} else if rawApiSound == "" || rawApiSound == "0" {
parsedSound = .none
} else {
let soundId: Int32
if let id = Int32(rawApiSound) {
soundId = id
} else {
soundId = 100
}
if soundId >= 100 && soundId <= 111 {
parsedSound = .bundledModern(id: soundId - 100)
} else if soundId >= 2 && soundId <= 9 {
parsedSound = .bundledClassic(id: soundId - 2)
} else {
parsedSound = defaultCloudPeerNotificationSound
}
}
self = parsedSound
case let .notificationSoundRingtone(notificationSoundRingtoneData):
let id = notificationSoundRingtoneData.id
self = .cloud(fileId: id)
}
}
var apiSound: Api.NotificationSound {
switch self {
case .none:
return .notificationSoundNone
case .default:
return .notificationSoundDefault
case let .bundledModern(id):
let string = "\(id + 100)"
return .notificationSoundLocal(.init(title: string, data: string))
case let .bundledClassic(id):
let string = "\(id + 2)"
return .notificationSoundLocal(.init(title: string, data: string))
case let .cloud(fileId):
return .notificationSoundRingtone(.init(id: fileId))
}
}
}
@@ -0,0 +1,371 @@
import Foundation
import Postbox
import TelegramApi
func parsedTelegramProfilePhoto(_ photo: Api.UserProfilePhoto) -> [TelegramMediaImageRepresentation] {
var representations: [TelegramMediaImageRepresentation] = []
switch photo {
case let .userProfilePhoto(userProfilePhotoData):
let (flags, id, strippedThumb, dcId) = (userProfilePhotoData.flags, userProfilePhotoData.photoId, userProfilePhotoData.strippedThumb, userProfilePhotoData.dcId)
let hasVideo = (flags & (1 << 0)) != 0
let isPersonal = (flags & (1 << 2)) != 0
let smallResource: TelegramMediaResource
let fullSizeResource: TelegramMediaResource
smallResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: id, sizeSpec: .small, volumeId: nil, localId: nil)
fullSizeResource = CloudPeerPhotoSizeMediaResource(datacenterId: dcId, photoId: id, sizeSpec: .fullSize, volumeId: nil, localId: nil)
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 80, height: 80), resource: smallResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData(), hasVideo: hasVideo, isPersonal: isPersonal))
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: fullSizeResource, progressiveSizes: [], immediateThumbnailData: strippedThumb?.makeData(), hasVideo: hasVideo, isPersonal: isPersonal))
case .userProfilePhotoEmpty:
break
}
return representations
}
extension TelegramPeerUsername {
init(apiUsername: Api.Username) {
switch apiUsername {
case let .username(usernameData):
let (flags, username) = (usernameData.flags, usernameData.username)
self.init(flags: Flags(rawValue: flags), username: username)
}
}
}
extension PeerVerification {
init(apiBotVerification: Api.BotVerification) {
switch apiBotVerification {
case let .botVerification(botVerificationData):
let (botId, icon, description) = (botVerificationData.botId, botVerificationData.icon, botVerificationData.description)
self.init(
botId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)),
iconFileId: icon,
description: description
)
}
}
}
extension TelegramUser {
convenience init(user: Api.User) {
switch user {
case let .user(userData):
let (flags, flags2, id, accessHash, firstName, lastName, username, phone, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, usernames, _, color, profileColor, subscriberCount, verificationIconFileId, _) = (userData.flags, userData.flags2, userData.id, userData.accessHash, userData.firstName, userData.lastName, userData.username, userData.phone, userData.photo, userData.status, userData.botInfoVersion, userData.restrictionReason, userData.botInlinePlaceholder, userData.langCode, userData.emojiStatus, userData.usernames, userData.storiesMaxId, userData.color, userData.profileColor, userData.botActiveUsers, userData.botVerificationIcon, userData.sendPaidMessagesStars)
let representations: [TelegramMediaImageRepresentation] = photo.flatMap(parsedTelegramProfilePhoto) ?? []
let isMin = (flags & (1 << 20)) != 0
let accessHashValue = accessHash.flatMap { value -> TelegramPeerAccessHash in
if isMin {
return .genericPublic(value)
} else {
return .personal(value)
}
}
var userFlags: UserInfoFlags = []
if (flags & (1 << 12)) != 0 {
userFlags.insert(.mutualContact)
}
if (flags & (1 << 17)) != 0 {
userFlags.insert(.isVerified)
}
if (flags & (1 << 23)) != 0 {
userFlags.insert(.isSupport)
}
if (flags & (1 << 24)) != 0 {
userFlags.insert(.isScam)
}
if (flags & (1 << 26)) != 0 {
userFlags.insert(.isFake)
}
if (flags & (1 << 28)) != 0 {
userFlags.insert(.isPremium)
}
if (flags2 & (1 << 2)) != 0 {
userFlags.insert(.isCloseFriend)
}
if (flags2 & (1 << 10)) != 0 {
userFlags.insert(.requirePremium)
}
if (flags2 & (1 << 15)) != 0 {
userFlags.insert(.requireStars)
}
var storiesHidden: Bool?
if !isMin {
storiesHidden = (flags2 & (1 << 3)) != 0
}
var botInfo: BotUserInfo?
if (flags & (1 << 14)) != 0 {
var botFlags = BotUserInfoFlags()
if (flags & (1 << 15)) != 0 {
botFlags.insert(.hasAccessToChatHistory)
}
if (flags & (1 << 16)) == 0 {
botFlags.insert(.worksWithGroups)
}
if (flags & (1 << 21)) != 0 {
botFlags.insert(.requiresGeolocationForInlineRequests)
}
if (flags & (1 << 27)) != 0 {
botFlags.insert(.canBeAddedToAttachMenu)
}
if (flags2 & (1 << 1)) != 0 {
botFlags.insert(.canEdit)
}
if (flags2 & (1 << 11)) != 0 {
botFlags.insert(.isBusiness)
}
if (flags2 & (1 << 13)) != 0 {
botFlags.insert(.hasWebApp)
}
if (flags2 & (1 << 16)) != 0 {
botFlags.insert(.hasForum)
}
if (flags2 & (1 << 17)) != 0 {
botFlags.insert(.forumManagedByUser)
}
botInfo = BotUserInfo(flags: botFlags, inlinePlaceholder: botInlinePlaceholder)
}
let restrictionInfo: PeerAccessRestrictionInfo? = restrictionReason.flatMap(PeerAccessRestrictionInfo.init(apiReasons:))
var nameColor: PeerColor?
var backgroundEmojiId: Int64?
if let color = color {
switch color {
case let .peerColor(peerColorData):
let (_, color, backgroundEmojiIdValue) = (peerColorData.flags, peerColorData.color, peerColorData.backgroundEmojiId)
if let color {
nameColor = .preset(PeerNameColor(rawValue: color))
}
backgroundEmojiId = backgroundEmojiIdValue
case let .peerColorCollectible(peerColorCollectibleData):
let (_, collectibleId, giftEmojiId, backgroundEmojiIdValue, accentColor, colors, darkAccentColor, darkColors) = (peerColorCollectibleData.flags, peerColorCollectibleData.collectibleId, peerColorCollectibleData.giftEmojiId, peerColorCollectibleData.backgroundEmojiId, peerColorCollectibleData.accentColor, peerColorCollectibleData.colors, peerColorCollectibleData.darkAccentColor, peerColorCollectibleData.darkColors)
nameColor = .collectible(PeerCollectibleColor(
collectibleId: collectibleId,
giftEmojiFileId: giftEmojiId,
backgroundEmojiId: backgroundEmojiIdValue,
accentColor: UInt32(bitPattern: accentColor),
colors: colors.map { UInt32(bitPattern: $0) },
darkAccentColor: darkAccentColor.flatMap { UInt32(bitPattern: $0) },
darkColors: darkColors.flatMap { $0.map { UInt32(bitPattern: $0) } }
))
backgroundEmojiId = backgroundEmojiIdValue
case .inputPeerColorCollectible:
break
}
}
var profileColorIndex: Int32?
var profileBackgroundEmojiId: Int64?
if let profileColor = profileColor {
switch profileColor {
case let .peerColor(peerColorData):
let (_, color, backgroundEmojiIdValue) = (peerColorData.flags, peerColorData.color, peerColorData.backgroundEmojiId)
profileColorIndex = color
profileBackgroundEmojiId = backgroundEmojiIdValue
default:
break
}
}
self.init(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)), accessHash: accessHashValue, firstName: firstName, lastName: lastName, username: username, phone: phone, photo: representations, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), usernames: usernames?.map(TelegramPeerUsername.init(apiUsername:)) ?? [], storiesHidden: storiesHidden, nameColor: nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, subscriberCount: subscriberCount, verificationIconFileId: verificationIconFileId)
case let .userEmpty(userEmptyData):
let id = userEmptyData.id
self.init(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)), accessHash: nil, firstName: nil, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil, nameColor: nil, backgroundEmojiId: nil, profileColor: nil, profileBackgroundEmojiId: nil, subscriberCount: nil, verificationIconFileId: nil)
}
}
static func merge(_ lhs: TelegramUser?, rhs: Api.User) -> TelegramUser? {
switch rhs {
case let .user(userData):
let (flags, _, _, rhsAccessHash, _, _, _, _, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, _, _, color, profileColor, subscriberCount, _, _) = (userData.flags, userData.flags2, userData.id, userData.accessHash, userData.firstName, userData.lastName, userData.username, userData.phone, userData.photo, userData.status, userData.botInfoVersion, userData.restrictionReason, userData.botInlinePlaceholder, userData.langCode, userData.emojiStatus, userData.usernames, userData.storiesMaxId, userData.color, userData.profileColor, userData.botActiveUsers, userData.botVerificationIcon, userData.sendPaidMessagesStars)
let isMin = (flags & (1 << 20)) != 0
if !isMin {
return TelegramUser(user: rhs)
} else {
let applyMinPhoto = (flags & (1 << 25)) != 0
let telegramPhoto: [TelegramMediaImageRepresentation]
if let photo = photo, applyMinPhoto {
telegramPhoto = parsedTelegramProfilePhoto(photo)
} else if let currentPhoto = lhs?.photo {
telegramPhoto = currentPhoto
} else {
telegramPhoto = []
}
if let lhs = lhs {
var userFlags: UserInfoFlags = []
if (flags & (1 << 12)) != 0 {
userFlags.insert(.mutualContact)
}
if (flags & (1 << 17)) != 0 {
userFlags.insert(.isVerified)
}
if (flags & (1 << 23)) != 0 {
userFlags.insert(.isSupport)
}
if (flags & (1 << 24)) != 0 {
userFlags.insert(.isScam)
}
if (flags & Int32(1 << 26)) != 0 {
userFlags.insert(.isFake)
}
if (flags & (1 << 28)) != 0 {
userFlags.insert(.isPremium)
}
if lhs.flags.contains(.isCloseFriend) {
userFlags.insert(.isCloseFriend)
}
if lhs.flags.contains(.requirePremium) {
userFlags.insert(.requirePremium)
}
var botInfo: BotUserInfo?
if (flags & (1 << 14)) != 0 {
var botFlags = BotUserInfoFlags()
if (flags & (1 << 15)) != 0 {
botFlags.insert(.hasAccessToChatHistory)
}
if (flags & (1 << 16)) == 0 {
botFlags.insert(.worksWithGroups)
}
if (flags & (1 << 21)) != 0 {
botFlags.insert(.requiresGeolocationForInlineRequests)
}
if (flags & (1 << 27)) != 0 {
botFlags.insert(.canBeAddedToAttachMenu)
}
if let botInfo = lhs.botInfo, botInfo.flags.contains(.canEdit) {
botFlags.insert(.canEdit)
}
botInfo = BotUserInfo(flags: botFlags, inlinePlaceholder: botInlinePlaceholder)
}
let restrictionInfo: PeerAccessRestrictionInfo? = restrictionReason.flatMap(PeerAccessRestrictionInfo.init)
let rhsAccessHashValue = rhsAccessHash.flatMap { value -> TelegramPeerAccessHash in
if isMin {
return .genericPublic(value)
} else {
return .personal(value)
}
}
let accessHash: TelegramPeerAccessHash?
if let rhsAccessHashValue = rhsAccessHashValue, case .personal = rhsAccessHashValue {
accessHash = rhsAccessHashValue
} else {
accessHash = lhs.accessHash ?? rhsAccessHashValue
}
var nameColor: PeerColor?
var backgroundEmojiId: Int64?
if let color {
switch color {
case let .peerColor(peerColorData):
let (_, color, backgroundEmojiIdValue) = (peerColorData.flags, peerColorData.color, peerColorData.backgroundEmojiId)
if let color {
nameColor = .preset(PeerNameColor(rawValue: color))
}
backgroundEmojiId = backgroundEmojiIdValue
case let .peerColorCollectible(peerColorCollectibleData):
let (_, collectibleId, giftEmojiId, backgroundEmojiIdValue, accentColor, colors, darkAccentColor, darkColors) = (peerColorCollectibleData.flags, peerColorCollectibleData.collectibleId, peerColorCollectibleData.giftEmojiId, peerColorCollectibleData.backgroundEmojiId, peerColorCollectibleData.accentColor, peerColorCollectibleData.colors, peerColorCollectibleData.darkAccentColor, peerColorCollectibleData.darkColors)
nameColor = .collectible(PeerCollectibleColor(
collectibleId: collectibleId,
giftEmojiFileId: giftEmojiId,
backgroundEmojiId: backgroundEmojiIdValue,
accentColor: UInt32(bitPattern: accentColor),
colors: colors.map { UInt32(bitPattern: $0) },
darkAccentColor: darkAccentColor.flatMap { UInt32(bitPattern: $0) },
darkColors: darkColors.flatMap { $0.map { UInt32(bitPattern: $0) } }
))
backgroundEmojiId = backgroundEmojiIdValue
case .inputPeerColorCollectible:
break
}
}
var profileColorIndex: Int32?
var profileBackgroundEmojiId: Int64?
if let profileColor = profileColor {
switch profileColor {
case let .peerColor(peerColorData):
let (_, color, backgroundEmojiIdValue) = (peerColorData.flags, peerColorData.color, peerColorData.backgroundEmojiId)
profileColorIndex = color
profileBackgroundEmojiId = backgroundEmojiIdValue
default:
break
}
}
return TelegramUser(id: lhs.id, accessHash: accessHash, firstName: lhs.firstName, lastName: lhs.lastName, username: lhs.username, phone: lhs.phone, photo: telegramPhoto, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags, emojiStatus: emojiStatus.flatMap(PeerEmojiStatus.init(apiStatus:)), usernames: lhs.usernames, storiesHidden: lhs.storiesHidden, nameColor: nameColor, backgroundEmojiId: backgroundEmojiId, profileColor: profileColorIndex.flatMap { PeerNameColor(rawValue: $0) }, profileBackgroundEmojiId: profileBackgroundEmojiId, subscriberCount: subscriberCount, verificationIconFileId: lhs.verificationIconFileId)
} else {
return TelegramUser(user: rhs)
}
}
case .userEmpty:
return TelegramUser(user: rhs)
}
}
static func merge(lhs: TelegramUser?, rhs: TelegramUser) -> TelegramUser {
guard let lhs = lhs else {
return rhs
}
if let rhsAccessHash = rhs.accessHash, case .personal = rhsAccessHash {
return rhs
} else {
var userFlags: UserInfoFlags = []
if rhs.flags.contains(.isVerified) {
userFlags.insert(.isVerified)
}
if rhs.flags.contains(.isSupport) {
userFlags.insert(.isSupport)
}
if rhs.flags.contains(.isScam) {
userFlags.insert(.isScam)
}
if rhs.flags.contains(.isFake) {
userFlags.insert(.isFake)
}
if rhs.flags.contains(.isPremium) {
userFlags.insert(.isPremium)
}
let botInfo: BotUserInfo? = rhs.botInfo
let emojiStatus = rhs.emojiStatus
let restrictionInfo: PeerAccessRestrictionInfo? = rhs.restrictionInfo
let accessHash: TelegramPeerAccessHash?
if let rhsAccessHashValue = rhs.accessHash, case .personal = rhsAccessHashValue {
accessHash = rhsAccessHashValue
} else {
accessHash = lhs.accessHash ?? rhs.accessHash
}
let photo: [TelegramMediaImageRepresentation]
if case .genericPublic = rhs.accessHash {
photo = lhs.photo
} else {
photo = rhs.photo
}
var storiesHidden: Bool?
if let value = rhs.storiesHidden {
storiesHidden = value
} else {
storiesHidden = lhs.storiesHidden
}
return TelegramUser(id: lhs.id, accessHash: accessHash, firstName: lhs.firstName, lastName: lhs.lastName, username: lhs.username, phone: lhs.phone, photo: photo, botInfo: botInfo, restrictionInfo: restrictionInfo, flags: userFlags, emojiStatus: emojiStatus, usernames: lhs.usernames, storiesHidden: storiesHidden, nameColor: rhs.nameColor, backgroundEmojiId: rhs.backgroundEmojiId, profileColor: rhs.profileColor, profileBackgroundEmojiId: rhs.profileBackgroundEmojiId, subscriberCount: rhs.subscriberCount, verificationIconFileId: rhs.verificationIconFileId)
}
}
}
@@ -0,0 +1,45 @@
import Foundation
import Postbox
import TelegramApi
extension TelegramUserPresence {
convenience init(apiStatus: Api.UserStatus) {
switch apiStatus {
case .userStatusEmpty:
self.init(status: .none, lastActivity: 0)
case let .userStatusOnline(userStatusOnlineData):
let (expires) = (userStatusOnlineData.expires)
self.init(status: .present(until: expires), lastActivity: 0)
case let .userStatusOffline(userStatusOfflineData):
let (wasOnline) = (userStatusOfflineData.wasOnline)
self.init(status: .present(until: wasOnline), lastActivity: 0)
case let .userStatusRecently(userStatusRecentlyData):
let (flags) = (userStatusRecentlyData.flags)
let isHidden = (flags & (1 << 0)) != 0
self.init(status: .recently(isHidden: isHidden), lastActivity: 0)
case let .userStatusLastWeek(userStatusLastWeekData):
let (flags) = (userStatusLastWeekData.flags)
let isHidden = (flags & (1 << 0)) != 0
self.init(status: .lastWeek(isHidden: isHidden), lastActivity: 0)
case let .userStatusLastMonth(userStatusLastMonthData):
let (flags) = (userStatusLastMonthData.flags)
let isHidden = (flags & (1 << 0)) != 0
self.init(status: .lastMonth(isHidden: isHidden), lastActivity: 0)
}
}
convenience init?(apiUser: Api.User) {
switch apiUser {
case let .user(userData):
let status = userData.status
if let status = status {
self.init(apiStatus: status)
} else {
self.init(status: .none, lastActivity: 0)
}
case .userEmpty:
return nil
}
}
}
@@ -0,0 +1,96 @@
import Foundation
import Postbox
import TelegramApi
func apiEntitiesFromMessageTextEntities(_ entities: [MessageTextEntity], associatedPeers: SimpleDictionary<PeerId, Peer>) -> [Api.MessageEntity] {
var apiEntities: [Api.MessageEntity] = []
for entity in entities {
let offset: Int32 = Int32(entity.range.lowerBound)
let length: Int32 = Int32(entity.range.upperBound - entity.range.lowerBound)
switch entity.type {
case .Unknown:
break
case .Mention:
apiEntities.append(.messageEntityMention(.init(offset: offset, length: length)))
case .Hashtag:
apiEntities.append(.messageEntityHashtag(.init(offset: offset, length: length)))
case .BotCommand:
apiEntities.append(.messageEntityBotCommand(.init(offset: offset, length: length)))
case .Url:
apiEntities.append(.messageEntityUrl(.init(offset: offset, length: length)))
case .Email:
apiEntities.append(.messageEntityEmail(.init(offset: offset, length: length)))
case .Bold:
apiEntities.append(.messageEntityBold(.init(offset: offset, length: length)))
case .Italic:
apiEntities.append(.messageEntityItalic(.init(offset: offset, length: length)))
case .Code:
apiEntities.append(.messageEntityCode(.init(offset: offset, length: length)))
case let .Pre(language):
apiEntities.append(.messageEntityPre(.init(offset: offset, length: length, language: language ?? "")))
case let .TextUrl(url):
apiEntities.append(.messageEntityTextUrl(.init(offset: offset, length: length, url: url)))
case let .TextMention(peerId):
if let peer = associatedPeers[peerId], let inputUser = apiInputUser(peer) {
apiEntities.append(.inputMessageEntityMentionName(.init(offset: offset, length: length, userId: inputUser)))
}
case .PhoneNumber:
break
case .Strikethrough:
apiEntities.append(.messageEntityStrike(.init(offset: offset, length: length)))
case let .BlockQuote(isCollapsed):
var flags: Int32 = 0
if isCollapsed {
flags |= 1 << 0
}
apiEntities.append(.messageEntityBlockquote(.init(flags: flags, offset: offset, length: length)))
case .Underline:
apiEntities.append(.messageEntityUnderline(.init(offset: offset, length: length)))
case .BankCard:
apiEntities.append(.messageEntityBankCard(.init(offset: offset, length: length)))
case .Spoiler:
apiEntities.append(.messageEntitySpoiler(.init(offset: offset, length: length)))
case let .CustomEmoji(_, fileId):
apiEntities.append(.messageEntityCustomEmoji(.init(offset: offset, length: length, documentId: fileId)))
case let .FormattedDate(format, date):
var flags: Int32 = 0
switch format {
case .relative:
flags |= 1 << 0
case let .full(timeFormat, dateFormat, dayOfWeek):
switch timeFormat {
case .short:
flags |= 1 << 1
case .long:
flags |= 1 << 2
default:
break
}
switch dateFormat {
case .short:
flags |= 1 << 3
case .long:
flags |= 1 << 4
default:
break
}
if dayOfWeek {
flags |= 1 << 5
}
default:
break
}
apiEntities.append(.messageEntityFormattedDate(.init(flags: flags, offset: offset, length: length, date: date)))
case .Custom:
break
}
}
return apiEntities
}
func apiTextAttributeEntities(_ attribute: TextEntitiesMessageAttribute, associatedPeers: SimpleDictionary<PeerId, Peer>) -> [Api.MessageEntity] {
return apiEntitiesFromMessageTextEntities(attribute.entities, associatedPeers: associatedPeers)
}
@@ -0,0 +1,80 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
extension TelegramTheme {
convenience init(apiTheme: Api.Theme) {
switch apiTheme {
case let .theme(themeData):
let (flags, id, accessHash, slug, title, document, settings, emoticon, installCount) = (themeData.flags, themeData.id, themeData.accessHash, themeData.slug, themeData.title, themeData.document, themeData.settings, themeData.emoticon, themeData.installsCount)
self.init(id: id, accessHash: accessHash, slug: slug, emoticon: emoticon, title: title, file: document.flatMap { telegramMediaFileFromApiDocument($0, altDocuments: []) }, settings: settings?.compactMap(TelegramThemeSettings.init(apiThemeSettings:)), isCreator: (flags & 1 << 0) != 0, isDefault: (flags & 1 << 1) != 0, installCount: installCount)
}
}
}
extension TelegramBaseTheme {
init(apiBaseTheme: Api.BaseTheme) {
switch apiBaseTheme {
case .baseThemeClassic:
self = .classic
case .baseThemeDay:
self = .day
case .baseThemeNight:
self = .night
case .baseThemeTinted:
self = .tinted
case .baseThemeArctic:
self = .day
}
}
var apiBaseTheme: Api.BaseTheme {
switch self {
case .classic:
return .baseThemeClassic
case .day:
return .baseThemeDay
case .night:
return .baseThemeNight
case .tinted:
return .baseThemeTinted
}
}
}
extension TelegramThemeSettings {
convenience init?(apiThemeSettings: Api.ThemeSettings) {
switch apiThemeSettings {
case let .themeSettings(themeSettingsData):
let (flags, baseTheme, accentColor, outboxAccentColor, messageColors, wallpaper) = (themeSettingsData.flags, themeSettingsData.baseTheme, themeSettingsData.accentColor, themeSettingsData.outboxAccentColor, themeSettingsData.messageColors, themeSettingsData.wallpaper)
self.init(baseTheme: TelegramBaseTheme(apiBaseTheme: baseTheme), accentColor: UInt32(bitPattern: accentColor), outgoingAccentColor: outboxAccentColor.flatMap { UInt32(bitPattern: $0) }, messageColors: messageColors?.map(UInt32.init(bitPattern:)) ?? [], animateMessageColors: (flags & 1 << 2) != 0, wallpaper: wallpaper.flatMap(TelegramWallpaper.init(apiWallpaper:)))
}
}
var apiInputThemeSettings: Api.InputThemeSettings {
var flags: Int32 = 0
if !self.messageColors.isEmpty {
flags |= 1 << 0
}
if self.animateMessageColors {
flags |= 1 << 2
}
if let _ = self.outgoingAccentColor {
flags |= 1 << 3
}
var inputWallpaper: Api.InputWallPaper?
var inputWallpaperSettings: Api.WallPaperSettings?
if let wallpaper = self.wallpaper, let inputWallpaperAndSettings = wallpaper.apiInputWallpaperAndSettings {
inputWallpaper = inputWallpaperAndSettings.0
inputWallpaperSettings = inputWallpaperAndSettings.1
flags |= 1 << 1
}
return .inputThemeSettings(.init(flags: flags, baseTheme: self.baseTheme.apiBaseTheme, accentColor: Int32(bitPattern: self.accentColor), outboxAccentColor: self.outgoingAccentColor.flatMap { Int32(bitPattern: $0) }, messageColors: self.messageColors.isEmpty ? nil : self.messageColors.map(Int32.init(bitPattern:)), wallpaper: inputWallpaper, wallpaperSettings: inputWallpaperSettings))
}
}
@@ -0,0 +1,15 @@
import Foundation
import Postbox
public final class TypingDraftMessageAttribute: MessageAttribute {
public init() {
}
public init(decoder: PostboxDecoder) {
preconditionFailure()
}
public func encode(_ encoder: PostboxEncoder) {
preconditionFailure()
}
}
@@ -0,0 +1,125 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
extension WallpaperSettings {
init(apiWallpaperSettings: Api.WallPaperSettings) {
switch apiWallpaperSettings {
case let .wallPaperSettings(wallPaperSettingsData):
let (flags, backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor, intensity, rotation, emoticon) = (wallPaperSettingsData.flags, wallPaperSettingsData.backgroundColor, wallPaperSettingsData.secondBackgroundColor, wallPaperSettingsData.thirdBackgroundColor, wallPaperSettingsData.fourthBackgroundColor, wallPaperSettingsData.intensity, wallPaperSettingsData.rotation, wallPaperSettingsData.emoticon)
var colors: [UInt32] = []
if let backgroundColor = backgroundColor {
colors.append(UInt32(bitPattern: backgroundColor))
}
if let secondBackgroundColor = secondBackgroundColor {
colors.append(UInt32(bitPattern: secondBackgroundColor))
}
if let thirdBackgroundColor = thirdBackgroundColor {
colors.append(UInt32(bitPattern: thirdBackgroundColor))
}
if let fourthBackgroundColor = fourthBackgroundColor {
colors.append(UInt32(bitPattern: fourthBackgroundColor))
}
self = WallpaperSettings(blur: (flags & 1 << 1) != 0, motion: (flags & 1 << 2) != 0, colors: colors, intensity: intensity, rotation: rotation, emoticon: emoticon)
}
}
}
func apiWallpaperSettings(_ wallpaperSettings: WallpaperSettings) -> Api.WallPaperSettings {
var flags: Int32 = 0
var backgroundColor: Int32?
if wallpaperSettings.colors.count >= 1 {
flags |= (1 << 0)
backgroundColor = Int32(bitPattern: wallpaperSettings.colors[0])
}
if wallpaperSettings.blur {
flags |= (1 << 1)
}
if wallpaperSettings.motion {
flags |= (1 << 2)
}
if let _ = wallpaperSettings.intensity {
flags |= (1 << 3)
}
if let _ = wallpaperSettings.emoticon {
flags |= (1 << 7)
}
var secondBackgroundColor: Int32?
if wallpaperSettings.colors.count >= 2 {
flags |= (1 << 4)
secondBackgroundColor = Int32(bitPattern: wallpaperSettings.colors[1])
}
var thirdBackgroundColor: Int32?
if wallpaperSettings.colors.count >= 3 {
flags |= (1 << 5)
thirdBackgroundColor = Int32(bitPattern: wallpaperSettings.colors[2])
}
var fourthBackgroundColor: Int32?
if wallpaperSettings.colors.count >= 4 {
flags |= (1 << 6)
fourthBackgroundColor = Int32(bitPattern: wallpaperSettings.colors[3])
}
return .wallPaperSettings(.init(flags: flags, backgroundColor: backgroundColor, secondBackgroundColor: secondBackgroundColor, thirdBackgroundColor: thirdBackgroundColor, fourthBackgroundColor: fourthBackgroundColor, intensity: wallpaperSettings.intensity, rotation: wallpaperSettings.rotation ?? 0, emoticon: wallpaperSettings.emoticon))
}
extension TelegramWallpaper {
init(apiWallpaper: Api.WallPaper) {
switch apiWallpaper {
case let .wallPaper(wallPaperData):
let (id, flags, accessHash, slug, document, settings) = (wallPaperData.id, wallPaperData.flags, wallPaperData.accessHash, wallPaperData.slug, wallPaperData.document, wallPaperData.settings)
if let file = telegramMediaFileFromApiDocument(document, altDocuments: []) {
let wallpaperSettings: WallpaperSettings
if let settings = settings {
wallpaperSettings = WallpaperSettings(apiWallpaperSettings: settings)
} else {
wallpaperSettings = WallpaperSettings()
}
self = .file(TelegramWallpaper.File(id: id, accessHash: accessHash, isCreator: (flags & 1 << 0) != 0, isDefault: (flags & 1 << 1) != 0, isPattern: (flags & 1 << 3) != 0, isDark: (flags & 1 << 4) != 0, slug: slug, file: file, settings: wallpaperSettings))
} else {
//assertionFailure()
self = .color(0xffffff)
}
case let .wallPaperNoFile(wallPaperNoFileData):
let (id, _, settings) = (wallPaperNoFileData.id, wallPaperNoFileData.flags, wallPaperNoFileData.settings)
if let settings = settings, case let .wallPaperSettings(wallPaperSettingsData) = settings {
let (_, backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor, _, rotation, emoticon) = (wallPaperSettingsData.flags, wallPaperSettingsData.backgroundColor, wallPaperSettingsData.secondBackgroundColor, wallPaperSettingsData.thirdBackgroundColor, wallPaperSettingsData.fourthBackgroundColor, wallPaperSettingsData.intensity, wallPaperSettingsData.rotation, wallPaperSettingsData.emoticon)
if id == 0, let emoticon = emoticon {
self = .emoticon(emoticon)
return
}
let colors: [UInt32] = ([backgroundColor, secondBackgroundColor, thirdBackgroundColor, fourthBackgroundColor] as [Int32?]).compactMap({ color -> UInt32? in
return color.flatMap(UInt32.init(bitPattern:))
})
if colors.count > 1 {
self = .gradient(TelegramWallpaper.Gradient(id: id, colors: colors, settings: WallpaperSettings(rotation: rotation)))
} else if colors.count == 1 {
self = .color(UInt32(bitPattern: colors[0]))
} else {
self = .color(0xffffff)
}
} else {
self = .color(0xffffff)
}
}
}
var apiInputWallpaperAndSettings: (Api.InputWallPaper, Api.WallPaperSettings)? {
switch self {
case .builtin:
return nil
case let .file(file):
return (.inputWallPaperSlug(.init(slug: file.slug)), apiWallpaperSettings(file.settings))
case let .color(color):
return (.inputWallPaperNoFile(.init(id: 0)), apiWallpaperSettings(WallpaperSettings(colors: [color])))
case let .gradient(gradient):
return (.inputWallPaperNoFile(.init(id: gradient.id ?? 0)), apiWallpaperSettings(WallpaperSettings(colors: gradient.colors, rotation: gradient.settings.rotation)))
case let .emoticon(emoticon):
return (.inputWallPaperNoFile(.init(id: 0)), apiWallpaperSettings(WallpaperSettings(emoticon: emoticon)))
default:
return nil
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,74 @@
import Postbox
import TelegramApi
import SwiftSignalKit
public func actualizedPeer(accountPeerId: PeerId, postbox: Postbox, network: Network, peer: Peer) -> Signal<Peer, NoError> {
return postbox.transaction { transaction -> Signal<Peer, NoError> in
var signal: Signal<Peer, NoError>
var actualizeChannel: Api.InputChannel?
if let currentPeer = transaction.getPeer(peer.id) {
signal = .single(currentPeer)
if let currentPeer = currentPeer as? TelegramChannel {
switch currentPeer.participationStatus {
case .left, .kicked:
actualizeChannel = apiInputChannel(currentPeer)
default:
break
}
}
} else {
signal = .single(peer)
if let peer = peer as? TelegramChannel {
actualizeChannel = apiInputChannel(peer)
}
}
if let actualizeChannel = actualizeChannel {
let remote = network.request(Api.functions.channels.getChannels(id: [actualizeChannel]))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.Chats?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Peer, NoError> in
return postbox.transaction { transaction -> Signal<Peer, NoError> in
var parsedPeers: AccumulatedPeers?
if let result = result {
let chats: [Api.Chat]
switch result {
case let .chats(chatsData):
let apiChats = chatsData.chats
chats = apiChats
case let .chatsSlice(chatsSliceData):
let apiChats = chatsSliceData.chats
chats = apiChats
}
let parsedPeersValue = AccumulatedPeers(transaction: transaction, chats: chats, users: [])
if parsedPeersValue.allIds.contains(peer.id) {
parsedPeers = parsedPeersValue
}
}
if let parsedPeers = parsedPeers {
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
if let peer = transaction.getPeer(peer.id) {
return .single(peer)
}
}
return .complete()
}
|> switchToLatest
}
signal = signal |> then(remote)
}
let updatedView: Signal<Peer, NoError> = postbox.combinedView(keys: [.peer(peerId: peer.id, components: .all)])
|> mapToSignal { view -> Signal<Peer, NoError> in
if let peerView = view.views[.peer(peerId: peer.id, components: .all)] as? PeerView, let peer = peerView.peers[peerView.peerId] {
return .single(peer)
}
return .complete()
}
return (signal |> then(updatedView)) |> distinctUntilChanged(isEqual: { $0.isEqual($1) })
} |> switchToLatest
}
@@ -0,0 +1,72 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
public func loadedPeerFromMessage(account: Account, peerId: PeerId, messageId: MessageId) -> Signal<Peer?, NoError> {
let accountPeerId = account.peerId
return account.postbox.transaction { transaction -> Signal<Peer?, NoError> in
if let peer = transaction.getPeer(peerId) {
if let user = peer as? TelegramUser {
if let accessHash = user.accessHash, accessHash.value != 0 {
return .single(user)
} else {
let messageSignal: Signal<Api.messages.Messages?, NoError>?
if messageId.peerId.namespace == Namespaces.Peer.CloudUser || messageId.peerId.namespace == Namespaces.Peer.CloudGroup {
messageSignal = account.network.request(Api.functions.messages.getMessages(id: [Api.InputMessage.inputMessageID(.init(id: messageId.id))]))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.Messages?, NoError> in
return .single(nil)
}
} else if messageId.peerId.namespace == Namespaces.Peer.CloudChannel, let channelPeer = transaction.getPeer(messageId.peerId), let inputChannel = apiInputChannel(channelPeer) {
messageSignal = account.network.request(Api.functions.channels.getMessages(channel: inputChannel, id: [Api.InputMessage.inputMessageID(.init(id: messageId.id))]))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.Messages?, NoError> in
return .single(nil)
}
} else {
messageSignal = nil
}
if let messageSignal = messageSignal {
return messageSignal |> mapToSignal { result -> Signal<Peer?, NoError> in
return account.postbox.transaction { transaction -> Peer? in
if let result = result {
let apiUsers: [Api.User]
switch result {
case let .messages(messagesData):
let users = messagesData.users
apiUsers = users
case let .messagesSlice(messagesSliceData):
let users = messagesSliceData.users
apiUsers = users
case let .channelMessages(channelMessagesData):
let users = channelMessagesData.users
apiUsers = users
case .messagesNotModified:
apiUsers = []
}
let parsedPeers = AccumulatedPeers(users: apiUsers)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
if let peer = transaction.getPeer(peerId) {
return peer
}
}
return nil
}
}
} else {
return .single(nil)
}
}
} else {
return .single(peer)
}
} else {
return .single(nil)
}
} |> switchToLatest
}
@@ -0,0 +1,251 @@
import TelegramApi
import SwiftSignalKit
import Postbox
import Foundation
public enum InternalUpdaterError {
case generic
case xmlLoad
case archiveLoad
}
public func requestUpdatesXml(account: Account, source: String) -> Signal<Data, InternalUpdaterError> {
return TelegramEngine(account: account).peers.resolvePeerByName(name: source, referrer: nil)
|> castError(InternalUpdaterError.self)
|> mapToSignal { result -> Signal<Peer?, InternalUpdaterError> in
switch result {
case .progress:
return .never()
case let .result(peer):
return .single(peer?._asPeer())
}
}
|> mapToSignal { peer -> Signal<Data, InternalUpdaterError> in
if let peer = peer, let inputPeer = apiInputPeer(peer) {
return account.network.request(Api.functions.messages.getHistory(peer: inputPeer, offsetId: 0, offsetDate: 0, addOffset: 0, limit: 1, maxId: Int32.max, minId: 0, hash: 0))
|> retryRequest
|> castError(InternalUpdaterError.self)
|> mapToSignal { result in
switch result {
case let .channelMessages(channelMessagesData):
let (apiMessages, apiChats, apiUsers) = (channelMessagesData.messages, channelMessagesData.chats, channelMessagesData.users)
if let apiMessage = apiMessages.first, let storeMessage = StoreMessage(apiMessage: apiMessage, accountPeerId: account.peerId, peerIsForum: peer.isForum) {
var peers: [PeerId: Peer] = [:]
for chat in apiChats {
if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) {
peers[groupOrChannel.id] = groupOrChannel
}
}
for user in apiUsers {
let telegramUser = TelegramUser(user: user)
peers[telegramUser.id] = telegramUser
}
if let message = locallyRenderedMessage(message: storeMessage, peers: peers), let media = message.media.first as? TelegramMediaFile {
return Signal { subscriber in
let fetchDispsable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: MediaResourceReference.media(media: AnyMediaReference.message(message: MessageReference(message), media: media), resource: media.resource)).start()
let dataDisposable = account.postbox.mediaBox.resourceData(media.resource, option: .complete(waitUntilFetchStatus: true)).start(next: { data in
if data.complete {
if let data = try? Data(contentsOf: URL.init(fileURLWithPath: data.path)) {
subscriber.putNext(data)
subscriber.putCompletion()
} else {
subscriber.putError(.xmlLoad)
}
}
})
return ActionDisposable {
fetchDispsable.dispose()
dataDisposable.dispose()
}
}
}
}
default:
break
}
return .fail(.xmlLoad)
}
} else {
return .fail(.xmlLoad)
}
}
}
public enum AppUpdateDownloadResult {
case started(Int)
case progress(Int, Int)
case finished(String)
}
public func downloadAppUpdate(account: Account, source: String, messageId: Int32) -> Signal<AppUpdateDownloadResult, InternalUpdaterError> {
return TelegramEngine(account: account).peers.resolvePeerByName(name: source, referrer: nil)
|> castError(InternalUpdaterError.self)
|> mapToSignal { result -> Signal<Peer?, InternalUpdaterError> in
switch result {
case .progress:
return .never()
case let .result(peer):
return .single(peer?._asPeer())
}
}
|> mapToSignal { peer in
if let peer = peer, let inputChannel = apiInputChannel(peer) {
return account.network.request(Api.functions.channels.getMessages(channel: inputChannel, id: [Api.InputMessage.inputMessageID(.init(id: messageId))]))
|> retryRequest
|> castError(InternalUpdaterError.self)
|> mapToSignal { messages in
switch messages {
case let .channelMessages(channelMessagesData):
let (apiMessages, apiChats, apiUsers) = (channelMessagesData.messages, channelMessagesData.chats, channelMessagesData.users)
var peers: [PeerId: Peer] = [:]
for chat in apiChats {
if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) {
peers[groupOrChannel.id] = groupOrChannel
}
}
for user in apiUsers {
let telegramUser = TelegramUser(user: user)
peers[telegramUser.id] = telegramUser
}
let messageAndFile:(Message, TelegramMediaFile)? = apiMessages.compactMap { value in
return StoreMessage(apiMessage: value, accountPeerId: account.peerId, peerIsForum: peer.isForum)
}.compactMap { value in
return locallyRenderedMessage(message: value, peers: peers)
}.sorted(by: {
$0.id > $1.id
}).first(where: { value -> Bool in
return value.media.first is TelegramMediaFile
}).map { ($0, $0.media.first as! TelegramMediaFile )}
if let (message, media) = messageAndFile {
return Signal { subscriber in
var dataDisposable: Disposable?
var fetchDisposable: Disposable?
var statusDisposable: Disposable?
let removeDisposable = account.postbox.mediaBox.removeCachedResources([media.resource.id]).start(completed: {
let reference = MediaResourceReference.media(media: .message(message: MessageReference(message), media: media), resource: media.resource)
fetchDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: reference).start()
statusDisposable = account.postbox.mediaBox.resourceStatus(media.resource).start(next: { status in
switch status {
case let .Fetching(_, progress):
if let size = media.size {
if progress == 0 {
subscriber.putNext(.started(Int(size)))
} else {
subscriber.putNext(.progress(Int(progress * Float(size)), Int(size)))
}
}
default:
break
}
})
dataDisposable = account.postbox.mediaBox.resourceData(media.resource, option: .complete(waitUntilFetchStatus: true)).start(next: { data in
if data.complete {
subscriber.putNext(.finished(data.path))
subscriber.putCompletion()
}
})
})
return ActionDisposable {
fetchDisposable?.dispose()
dataDisposable?.dispose()
statusDisposable?.dispose()
removeDisposable.dispose()
}
}
} else {
return .fail(.archiveLoad)
}
default:
break
}
return .fail(.archiveLoad)
}
} else {
return .fail(.archiveLoad)
}
}
}
public func requestApplicationIcons(engine: TelegramEngine, source: String = "macos_app_icons") -> Signal<Void, NoError> {
return engine.peers.resolvePeerByName(name: source, referrer: nil)
|> mapToSignal { result -> Signal<Peer?, NoError> in
switch result {
case .progress:
return .never()
case let .result(peer):
return .single(peer?._asPeer())
}
} |> mapToSignal { peer -> Signal<Void, NoError> in
if let peer = peer, let inputPeer = apiInputPeer(peer) {
return engine.account.network.request(Api.functions.messages.getHistory(peer: inputPeer, offsetId: 0, offsetDate: 0, addOffset: 0, limit: 100, maxId: Int32.max, minId: 0, hash: 0))
|> retryRequest
|> mapToSignal { result in
switch result {
case let .channelMessages(channelMessagesData):
let (apiMessages, apiChats, apiUsers) = (channelMessagesData.messages, channelMessagesData.chats, channelMessagesData.users)
var icons: [TelegramApplicationIcons.Icon] = []
for apiMessage in apiMessages.reversed() {
if let storeMessage = StoreMessage(apiMessage: apiMessage, accountPeerId: engine.account.peerId, peerIsForum: peer.isForum) {
var peers: [PeerId: Peer] = [:]
for chat in apiChats {
if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) {
peers[groupOrChannel.id] = groupOrChannel
}
}
for user in apiUsers {
let telegramUser = TelegramUser(user: user)
peers[telegramUser.id] = telegramUser
}
if let message = locallyRenderedMessage(message: storeMessage, peers: peers), let media = message.media.first as? TelegramMediaFile {
icons.append(.init(file: media, reference: MessageReference(message)))
}
}
}
return _internal_updateApplicationIcons(postbox: engine.account.postbox, icons: .init(icons: icons))
default:
break
}
return .complete()
}
} else {
return .complete()
}
}
}
/*
return Signal { subscriber in
let fetchDispsable = fetchedMediaResource(mediaBox: engine.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: MediaResourceReference.media(media: AnyMediaReference.message(message: MessageReference(message), media: media), resource: media.resource)).start()
let dataDisposable = engine.account.postbox.mediaBox.resourceData(media.resource, option: .complete(waitUntilFetchStatus: true)).start(next: { data in
if data.complete {
if let data = try? Data(contentsOf: URL.init(fileURLWithPath: data.path)) {
subscriber.putNext(data)
subscriber.putCompletion()
} else {
subscriber.putError(.xmlLoad)
}
}
})
return ActionDisposable {
fetchDispsable.dispose()
dataDisposable.dispose()
}
}
*/
@@ -0,0 +1,80 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
private typealias SignalKitTimer = SwiftSignalKit.Timer
private final class NotificationAutolockReportManagerImpl {
private let queue: Queue
private let network: Network
let isPerformingUpdate = ValuePromise<Bool>(false, ignoreRepeated: true)
private var deadlineDisposable: Disposable?
private let currentRequestDisposable = MetaDisposable()
private var onlineTimer: SignalKitTimer?
init(queue: Queue, deadline: Signal<Int32?, NoError>, network: Network) {
self.queue = queue
self.network = network
self.deadlineDisposable = (deadline
|> distinctUntilChanged
|> deliverOn(self.queue)).start(next: { [weak self] value in
self?.updateDeadline(value)
})
}
deinit {
assert(self.queue.isCurrent())
self.deadlineDisposable?.dispose()
self.currentRequestDisposable.dispose()
self.onlineTimer?.invalidate()
}
private func updateDeadline(_ deadline: Int32?) {
self.isPerformingUpdate.set(true)
let value: Int32
if let deadline = deadline {
value = max(0, deadline - Int32(CFAbsoluteTimeGetCurrent()))
} else {
value = -1
}
self.currentRequestDisposable.set((self.network.request(Api.functions.account.updateDeviceLocked(period: value))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> deliverOn(self.queue)).start(completed: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.isPerformingUpdate.set(false)
}))
}
}
final class NotificationAutolockReportManager {
private let queue = Queue()
private let impl: QueueLocalObject<NotificationAutolockReportManagerImpl>
init(deadline: Signal<Int32?, NoError>, network: Network) {
let queue = self.queue
self.impl = QueueLocalObject(queue: self.queue, generate: {
return NotificationAutolockReportManagerImpl(queue: queue, deadline: deadline, network: network)
})
}
func isPerformingUpdate() -> Signal<Bool, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.isPerformingUpdate.get().start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
}
@@ -0,0 +1,71 @@
//
// File.swift
//
//
// Created by Mikhail Filimonov on 25.01.2024.
//
import Foundation
import Postbox
import SwiftSignalKit
public struct TelegramApplicationIcons : PostboxCoding, Equatable {
public init(decoder: PostboxDecoder) {
self.icons = (try? decoder.decodeObjectArrayWithCustomDecoderForKey("i", decoder: { Icon(decoder: $0) })) ?? []
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeObjectArray(self.icons, forKey: "i")
}
public struct Icon : PostboxCoding, Equatable {
public init(decoder: PostboxDecoder) {
self.file = decoder.decodeObjectForKey("f") as! TelegramMediaFile
self.reference = decoder.decodeObjectForKey("r", decoder: { MessageReference(decoder: $0) }) as! MessageReference
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeObject(self.file, forKey: "f")
encoder.encodeObject(self.reference, forKey: "r")
}
public let file: TelegramMediaFile
public let reference: MessageReference
init(file: TelegramMediaFile, reference: MessageReference) {
self.file = file
self.reference = reference
}
}
public var icons: [Icon]
init(icons: [Icon]) {
self.icons = icons
}
static var entryId: ItemCacheEntryId {
let cacheKey = ValueBoxKey(length: 1)
cacheKey.setInt8(0, value: 0)
return ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.applicationIcons, key: cacheKey)
}
}
func _internal_applicationIcons(account: Account) -> Signal<TelegramApplicationIcons, NoError> {
let key = PostboxViewKey.cachedItem(TelegramApplicationIcons.entryId)
return account.postbox.combinedView(keys: [key])
|> mapToSignal { views -> Signal<TelegramApplicationIcons, NoError> in
guard let icons = (views.views[key] as? CachedItemView)?.value?.getLegacy(TelegramApplicationIcons.self) as? TelegramApplicationIcons else {
return .single(.init(icons: []))
}
return .single(icons)
}
}
func _internal_updateApplicationIcons(postbox: Postbox, icons: TelegramApplicationIcons) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
let entry = CodableEntry(legacyValue: icons)
transaction.putItemCacheEntry(id: TelegramApplicationIcons.entryId, entry: entry)
}
}
@@ -0,0 +1,469 @@
import Foundation
import Postbox
import MtProtoKit
import SwiftSignalKit
import TelegramApi
private func roundUp(_ value: Int, to multiple: Int) -> Int {
if multiple == 0 {
return value
}
let remainder = value % multiple
if remainder == 0 {
return value
}
return value + multiple - remainder
}
enum UploadPartError {
case generic
case invalidMedia
}
private func wrapMethodBody(_ body: (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>), useCompression: Bool) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
if useCompression {
if let compressed = MTGzip.compress(body.1.makeData()) {
if compressed.count < body.1.size {
let os = MTOutputStream()
os.write(0x3072cfa1 as Int32)
os.writeBytes(compressed)
return (body.0, Buffer(data: os.currentBytes()), body.2)
}
}
}
return body
}
class Download: NSObject, MTRequestMessageServiceDelegate {
let datacenterId: Int
let isCdn: Bool
let context: MTContext
let mtProto: MTProto
let requestService: MTRequestMessageService
let useRequestTimeoutTimers: Bool
private let logPrefix = Atomic<String?>(value: nil)
private var shouldKeepConnectionDisposable: Disposable?
init(queue: Queue, datacenterId: Int, isMedia: Bool, isCdn: Bool, context: MTContext, masterDatacenterId: Int, usageInfo: MTNetworkUsageCalculationInfo?, shouldKeepConnection: Signal<Bool, NoError>, useRequestTimeoutTimers: Bool) {
self.datacenterId = datacenterId
self.isCdn = isCdn
self.context = context
self.useRequestTimeoutTimers = useRequestTimeoutTimers
var requiredAuthToken: Any?
var authTokenMasterDatacenterId: Int = 0
if !isCdn && datacenterId != masterDatacenterId {
authTokenMasterDatacenterId = masterDatacenterId
requiredAuthToken = Int(datacenterId) as NSNumber
}
self.mtProto = MTProto(context: self.context, datacenterId: datacenterId, usageCalculationInfo: usageInfo, requiredAuthToken: requiredAuthToken, authTokenMasterDatacenterId: authTokenMasterDatacenterId)
let logPrefix = self.logPrefix
self.mtProto.getLogPrefix = {
return logPrefix.with { $0 }
}
self.mtProto.cdn = isCdn
self.mtProto.useTempAuthKeys = self.context.useTempAuthKeys && !isCdn
self.mtProto.media = isMedia
self.requestService = MTRequestMessageService(context: self.context)
self.requestService.forceBackgroundRequests = true
super.init()
self.requestService.delegate = self
self.mtProto.add(self.requestService)
let mtProto = self.mtProto
self.shouldKeepConnectionDisposable = (shouldKeepConnection |> distinctUntilChanged |> deliverOn(queue)).start(next: { [weak mtProto] value in
if let mtProto = mtProto {
if value {
Logger.shared.log("Network", "Resume worker network connection")
mtProto.resume()
} else {
Logger.shared.log("Network", "Pause worker network connection")
mtProto.pause()
}
}
})
}
deinit {
self.mtProto.remove(self.requestService)
self.mtProto.stop()
self.mtProto.finalizeSession()
self.shouldKeepConnectionDisposable?.dispose()
}
func requestMessageServiceAuthorizationRequired(_ requestMessageService: MTRequestMessageService!) {
self.context.updateAuthTokenForDatacenter(withId: self.datacenterId, authToken: nil)
self.context.authTokenForDatacenter(withIdRequired: self.datacenterId, authToken:self.mtProto.requiredAuthToken, masterDatacenterId: self.mtProto.authTokenMasterDatacenterId)
}
static func uploadPart(multiplexedManager: MultiplexedRequestManager, datacenterId: Int, consumerId: Int64, tag: MediaResourceFetchTag?, fileId: Int64, index: Int, data: Data, asBigPart: Bool, bigTotalParts: Int? = nil, useCompression: Bool = false, onFloodWaitError: ((String) -> Void)? = nil) -> Signal<Void, UploadPartError> {
let saveFilePart: (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>)
if asBigPart {
let totalParts: Int32
if let bigTotalParts = bigTotalParts, bigTotalParts > 0 && bigTotalParts < Int32.max {
totalParts = Int32(bigTotalParts)
} else {
totalParts = -1
}
saveFilePart = Api.functions.upload.saveBigFilePart(fileId: fileId, filePart: Int32(index), fileTotalParts: totalParts, bytes: Buffer(data: data))
} else {
saveFilePart = Api.functions.upload.saveFilePart(fileId: fileId, filePart: Int32(index), bytes: Buffer(data: data))
}
return multiplexedManager.request(to: .main(datacenterId), consumerId: consumerId, resourceId: nil, data: wrapMethodBody(saveFilePart, useCompression: useCompression), tag: tag, continueInBackground: true, onFloodWaitError: onFloodWaitError, expectedResponseSize: nil)
|> mapError { error -> UploadPartError in
if error.errorCode == 400 {
return .invalidMedia
} else {
return .generic
}
}
|> mapToSignal { _ -> Signal<Void, UploadPartError> in
return .complete()
}
}
func uploadPart(fileId: Int64, index: Int, data: Data, asBigPart: Bool, bigTotalParts: Int? = nil, useCompression: Bool = false, onFloodWaitError: ((String) -> Void)? = nil) -> Signal<Void, UploadPartError> {
return Signal<Void, MTRpcError> { subscriber in
let request = MTRequest()
var saveFilePart: (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>)
if asBigPart {
let totalParts: Int32
if let bigTotalParts = bigTotalParts {
totalParts = Int32(bigTotalParts)
} else {
totalParts = -1
}
saveFilePart = Api.functions.upload.saveBigFilePart(fileId: fileId, filePart: Int32(index), fileTotalParts: totalParts, bytes: Buffer(data: data))
} else {
saveFilePart = Api.functions.upload.saveFilePart(fileId: fileId, filePart: Int32(index), bytes: Buffer(data: data))
}
saveFilePart = wrapMethodBody(saveFilePart, useCompression: useCompression)
request.setPayload(saveFilePart.1.makeData() as Data, metadata: WrappedRequestMetadata(metadata: WrappedFunctionDescription(saveFilePart.0), tag: nil), shortMetadata: WrappedRequestShortMetadata(shortMetadata: WrappedShortFunctionDescription(saveFilePart.0)), responseParser: { response in
if let result = saveFilePart.2.parse(Buffer(data: response)) {
return BoxedMessage(result)
}
return nil
})
request.dependsOnPasswordEntry = false
request.shouldContinueExecutionWithErrorContext = { errorContext in
guard let errorContext = errorContext else {
return true
}
if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText {
onFloodWaitError(errorText)
}
return true
}
request.completed = { (boxedResponse, timestamp, error) -> () in
if let error = error {
subscriber.putError(error)
} else {
subscriber.putCompletion()
}
}
let internalId: Any! = request.internalId
self.requestService.add(request)
return ActionDisposable {
self.requestService.removeRequest(byInternalId: internalId)
}
} |> `catch` { value -> Signal<Void, UploadPartError> in
if value.errorCode == 400 {
return .fail(.invalidMedia)
} else {
return .fail(.generic)
}
}
}
func webFilePart(location: Api.InputWebFileLocation, offset: Int, length: Int) -> Signal<Data, NoError> {
return Signal<Data, MTRpcError> { subscriber in
let request = MTRequest()
request.expectedResponseSize = Int32(length)
var updatedLength = roundUp(length, to: 4096)
while updatedLength % 4096 != 0 || 1048576 % updatedLength != 0 {
updatedLength += 1
}
let data = Api.functions.upload.getWebFile(location: location, offset: Int32(offset), limit: Int32(updatedLength))
request.setPayload(data.1.makeData() as Data, metadata: WrappedRequestMetadata(metadata: WrappedFunctionDescription(data.0), tag: nil), shortMetadata: WrappedRequestShortMetadata(shortMetadata: WrappedFunctionDescription(data.0)), responseParser: { response in
if let result = data.2.parse(Buffer(data: response)) {
return BoxedMessage(result)
}
return nil
})
request.dependsOnPasswordEntry = false
request.needsTimeoutTimer = self.useRequestTimeoutTimers
request.shouldContinueExecutionWithErrorContext = { errorContext in
return true
}
request.completed = { (boxedResponse, timestamp, error) -> () in
if let error = error {
subscriber.putError(error)
} else {
if let result = (boxedResponse as! BoxedMessage).body as? Api.upload.WebFile {
switch result {
case let .webFile(webFileData):
let bytes = webFileData.bytes
subscriber.putNext(bytes.makeData())
}
subscriber.putCompletion()
}
else {
subscriber.putError(MTRpcError(errorCode: 500, errorDescription: "TL_VERIFICATION_ERROR"))
}
}
}
let internalId: Any! = request.internalId
self.requestService.add(request)
return ActionDisposable {
self.requestService.removeRequest(byInternalId: internalId)
}
} |> retryRequest
}
func part(location: Api.InputFileLocation, offset: Int64, length: Int) -> Signal<Data, NoError> {
return Signal<Data, MTRpcError> { subscriber in
let request = MTRequest()
request.expectedResponseSize = Int32(length)
var updatedLength = roundUp(length, to: 4096)
while updatedLength % 4096 != 0 || 1048576 % updatedLength != 0 {
updatedLength += 1
}
let data = Api.functions.upload.getFile(flags: 0, location: location, offset: offset, limit: Int32(updatedLength))
request.setPayload(data.1.makeData() as Data, metadata: WrappedRequestMetadata(metadata: WrappedFunctionDescription(data.0), tag: nil), shortMetadata: WrappedRequestShortMetadata(shortMetadata: WrappedShortFunctionDescription(data.0)), responseParser: { response in
if let result = data.2.parse(Buffer(data: response)) {
return BoxedMessage(result)
}
return nil
})
request.dependsOnPasswordEntry = false
request.needsTimeoutTimer = self.useRequestTimeoutTimers
request.shouldContinueExecutionWithErrorContext = { errorContext in
return true
}
request.completed = { (boxedResponse, timestamp, error) -> () in
if let error = error {
subscriber.putError(error)
} else {
if let result = (boxedResponse as! BoxedMessage).body as? Api.upload.File {
switch result {
case let .file(fileData):
let bytes = fileData.bytes
subscriber.putNext(bytes.makeData())
case .fileCdnRedirect:
break
}
subscriber.putCompletion()
}
else {
subscriber.putError(MTRpcError(errorCode: 500, errorDescription: "TL_VERIFICATION_ERROR"))
}
}
}
let internalId: Any! = request.internalId
self.requestService.add(request)
return ActionDisposable {
self.requestService.removeRequest(byInternalId: internalId)
}
}
|> retryRequest
}
func request<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), expectedResponseSize: Int32? = nil, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil) -> Signal<T, MTRpcError> {
return Signal { subscriber in
let request = MTRequest()
request.expectedResponseSize = expectedResponseSize ?? 0
request.setPayload(data.1.makeData() as Data, metadata: WrappedRequestMetadata(metadata: WrappedFunctionDescription(data.0), tag: nil), shortMetadata: WrappedRequestShortMetadata(shortMetadata: WrappedShortFunctionDescription(data.0)), responseParser: { response in
if let result = data.2.parse(Buffer(data: response)) {
return BoxedMessage(result)
}
return nil
})
request.dependsOnPasswordEntry = false
request.needsTimeoutTimer = self.useRequestTimeoutTimers
request.shouldContinueExecutionWithErrorContext = { errorContext in
guard let errorContext = errorContext else {
return true
}
if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText {
onFloodWaitError(errorText)
}
if errorContext.floodWaitSeconds > 0 && !automaticFloodWait {
return false
}
return true
}
request.completed = { (boxedResponse, timestamp, error) -> () in
if let error = error {
subscriber.putError(error)
} else {
if let result = (boxedResponse as! BoxedMessage).body as? T {
subscriber.putNext(result)
subscriber.putCompletion()
}
else {
subscriber.putError(MTRpcError(errorCode: 500, errorDescription: "TL_VERIFICATION_ERROR"))
}
}
}
let internalId: Any! = request.internalId
self.requestService.add(request)
return ActionDisposable {
self.requestService.removeRequest(byInternalId: internalId)
}
}
}
func requestWithAdditionalData<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil, failOnServerErrors: Bool = false, expectedResponseSize: Int32? = nil) -> Signal<(T, Double), (MTRpcError, Double)> {
return Signal { subscriber in
let request = MTRequest()
request.expectedResponseSize = expectedResponseSize ?? 0
request.setPayload(data.1.makeData() as Data, metadata: WrappedRequestMetadata(metadata: WrappedFunctionDescription(data.0), tag: nil), shortMetadata: WrappedRequestShortMetadata(shortMetadata: WrappedShortFunctionDescription(data.0)), responseParser: { response in
if let result = data.2.parse(Buffer(data: response)) {
return BoxedMessage(result)
}
return nil
})
request.dependsOnPasswordEntry = false
request.needsTimeoutTimer = self.useRequestTimeoutTimers
request.shouldContinueExecutionWithErrorContext = { errorContext in
guard let errorContext = errorContext else {
return true
}
if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText {
onFloodWaitError(errorText)
}
if errorContext.floodWaitSeconds > 0 && !automaticFloodWait {
return false
}
if errorContext.internalServerErrorCount > 0 && failOnServerErrors {
return false
}
return true
}
request.completed = { (boxedResponse, info, error) -> () in
if let error = error {
subscriber.putError((error, info?.timestamp ?? 0.0))
} else {
if let result = (boxedResponse as! BoxedMessage).body as? T {
subscriber.putNext((result, info?.timestamp ?? 0.0))
subscriber.putCompletion()
}
else {
subscriber.putError((MTRpcError(errorCode: 500, errorDescription: "TL_VERIFICATION_ERROR"), info?.timestamp ?? 0.0))
}
}
}
let internalId: Any! = request.internalId
self.requestService.add(request)
return ActionDisposable {
self.requestService.removeRequest(byInternalId: internalId)
}
}
}
func rawRequest(_ data: (FunctionDescription, Buffer, (Buffer) -> Any?), automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil, failOnServerErrors: Bool = false, logPrefix: String = "", expectedResponseSize: Int32? = nil) -> Signal<(Any, NetworkResponseInfo), (MTRpcError, Double)> {
let requestService = self.requestService
return Signal { subscriber in
let request = MTRequest()
request.expectedResponseSize = expectedResponseSize ?? 0
request.setPayload(data.1.makeData() as Data, metadata: WrappedRequestMetadata(metadata: WrappedFunctionDescription(data.0), tag: nil), shortMetadata: WrappedRequestShortMetadata(shortMetadata: WrappedShortFunctionDescription(data.0)), responseParser: { response in
if let result = data.2(Buffer(data: response)) {
return BoxedMessage(result)
}
return nil
})
request.dependsOnPasswordEntry = false
request.needsTimeoutTimer = self.useRequestTimeoutTimers
request.shouldContinueExecutionWithErrorContext = { errorContext in
guard let errorContext = errorContext else {
return true
}
if let onFloodWaitError, errorContext.floodWaitSeconds > 0, let errorText = errorContext.floodWaitErrorText {
onFloodWaitError(errorText)
}
if errorContext.floodWaitSeconds > 0 && !automaticFloodWait {
return false
}
if errorContext.internalServerErrorCount > 0 && failOnServerErrors {
return false
}
return true
}
request.completed = { (boxedResponse, info, error) -> () in
if let error = error {
subscriber.putError((error, info?.timestamp ?? 0))
} else {
let mappedInfo = NetworkResponseInfo(
timestamp: info?.timestamp ?? 0.0,
networkType: info?.networkType == 0 ? .wifi : .cellular,
networkDuration: info?.duration ?? 0.0
)
subscriber.putNext(((boxedResponse as! BoxedMessage).body, mappedInfo))
subscriber.putCompletion()
}
}
let internalId: Any! = request.internalId
requestService.add(request)
return ActionDisposable { [weak requestService] in
requestService?.removeRequest(byInternalId: internalId)
}
}
}
}
@@ -0,0 +1,35 @@
import Foundation
import Postbox
import SwiftSignalKit
import MtProtoKit
public func fetchHttpResource(url: String, preserveExactUrl: Bool = false) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
var urlString: String? = url
if !preserveExactUrl {
urlString = url.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)
}
if let urlString, let url = URL(string: urlString) {
let signal = MTHttpRequestOperation.data(forHttpUrl: url)!
return Signal { subscriber in
subscriber.putNext(.reset)
let disposable = signal.start(next: { next in
if let response = next as? MTHttpResponse {
let fetchResult: MediaResourceDataFetchResult = .dataPart(resourceOffset: 0, data: response.data, range: 0 ..< Int64(response.data.count), complete: true)
subscriber.putNext(fetchResult)
subscriber.putCompletion()
} else {
subscriber.putError(.generic)
}
}, error: { _ in
subscriber.putError(.generic)
}, completed: {
})
return ActionDisposable {
disposable?.dispose()
}
}
} else {
return .never()
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,543 @@
import SGSimpleSettings
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
import MtProtoKit
import CryptoUtils
import ManagedFile
private typealias SignalKitTimer = SwiftSignalKit.Timer
private struct UploadPart {
let fileId: Int64
let index: Int
let data: Data
let bigTotalParts: Int?
let bigPart: Bool
}
private func md5(_ data: Data) -> Data {
return data.withUnsafeBytes { rawBytes -> Data in
let bytes = rawBytes.baseAddress!
return CryptoMD5(bytes, Int32(data.count))
}
}
private final class MultipartUploadState {
let aesKey: Data
var aesIv: Data
var effectiveSize: Int64 = 0
init(encryptionKey: SecretFileEncryptionKey?) {
if let encryptionKey = encryptionKey {
self.aesKey = encryptionKey.aesKey
self.aesIv = encryptionKey.aesIv
} else {
self.aesKey = Data()
self.aesIv = Data()
}
}
func transformHeader(data: Data) -> Data {
assert(self.aesKey.isEmpty)
self.effectiveSize += Int64(data.count)
return data
}
func transform(data: Data) -> Data {
if self.aesKey.count != 0 {
var encryptedData = data
var paddingSize = 0
while (encryptedData.count + paddingSize) % 16 != 0 {
paddingSize += 1
}
if paddingSize != 0 {
encryptedData.count = encryptedData.count + paddingSize
}
let encryptedDataCount = encryptedData.count
encryptedData.withUnsafeMutableBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
if paddingSize != 0 {
arc4random_buf(bytes.advanced(by: encryptedDataCount - paddingSize), paddingSize)
}
self.aesIv.withUnsafeMutableBytes { rawIv -> Void in
let iv = rawIv.baseAddress!.assumingMemoryBound(to: UInt8.self)
MTAesEncryptBytesInplaceAndModifyIv(bytes, encryptedDataCount, self.aesKey, iv)
}
}
self.effectiveSize += Int64(encryptedData.count)
return encryptedData
} else {
self.effectiveSize += Int64(data.count)
return data
}
}
func finalize() -> Int64 {
return self.effectiveSize
}
}
private struct MultipartIntermediateResult {
let id: Int64
let partCount: Int32
let md5Digest: String
let size: Int64
let bigTotalParts: Int?
}
private enum MultipartUploadData {
case resourceData(MediaResourceData)
case data(Data)
var size: Int64 {
switch self {
case let .resourceData(data):
return data.size
case let .data(data):
return Int64(data.count)
}
}
var complete: Bool {
switch self {
case let .resourceData(data):
return data.complete
case .data:
return true
}
}
}
private enum HeaderPartState {
case notStarted
case uploading
case ready
}
private final class MultipartUploadManager {
let parallelParts: Int
var defaultPartSize: Int64
var bigTotalParts: Int?
var bigParts: Bool
private let forceNoBigParts: Bool
private let useLargerParts: Bool
let queue = Queue()
let fileId: Int64
let dataSignal: Signal<MultipartUploadData, NoError>
var committedOffset: Int64
let uploadPart: (UploadPart) -> Signal<Void, UploadPartError>
let progress: (Float) -> Void
let completed: (MultipartIntermediateResult?) -> Void
var uploadingParts: [Int64: (Int64, Disposable)] = [:]
var uploadedParts: [Int64: Int64] = [:]
let dataDisposable = MetaDisposable()
var resourceData: MultipartUploadData?
var headerPartState: HeaderPartState
let state: MultipartUploadState
init(headerSize: Int32, data: Signal<MultipartUploadData, NoError>, encryptionKey: SecretFileEncryptionKey?, hintFileSize: Int64?, hintFileIsLarge: Bool, forceNoBigParts: Bool, useLargerParts: Bool, increaseParallelParts: Bool, uploadPart: @escaping (UploadPart) -> Signal<Void, UploadPartError>, progress: @escaping (Float) -> Void, completed: @escaping (MultipartIntermediateResult?) -> Void) {
self.dataSignal = data
var fileId: Int64 = 0
arc4random_buf(&fileId, 8)
self.fileId = fileId
if increaseParallelParts {
self.parallelParts = 30
} else {
self.parallelParts = 3
}
self.forceNoBigParts = forceNoBigParts
self.useLargerParts = useLargerParts
self.state = MultipartUploadState(encryptionKey: encryptionKey)
self.committedOffset = 0
self.uploadPart = uploadPart
self.progress = progress
self.completed = completed
if headerSize == 0 {
self.headerPartState = .ready
} else {
self.headerPartState = .notStarted
}
if let hintFileSize = hintFileSize, hintFileSize > 10 * 1024 * 1024, !forceNoBigParts {
self.defaultPartSize = 512 * 1024
self.bigTotalParts = Int((hintFileSize / self.defaultPartSize) + (hintFileSize % self.defaultPartSize == 0 ? 0 : 1))
self.bigParts = true
} else if hintFileIsLarge, !forceNoBigParts {
self.defaultPartSize = 512 * 1024
self.bigTotalParts = nil
self.bigParts = true
} else if useLargerParts {
self.bigParts = false
self.defaultPartSize = 256 * 1024
self.bigTotalParts = nil
} else {
self.bigParts = false
self.defaultPartSize = 128 * 1024
self.bigTotalParts = nil
}
}
deinit {
let uploadingParts = self.uploadingParts
let dataDisposable = self.dataDisposable
self.queue.async {
for (_, (_, disposable)) in uploadingParts {
disposable.dispose()
}
dataDisposable.dispose()
}
}
func start() {
self.queue.async {
self.dataDisposable.set((self.dataSignal
|> deliverOn(self.queue)).startStrict(next: { [weak self] data in
if let strongSelf = self {
strongSelf.resourceData = data
strongSelf.checkState()
}
}))
}
}
func cancel() {
self.queue.async {
for (_, (_, disposable)) in self.uploadingParts {
disposable.dispose()
}
}
}
func checkState() {
if let resourceData = self.resourceData, resourceData.complete && resourceData.size != 0 {
if self.committedOffset == 0 && self.uploadedParts.isEmpty && self.uploadingParts.isEmpty {
if resourceData.size > 10 * 1024 * 1024, !self.forceNoBigParts {
self.defaultPartSize = 512 * 1024
self.bigTotalParts = Int(resourceData.size / self.defaultPartSize) + (resourceData.size % self.defaultPartSize == 0 ? 0 : 1)
self.bigParts = true
} else {
self.bigParts = false
if self.useLargerParts {
self.defaultPartSize = 256 * 1024
} else {
self.defaultPartSize = 16 * 1024
}
self.bigTotalParts = nil
}
}
}
var updatedCommittedOffset = false
for offset in self.uploadedParts.keys.sorted() {
if offset == self.committedOffset {
let partSize = self.uploadedParts[offset]!
self.committedOffset += partSize
updatedCommittedOffset = true
let _ = self.uploadedParts.removeValue(forKey: offset)
}
}
if updatedCommittedOffset {
if let resourceData = self.resourceData, resourceData.complete && resourceData.size != 0 {
self.progress(Float(self.committedOffset) / Float(resourceData.size))
}
}
if let resourceData = self.resourceData, resourceData.complete, self.committedOffset >= resourceData.size {
switch self.headerPartState {
case .ready:
let effectiveSize = self.state.finalize()
let effectivePartCount = Int32(effectiveSize / self.defaultPartSize + (effectiveSize % self.defaultPartSize == 0 ? 0 : 1))
var currentBigTotalParts = self.bigTotalParts
if self.bigParts {
currentBigTotalParts = Int(resourceData.size / self.defaultPartSize) + (resourceData.size % self.defaultPartSize == 0 ? 0 : 1)
}
self.completed(MultipartIntermediateResult(id: self.fileId, partCount: effectivePartCount, md5Digest: "", size: resourceData.size, bigTotalParts: currentBigTotalParts))
case .notStarted:
let partOffset: Int64 = 0
let partSize = min(resourceData.size - partOffset, self.defaultPartSize)
let partIndex = Int(partOffset / self.defaultPartSize)
let fileData: Data?
switch resourceData {
case let .resourceData(data):
fileData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: [.alwaysMapped])
case let .data(data):
fileData = data
}
if let fileData = fileData {
let partData = self.state.transformHeader(data: fileData.subdata(in: Int(partOffset) ..< Int(partOffset + partSize)))
var currentBigTotalParts: Int? = nil
if self.bigParts {
let totalParts = (resourceData.size / self.defaultPartSize) + (resourceData.size % self.defaultPartSize == 0 ? 0 : 1)
currentBigTotalParts = Int(totalParts)
}
self.headerPartState = .uploading
let part = self.uploadPart(UploadPart(fileId: self.fileId, index: partIndex, data: partData, bigTotalParts: currentBigTotalParts, bigPart: self.bigParts))
|> deliverOn(self.queue)
self.uploadingParts[0] = (partSize, part.startStrict(error: { [weak self] _ in
self?.completed(nil)
}, completed: { [weak self] in
if let strongSelf = self {
strongSelf.uploadingParts.removeValue(forKey: 0)?.1.dispose()
strongSelf.headerPartState = .ready
strongSelf.checkState()
}
}))
}
case .uploading:
break
}
} else if let resourceData = self.resourceData, self.state.aesKey.isEmpty || resourceData.complete {
while uploadingParts.count < self.parallelParts {
switch self.headerPartState {
case .notStarted:
if self.committedOffset == 0, !resourceData.complete {
self.committedOffset += self.defaultPartSize
}
case .ready, .uploading:
break
}
var nextOffset = self.committedOffset
for (offset, (size, _)) in self.uploadingParts {
nextOffset = max(nextOffset, offset + size)
}
for (offset, partSize) in self.uploadedParts {
nextOffset = max(nextOffset, offset + partSize)
}
let partOffset = nextOffset
let partSize = min(resourceData.size - partOffset, self.defaultPartSize)
if nextOffset < resourceData.size && partSize > 0 && (resourceData.complete || partSize == self.defaultPartSize) {
let partIndex = Int(partOffset / self.defaultPartSize)
let partData: Data?
switch resourceData {
case let .resourceData(data):
if let file = ManagedFile(queue: nil, path: data.path, mode: .read) {
let _ = file.seek(position: Int64(partOffset))
let data = file.readData(count: Int(partSize))
if data.count == partSize {
partData = data
} else {
partData = nil
}
} else {
partData = nil
}
case let .data(data):
if data.count >= partOffset + partSize {
partData = data.subdata(in: Int(partOffset) ..< Int(partOffset + partSize))
} else {
partData = nil
}
}
if let partData = partData {
let partData = self.state.transform(data: partData)
var currentBigTotalParts = self.bigTotalParts
if self.bigParts && resourceData.complete && partOffset + partSize == resourceData.size {
currentBigTotalParts = Int(resourceData.size / self.defaultPartSize) + (resourceData.size % self.defaultPartSize == 0 ? 0 : 1)
}
let part = self.uploadPart(UploadPart(fileId: self.fileId, index: partIndex, data: partData, bigTotalParts: currentBigTotalParts, bigPart: self.bigParts))
|> deliverOn(self.queue)
if partIndex == 0 {
switch self.headerPartState {
case .notStarted:
self.headerPartState = .uploading
case .ready, .uploading:
break
}
}
self.uploadingParts[nextOffset] = (partSize, part.startStrict(error: { [weak self] _ in
self?.completed(nil)
}, completed: { [weak self] in
if let strongSelf = self {
strongSelf.uploadingParts.removeValue(forKey: nextOffset)?.1.dispose()
strongSelf.uploadedParts[partOffset] = partSize
if partIndex == 0 {
strongSelf.headerPartState = .ready
}
strongSelf.checkState()
}
}))
} else {
self.completed(nil)
}
} else {
break
}
}
}
}
}
enum MultipartUploadResult {
case progress(Float)
case inputFile(Api.InputFile)
case inputSecretFile(Api.InputEncryptedFile, Int64, SecretFileEncryptionKey)
}
public enum MultipartUploadSource {
case resource(MediaResourceReference)
case data(Data)
case custom(Signal<MediaResourceData, NoError>)
case tempFile(TempBoxFile)
}
enum MultipartUploadError {
case generic
}
func multipartUpload(network: Network, postbox: Postbox, source: MultipartUploadSource, encrypt: Bool, tag: MediaResourceFetchTag?, hintFileSize: Int64?, hintFileIsLarge: Bool, forceNoBigParts: Bool, useLargerParts: Bool = false, increaseParallelParts: Bool = false, useMultiplexedRequests: Bool = true, useCompression: Bool = false) -> Signal<MultipartUploadResult, MultipartUploadError> {
enum UploadInterface {
case download(Download)
case multiplexed(manager: MultiplexedRequestManager, datacenterId: Int, consumerId: Int64)
}
let uploadInterface: Signal<UploadInterface, NoError>
if useMultiplexedRequests {
uploadInterface = .single(.multiplexed(manager: network.multiplexedRequestManager, datacenterId: network.datacenterId, consumerId: Int64.random(in: Int64.min ... Int64.max)))
} else {
uploadInterface = network.upload(tag: tag)
|> map { download -> UploadInterface in
return .download(download)
}
}
return uploadInterface
|> mapToSignalPromotingError { uploadInterface -> Signal<MultipartUploadResult, MultipartUploadError> in
return Signal { subscriber in
var encryptionKey: SecretFileEncryptionKey?
if encrypt {
var aesKey = Data()
aesKey.count = 32
var aesIv = Data()
aesIv.count = 32
aesKey.withUnsafeMutableBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!
arc4random_buf(bytes, 32)
}
aesIv.withUnsafeMutableBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!
arc4random_buf(bytes, 32)
}
encryptionKey = SecretFileEncryptionKey(aesKey: aesKey, aesIv: aesIv)
}
let dataSignal: Signal<MultipartUploadData, NoError>
let headerSize: Int32
let fetchedResource: Signal<Void, FetchResourceError>
switch source {
case let .resource(resource):
dataSignal = postbox.mediaBox.resourceData(resource.resource, option: .incremental(waitUntilFetchStatus: true)) |> map { MultipartUploadData.resourceData($0) }
headerSize = resource.resource.headerSize
fetchedResource = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: .other, userContentType: .other, reference: resource)
|> map { _ in }
case let .tempFile(file):
if let size = fileSize(file.path) {
dataSignal = .single(.resourceData(MediaResourceData(path: file.path, offset: 0, size: size, complete: true)))
headerSize = 0
fetchedResource = .complete()
} else {
subscriber.putError(.generic)
return EmptyDisposable
}
case let .data(data):
dataSignal = .single(.data(data))
headerSize = 0
fetchedResource = .complete()
case let .custom(signal):
headerSize = 1024
dataSignal = signal
|> map { data in
print("**data \(data) \(data.complete)")
return MultipartUploadData.resourceData(data)
}
fetchedResource = .complete()
}
let onFloodWaitError: (String) -> Void = { [weak network] error in
guard let network else {
return
}
if error.hasPrefix("FLOOD_PREMIUM_WAIT") {
network.addNetworkSpeedLimitedEvent(event: .upload)
}
}
// TODO(swiftgram): Change other variables for uploadSpeedBoost
let manager = MultipartUploadManager(headerSize: headerSize, data: dataSignal, encryptionKey: encryptionKey, hintFileSize: hintFileSize, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: forceNoBigParts, useLargerParts: useLargerParts || SGSimpleSettings.shared.uploadSpeedBoost, increaseParallelParts: increaseParallelParts || SGSimpleSettings.shared.uploadSpeedBoost, uploadPart: { part in
switch uploadInterface {
case let .download(download):
return download.uploadPart(fileId: part.fileId, index: part.index, data: part.data, asBigPart: part.bigPart, bigTotalParts: part.bigTotalParts, useCompression: useCompression, onFloodWaitError: onFloodWaitError)
case let .multiplexed(multiplexed, datacenterId, consumerId):
return Download.uploadPart(multiplexedManager: multiplexed, datacenterId: datacenterId, consumerId: consumerId, tag: nil, fileId: part.fileId, index: part.index, data: part.data, asBigPart: part.bigPart, bigTotalParts: part.bigTotalParts, useCompression: useCompression, onFloodWaitError: onFloodWaitError)
}
}, progress: { progress in
subscriber.putNext(.progress(progress))
}, completed: { result in
if let result = result {
if let encryptionKey = encryptionKey {
let keyDigest = md5(encryptionKey.aesKey + encryptionKey.aesIv)
var fingerprint: Int32 = 0
keyDigest.withUnsafeBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
withUnsafeMutableBytes(of: &fingerprint, { ptr -> Void in
let uintPtr = ptr.baseAddress!.assumingMemoryBound(to: UInt8.self)
uintPtr[0] = bytes[0] ^ bytes[4]
uintPtr[1] = bytes[1] ^ bytes[5]
uintPtr[2] = bytes[2] ^ bytes[6]
uintPtr[3] = bytes[3] ^ bytes[7]
})
}
if let _ = result.bigTotalParts {
let inputFile = Api.InputEncryptedFile.inputEncryptedFileBigUploaded(.init(id: result.id, parts: result.partCount, keyFingerprint: fingerprint))
subscriber.putNext(.inputSecretFile(inputFile, result.size, encryptionKey))
} else {
let inputFile = Api.InputEncryptedFile.inputEncryptedFileUploaded(.init(id: result.id, parts: result.partCount, md5Checksum: result.md5Digest, keyFingerprint: fingerprint))
subscriber.putNext(.inputSecretFile(inputFile, result.size, encryptionKey))
}
} else {
if let _ = result.bigTotalParts {
let inputFile = Api.InputFile.inputFileBig(.init(id: result.id, parts: result.partCount, name: "file.jpg"))
subscriber.putNext(.inputFile(inputFile))
} else {
let inputFile = Api.InputFile.inputFile(.init(id: result.id, parts: result.partCount, name: "file.jpg", md5Checksum: result.md5Digest))
subscriber.putNext(.inputFile(inputFile))
}
}
subscriber.putCompletion()
} else {
subscriber.putError(.generic)
}
})
manager.start()
let fetchedResourceDisposable = fetchedResource.start(error: { _ in
subscriber.putError(.generic)
})
return ActionDisposable {
manager.cancel()
fetchedResourceDisposable.dispose()
}
}
}
}
@@ -0,0 +1,400 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import MtProtoKit
enum MultiplexedRequestTarget: Equatable, Hashable, CustomStringConvertible {
case main(Int)
case cdn(Int)
var description: String {
switch self {
case let .main(id):
return "dc\(id)"
case let .cdn(id):
return "cdn\(id)"
}
}
}
private struct MultiplexedRequestTargetKey: Equatable, Hashable {
let target: MultiplexedRequestTarget
let continueInBackground: Bool
}
private final class RequestData {
let id: Int32
let consumerId: Int64
let resourceId: String?
let target: MultiplexedRequestTarget
let functionDescription: FunctionDescription
let payload: Buffer
let tag: MediaResourceFetchTag?
let continueInBackground: Bool
let automaticFloodWait: Bool
let onFloodWaitError: ((String) -> Void)?
let expectedResponseSize: Int32?
let deserializeResponse: (Buffer) -> Any?
let completed: (Any, NetworkResponseInfo) -> Void
let error: (MTRpcError, Double) -> Void
init(id: Int32, consumerId: Int64, resourceId: String?, target: MultiplexedRequestTarget, functionDescription: FunctionDescription, payload: Buffer, tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, onFloodWaitError: ((String) -> Void)?, expectedResponseSize: Int32?, deserializeResponse: @escaping (Buffer) -> Any?, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) {
self.id = id
self.consumerId = consumerId
self.resourceId = resourceId
self.target = target
self.functionDescription = functionDescription
self.tag = tag
self.continueInBackground = continueInBackground
self.automaticFloodWait = automaticFloodWait
self.onFloodWaitError = onFloodWaitError
self.expectedResponseSize = expectedResponseSize
self.payload = payload
self.deserializeResponse = deserializeResponse
self.completed = completed
self.error = error
}
}
private final class ExecutingRequestData {
let requestId: Int32
let disposable: Disposable
init(requestId: Int32, disposable: Disposable) {
self.requestId = requestId
self.disposable = disposable
}
}
private final class RequestTargetContext {
let id: Int32
let worker: Download
var requests: [ExecutingRequestData]
init(id: Int32, worker: Download) {
self.id = id
self.worker = worker
self.requests = []
}
}
private struct MultiplexedRequestTargetTimerKey: Equatable, Hashable {
let key: MultiplexedRequestTargetKey
let id: Int32
}
private typealias SignalKitTimer = SwiftSignalKit.Timer
struct NetworkResponseInfo {
var timestamp: Double
var networkType: NetworkStatsContext.NetworkType
var networkDuration: Double
}
private final class MultiplexedRequestManagerContext {
final class RequestManagerPriorityContext {
var resourceCounters: [String: Bag<Int>] = [:]
}
private let queue: Queue
private let takeWorker: (MultiplexedRequestTarget, MediaResourceFetchTag?, Bool) -> Download?
private let priorityContext = RequestManagerPriorityContext()
private var queuedRequests: [RequestData] = []
private var nextId: Int32 = 0
private var targetContexts: [MultiplexedRequestTargetKey: [RequestTargetContext]] = [:]
private var emptyTargetDisposables: [MultiplexedRequestTargetTimerKey: Disposable] = [:]
init(queue: Queue, takeWorker: @escaping (MultiplexedRequestTarget, MediaResourceFetchTag?, Bool) -> Download?) {
self.queue = queue
self.takeWorker = takeWorker
}
deinit {
for targetContextList in self.targetContexts.values {
for targetContext in targetContextList {
for request in targetContext.requests {
request.disposable.dispose()
}
}
}
for disposable in emptyTargetDisposables.values {
disposable.dispose()
}
}
func pushPriority(resourceId: String, priority: Int) -> Disposable {
let queue = self.queue
let counters: Bag<Int>
if let current = self.priorityContext.resourceCounters[resourceId] {
counters = current
} else {
counters = Bag()
self.priorityContext.resourceCounters[resourceId] = counters
}
let index = counters.add(priority)
self.updateState()
return ActionDisposable { [weak self, weak counters] in
queue.async {
guard let `self` = self else {
return
}
if let current = self.priorityContext.resourceCounters[resourceId], current === counters {
current.remove(index)
if current.isEmpty {
self.priorityContext.resourceCounters.removeValue(forKey: resourceId)
}
self.updateState()
}
}
}
}
func request(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, (Buffer) -> Any?), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool, onFloodWaitError: ((String) -> Void)? = nil, expectedResponseSize: Int32?, completed: @escaping (Any, NetworkResponseInfo) -> Void, error: @escaping (MTRpcError, Double) -> Void) -> Disposable {
let targetKey = MultiplexedRequestTargetKey(target: target, continueInBackground: continueInBackground)
let requestId = self.nextId
self.nextId += 1
self.queuedRequests.append(RequestData(id: requestId, consumerId: consumerId, resourceId: resourceId, target: target, functionDescription: data.0, payload: data.1, tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, onFloodWaitError: onFloodWaitError, expectedResponseSize: expectedResponseSize, deserializeResponse: { buffer in
return data.2(buffer)
}, completed: { result, info in
completed(result, info)
}, error: { e, timestamp in
error(e, timestamp)
}))
self.updateState()
let queue = self.queue
return ActionDisposable { [weak self] in
queue.async {
guard let strongSelf = self else {
return
}
for i in 0 ..< strongSelf.queuedRequests.count {
if strongSelf.queuedRequests[i].id == requestId {
strongSelf.queuedRequests.remove(at: i)
break
}
}
if strongSelf.targetContexts[targetKey] != nil {
outer: for targetContext in strongSelf.targetContexts[targetKey]! {
for i in 0 ..< targetContext.requests.count {
if targetContext.requests[i].requestId == requestId {
targetContext.requests[i].disposable.dispose()
targetContext.requests.remove(at: i)
break outer
}
}
}
}
strongSelf.updateState()
}
}
}
private func updateState() {
let maxRequestsPerWorker = 3
let maxWorkersPerTarget = 4
for request in self.queuedRequests.sorted(by: { lhs, rhs in
let lhsPriority = lhs.resourceId.flatMap { id in
if let counters = self.priorityContext.resourceCounters[id] {
return counters.copyItems().max() ?? 0
} else {
return 0
}
} ?? 0
let rhsPriority = rhs.resourceId.flatMap { id in
if let counters = self.priorityContext.resourceCounters[id] {
return counters.copyItems().max() ?? 0
} else {
return 0
}
} ?? 0
if lhsPriority != rhsPriority {
return lhsPriority > rhsPriority
}
return lhs.id < rhs.id
}) {
let targetKey = MultiplexedRequestTargetKey(target: request.target, continueInBackground: request.continueInBackground)
if self.targetContexts[targetKey] == nil {
self.targetContexts[targetKey] = []
}
var selectedContext: RequestTargetContext?
for targetContext in self.targetContexts[targetKey]! {
if targetContext.requests.count < maxRequestsPerWorker {
selectedContext = targetContext
break
}
}
if selectedContext == nil && self.targetContexts[targetKey]!.count < maxWorkersPerTarget {
if let worker = self.takeWorker(request.target, request.tag, request.continueInBackground) {
let contextId = self.nextId
self.nextId += 1
let targetContext = RequestTargetContext(id: contextId, worker: worker)
self.targetContexts[targetKey]!.append(targetContext)
selectedContext = targetContext
} else {
Logger.shared.log("MultiplexedRequestManager", "couldn't take worker")
}
}
if let selectedContext = selectedContext {
let disposable = MetaDisposable()
let requestId = request.id
selectedContext.requests.append(ExecutingRequestData(requestId: requestId, disposable: disposable))
let queue = self.queue
disposable.set(selectedContext.worker.rawRequest((request.functionDescription, request.payload, request.deserializeResponse), automaticFloodWait: request.automaticFloodWait, onFloodWaitError: request.onFloodWaitError, expectedResponseSize: request.expectedResponseSize).start(next: { [weak self, weak selectedContext] result, info in
queue.async {
guard let strongSelf = self else {
return
}
if let selectedContext = selectedContext {
for i in 0 ..< selectedContext.requests.count {
if selectedContext.requests[i].requestId == requestId {
selectedContext.requests.remove(at: i)
break
}
}
}
request.completed(result, info)
strongSelf.updateState()
}
}, error: { [weak self, weak selectedContext] error, timestamp in
queue.async {
guard let strongSelf = self else {
return
}
request.error(error, timestamp)
if let selectedContext = selectedContext {
for i in 0 ..< selectedContext.requests.count {
if selectedContext.requests[i].requestId == requestId {
selectedContext.requests.remove(at: i)
break
}
}
}
strongSelf.updateState()
}
}))
if let requestIndex = self.queuedRequests.firstIndex(where: { $0 === request }) {
self.queuedRequests.remove(at: requestIndex)
}
continue
}
}
self.checkEmptyContexts()
}
private func checkEmptyContexts() {
for (targetKey, contexts) in self.targetContexts {
for context in contexts {
let key = MultiplexedRequestTargetTimerKey(key: targetKey, id: context.id)
if context.requests.isEmpty {
if self.emptyTargetDisposables[key] == nil {
let disposable = MetaDisposable()
self.emptyTargetDisposables[key] = disposable
disposable.set((Signal<Never, NoError>.complete()
|> delay(20 * 60, queue: self.queue)
|> deliverOn(self.queue)).start(completed: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.emptyTargetDisposables.removeValue(forKey: key)
if strongSelf.targetContexts[targetKey] != nil {
for i in 0 ..< strongSelf.targetContexts[targetKey]!.count {
if strongSelf.targetContexts[targetKey]![i].id == key.id {
strongSelf.targetContexts[targetKey]!.remove(at: i)
break
}
}
}
}))
}
} else {
if let disposable = self.emptyTargetDisposables[key] {
disposable.dispose()
self.emptyTargetDisposables.removeValue(forKey: key)
}
}
}
}
}
}
final class MultiplexedRequestManager {
private let queue = Queue()
private let context: QueueLocalObject<MultiplexedRequestManagerContext>
init(takeWorker: @escaping (MultiplexedRequestTarget, MediaResourceFetchTag?, Bool) -> Download?) {
let queue = self.queue
self.context = QueueLocalObject(queue: self.queue, generate: {
return MultiplexedRequestManagerContext(queue: queue, takeWorker: takeWorker)
})
}
func pushPriority(resourceId: String, priority: Int) -> Disposable {
let disposable = MetaDisposable()
self.context.with { context in
disposable.set(context.pushPriority(resourceId: resourceId, priority: priority))
}
return disposable
}
func request<T>(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil, expectedResponseSize: Int32?) -> Signal<T, MTRpcError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.context.with { context in
disposable.set(context.request(to: target, consumerId: consumerId, resourceId: resourceId, data: (data.0, data.1, { buffer in
return data.2.parse(buffer)
}), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, onFloodWaitError: onFloodWaitError, expectedResponseSize: expectedResponseSize, completed: { result, _ in
if let result = result as? T {
subscriber.putNext(result)
subscriber.putCompletion()
} else {
subscriber.putError(MTRpcError(errorCode: 500, errorDescription: "TL_VERIFICATION_ERROR"))
}
}, error: { error, _ in
subscriber.putError(error)
}))
}
return disposable
}
}
func requestWithAdditionalInfo<T>(to target: MultiplexedRequestTarget, consumerId: Int64, resourceId: String?, data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), tag: MediaResourceFetchTag?, continueInBackground: Bool, automaticFloodWait: Bool = true, onFloodWaitError: ((String) -> Void)? = nil, expectedResponseSize: Int32?) -> Signal<(T, NetworkResponseInfo), (MTRpcError, Double)> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.context.with { context in
disposable.set(context.request(to: target, consumerId: consumerId, resourceId: resourceId, data: (data.0, data.1, { buffer in
return data.2.parse(buffer)
}), tag: tag, continueInBackground: continueInBackground, automaticFloodWait: automaticFloodWait, onFloodWaitError: onFloodWaitError, expectedResponseSize: expectedResponseSize, completed: { result, info in
if let result = result as? T {
subscriber.putNext((result, info))
subscriber.putCompletion()
} else {
subscriber.putError((MTRpcError(errorCode: 500, errorDescription: "TL_VERIFICATION_ERROR"), info.timestamp))
}
}, error: { error, timestamp in
subscriber.putError((error, timestamp))
}))
}
return disposable
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,356 @@
import Foundation
import Network
import MtProtoKit
import SwiftSignalKit
@available(iOS 12.0, macOS 14.0, *)
final class NetworkFrameworkTcpConnectionInterface: NSObject, MTTcpConnectionInterface {
private struct ReadRequest {
let length: Int
let tag: Int
}
private final class ExecutingReadRequest {
let request: ReadRequest
var data: Data
var readyLength: Int = 0
init(request: ReadRequest) {
self.request = request
self.data = Data(count: request.length)
}
}
private final class Impl {
private let queue: Queue
private weak var delegate: MTTcpConnectionInterfaceDelegate?
private let delegateQueue: DispatchQueue
private let requestChunkLength: Int
private var connection: NWConnection?
private var reportedDisconnection: Bool = false
private var currentInterfaceIsWifi: Bool = true
private var connectTimeoutTimer: SwiftSignalKit.Timer?
private var usageCalculationInfo: MTNetworkUsageCalculationInfo?
private var networkUsageManager: MTNetworkUsageManager?
private var readRequests: [ReadRequest] = []
private var currentReadRequest: ExecutingReadRequest?
init(
queue: Queue,
delegate: MTTcpConnectionInterfaceDelegate,
delegateQueue: DispatchQueue
) {
self.queue = queue
self.delegate = delegate
self.delegateQueue = delegateQueue
self.requestChunkLength = 256 * 1024
}
deinit {
}
func setUsageCalculationInfo(_ usageCalculationInfo: MTNetworkUsageCalculationInfo?) {
if self.usageCalculationInfo !== usageCalculationInfo {
self.usageCalculationInfo = usageCalculationInfo
if let usageCalculationInfo = usageCalculationInfo {
self.networkUsageManager = MTNetworkUsageManager(info: usageCalculationInfo)
} else {
self.networkUsageManager = nil
}
}
}
func connect(host: String, port: UInt16, timeout: Double) {
if self.connection != nil {
assertionFailure("A connection already exists")
return
}
let host = NWEndpoint.Host(host)
let port = NWEndpoint.Port(rawValue: port)!
let tcpOptions = NWProtocolTCP.Options()
tcpOptions.noDelay = true
tcpOptions.enableKeepalive = true
tcpOptions.keepaliveIdle = 5
tcpOptions.keepaliveCount = 2
tcpOptions.keepaliveInterval = 5
tcpOptions.enableFastOpen = true
let parameters = NWParameters(tls: nil, tcp: tcpOptions)
let connection = NWConnection(host: host, port: port, using: parameters)
self.connection = connection
let queue = self.queue
connection.stateUpdateHandler = { [weak self] state in
queue.async {
self?.stateUpdated(state: state)
}
}
connection.pathUpdateHandler = { [weak self] path in
queue.async {
guard let self = self else {
return
}
if path.usesInterfaceType(.cellular) {
self.currentInterfaceIsWifi = false
} else {
self.currentInterfaceIsWifi = true
}
}
}
connection.viabilityUpdateHandler = { [weak self] isViable in
queue.async {
guard let self = self else {
return
}
if !isViable {
self.cancelWithError(error: nil)
}
}
}
/*connection.betterPathUpdateHandler = { [weak self] hasBetterPath in
queue.async {
guard let self = self else {
return
}
if hasBetterPath {
self.cancelWithError(error: nil)
}
}
}*/
self.connectTimeoutTimer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { [weak self] in
guard let self = self else {
return
}
self.connectTimeoutTimer = nil
self.cancelWithError(error: nil)
}, queue: self.queue)
self.connectTimeoutTimer?.start()
connection.start(queue: self.queue.queue)
self.processReadRequests()
}
private func stateUpdated(state: NWConnection.State) {
switch state {
case .ready:
if let path = self.connection?.currentPath {
if path.usesInterfaceType(.cellular) {
self.currentInterfaceIsWifi = false
} else {
self.currentInterfaceIsWifi = true
}
}
if let connectTimeoutTimer = connectTimeoutTimer {
self.connectTimeoutTimer = nil
connectTimeoutTimer.invalidate()
}
let delegate = self.delegate
self.delegateQueue.async { [weak delegate] in
if let delegate = delegate {
delegate.connectionInterfaceDidConnect()
}
}
case let .failed(error):
self.cancelWithError(error: error)
default:
break
}
}
func write(data: Data) {
guard let connection = self.connection else {
Logger.shared.log("NetworkFrameworkTcpConnectionInterface", "write called while connection == nil")
return
}
connection.send(content: data, completion: .contentProcessed({ _ in
}))
self.networkUsageManager?.addOutgoingBytes(UInt(data.count), interface: self.currentInterfaceIsWifi ? MTNetworkUsageManagerInterfaceOther : MTNetworkUsageManagerInterfaceWWAN)
}
func read(length: Int, timeout: Double, tag: Int) {
self.readRequests.append(NetworkFrameworkTcpConnectionInterface.ReadRequest(length: length, tag: tag))
self.processReadRequests()
}
private func processReadRequests() {
if self.currentReadRequest != nil {
return
}
if self.readRequests.isEmpty {
return
}
let readRequest = self.readRequests.removeFirst()
let currentReadRequest = ExecutingReadRequest(request: readRequest)
self.currentReadRequest = currentReadRequest
self.processCurrentRead()
}
private func processCurrentRead() {
guard let currentReadRequest = self.currentReadRequest else {
return
}
guard let connection = self.connection else {
print("Connection not ready")
return
}
let requestChunkLength = min(self.requestChunkLength, currentReadRequest.request.length - currentReadRequest.readyLength)
if requestChunkLength == 0 {
self.currentReadRequest = nil
let delegate = self.delegate
let currentInterfaceIsWifi = self.currentInterfaceIsWifi
self.delegateQueue.async { [weak delegate] in
if let delegate = delegate {
delegate.connectionInterfaceDidRead(currentReadRequest.data, withTag: currentReadRequest.request.tag, networkType: currentInterfaceIsWifi ? 0 : 1)
}
}
self.processReadRequests()
} else {
connection.receive(minimumIncompleteLength: requestChunkLength, maximumLength: requestChunkLength, completion: { [weak self] data, context, isComplete, error in
guard let self = self, let currentReadRequest = self.currentReadRequest else {
return
}
if let data = data {
self.networkUsageManager?.addIncomingBytes(UInt(data.count), interface: self.currentInterfaceIsWifi ? MTNetworkUsageManagerInterfaceOther : MTNetworkUsageManagerInterfaceWWAN)
if data.count != 0 && data.count <= currentReadRequest.request.length - currentReadRequest.readyLength {
currentReadRequest.data.withUnsafeMutableBytes { currentBuffer in
guard let currentBytes = currentBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return
}
data.copyBytes(to: currentBytes.advanced(by: currentReadRequest.readyLength), count: data.count)
}
currentReadRequest.readyLength += data.count
let tag = currentReadRequest.request.tag
let readCount = data.count
let delegate = self.delegate
self.delegateQueue.async { [weak delegate] in
if let delegate = delegate {
delegate.connectionInterfaceDidReadPartialData(ofLength: UInt(readCount), tag: tag)
}
}
self.processCurrentRead()
} else {
self.cancelWithError(error: error)
}
if isComplete && data.count == 0 {
self.cancelWithError(error: nil)
}
} else {
self.cancelWithError(error: error)
}
})
}
}
private func cancelWithError(error: Error?) {
if let connectTimeoutTimer = self.connectTimeoutTimer {
self.connectTimeoutTimer = nil
connectTimeoutTimer.invalidate()
}
if !self.reportedDisconnection {
self.reportedDisconnection = true
let delegate = self.delegate
self.delegateQueue.async { [weak delegate] in
if let delegate = delegate {
delegate.connectionInterfaceDidDisconnectWithError(error)
}
}
}
if let connection = self.connection {
self.connection = nil
connection.cancel()
}
}
func disconnect() {
self.cancelWithError(error: nil)
}
func resetDelegate() {
self.delegate = nil
}
}
private static let sharedQueue = Queue(name: "NetworkFrameworkTcpConnectionInteface")
private let queue: Queue
private let impl: QueueLocalObject<Impl>
init(delegate: MTTcpConnectionInterfaceDelegate, delegateQueue: DispatchQueue) {
let queue = NetworkFrameworkTcpConnectionInterface.sharedQueue
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: {
return Impl(queue: queue, delegate: delegate, delegateQueue: delegateQueue)
})
}
func setGetLogPrefix(_ getLogPrefix: (() -> String)?) {
}
func setUsageCalculationInfo(_ usageCalculationInfo: MTNetworkUsageCalculationInfo?) {
self.impl.with { impl in
impl.setUsageCalculationInfo(usageCalculationInfo)
}
}
func connect(toHost inHost: String, onPort port: UInt16, viaInterface inInterface: String?, withTimeout timeout: TimeInterval, error errPtr: NSErrorPointer) -> Bool {
self.impl.with { impl in
impl.connect(host: inHost, port: port, timeout: timeout)
}
return true
}
func write(_ data: Data) {
self.impl.with { impl in
impl.write(data: data)
}
}
func readData(toLength length: UInt, withTimeout timeout: TimeInterval, tag: Int) {
self.impl.with { impl in
impl.read(length: Int(length), timeout: timeout, tag: tag)
}
}
func disconnect() {
self.impl.with { impl in
impl.disconnect()
}
}
func resetDelegate() {
self.impl.with { impl in
impl.resetDelegate()
}
}
}
@@ -0,0 +1,124 @@
import Foundation
import SwiftSignalKit
import Postbox
final class NetworkStatsContext {
enum NetworkType: Int32 {
case wifi = 0
case cellular = 1
}
struct DownloadEvent {
let networkType: NetworkType
let datacenterId: Int32
let size: Double
let networkDuration: Double
let issueDuration: Double
init(
networkType: NetworkType,
datacenterId: Int32,
size: Double,
networkDuration: Double,
issueDuration: Double
) {
self.networkType = networkType
self.datacenterId = datacenterId
self.size = size
self.networkDuration = networkDuration
self.issueDuration = issueDuration
}
}
private struct TargetKey: Hashable {
let networkType: NetworkType
let datacenterId: Int32
init(networkType: NetworkType, datacenterId: Int32) {
self.networkType = networkType
self.datacenterId = datacenterId
}
}
private final class AverageStats {
var networkBps: Double = 0.0
var issueDuration: Double = 0.0
var networkDelay: Double = 0.0
var count: Int = 0
var size: Int64 = 0
}
private final class Impl {
let queue: Queue
weak var postbox: Postbox?
var averageTargetStats: [TargetKey: AverageStats] = [:]
init(queue: Queue, postbox: Postbox?) {
self.queue = queue
self.postbox = postbox
}
func add(downloadEvents: [DownloadEvent]) {
for event in downloadEvents {
if event.networkDuration == 0.0 {
continue
}
let targetKey = TargetKey(networkType: event.networkType, datacenterId: event.datacenterId)
let averageStats: AverageStats
if let current = self.averageTargetStats[targetKey] {
averageStats = current
} else {
averageStats = AverageStats()
self.averageTargetStats[targetKey] = averageStats
}
averageStats.count += 1
averageStats.issueDuration += event.issueDuration
averageStats.networkDelay += event.issueDuration - event.networkDuration
averageStats.networkBps += event.size / event.networkDuration
averageStats.size += Int64(event.size)
}
self.maybeFlushStats()
}
private func maybeFlushStats() {
var removeKeys: [TargetKey] = []
for (targetKey, averageStats) in self.averageTargetStats {
if averageStats.count >= 1000 || averageStats.size >= 4 * 1024 * 1024 {
if let postbox = self.postbox {
addAppLogEvent(postbox: postbox, type: "download", data: .dictionary([
"n": .number(Double(targetKey.networkType.rawValue)),
"d": .number(Double(targetKey.datacenterId)),
"b": .number(averageStats.networkBps / Double(averageStats.count)),
"nd": .number(averageStats.networkDelay / Double(averageStats.count))
]))
}
removeKeys.append(targetKey)
}
}
for key in removeKeys {
self.averageTargetStats.removeValue(forKey: key)
}
}
}
private static let sharedQueue = Queue(name: "NetworkStatsContext")
private let queue: Queue
private let impl: QueueLocalObject<Impl>
init(postbox: Postbox) {
let queue = NetworkStatsContext.sharedQueue
self.queue = queue
self.impl = QueueLocalObject(queue: queue, generate: { [weak postbox] in
return Impl(queue: queue, postbox: postbox)
})
}
func add(downloadEvents: [DownloadEvent]) {
self.impl.with { impl in
impl.add(downloadEvents: downloadEvents)
}
}
}
@@ -0,0 +1,163 @@
import Foundation
import SwiftSignalKit
import MtProtoKit
import Reachability
#if os(iOS)
import CoreTelephony
#endif
#if os(iOS)
public enum CellularNetworkType {
case unknown
case gprs
case edge
case thirdG
case lte
}
extension CellularNetworkType {
init(accessTechnology: String) {
switch accessTechnology {
case CTRadioAccessTechnologyGPRS:
self = .gprs
case CTRadioAccessTechnologyEdge, CTRadioAccessTechnologyCDMA1x:
self = .edge
case CTRadioAccessTechnologyLTE:
self = .lte
case CTRadioAccessTechnologyWCDMA, CTRadioAccessTechnologyHSDPA, CTRadioAccessTechnologyHSUPA, CTRadioAccessTechnologyCDMAEVDORev0, CTRadioAccessTechnologyCDMAEVDORevA, CTRadioAccessTechnologyCDMAEVDORevB, CTRadioAccessTechnologyeHRPD:
self = .thirdG
default:
self = .unknown
}
}
}
#endif
public enum NetworkType: Equatable {
case none
case wifi
#if os(iOS)
case cellular(CellularNetworkType)
#endif
}
extension NetworkType {
#if os(iOS)
init(internalType: Reachability.NetworkType, cellularType: CellularNetworkType) {
switch internalType {
case .none:
self = .none
case .wifi:
self = .wifi
case .cellular:
self = .cellular(cellularType)
}
}
#else
init(internalType: Reachability.NetworkType) {
switch internalType {
case .none:
self = .none
case .wifi, .cellular:
self = .wifi
}
}
#endif
}
private final class NetworkTypeManagerImpl {
let queue: Queue
let updated: (NetworkType) -> Void
var networkTypeDisposable: Disposable?
var currentNetworkType: Reachability.NetworkType?
var networkType: NetworkType?
#if os(iOS)
var currentCellularType: CellularNetworkType
var cellularTypeObserver: NSObjectProtocol?
#endif
init(queue: Queue, updated: @escaping (NetworkType) -> Void) {
self.queue = queue
self.updated = updated
#if os(iOS)
let accessTechnology = CTTelephonyNetworkInfo().serviceCurrentRadioAccessTechnology?.values.first ?? ""
self.currentCellularType = CellularNetworkType(accessTechnology: accessTechnology)
self.cellularTypeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.CTServiceRadioAccessTechnologyDidChange, object: nil, queue: nil, using: { [weak self] notification in
queue.async {
guard let strongSelf = self else {
return
}
let accessTechnology = CTTelephonyNetworkInfo().serviceCurrentRadioAccessTechnology?.values.first ?? ""
let cellularType = CellularNetworkType(accessTechnology: accessTechnology)
if strongSelf.currentCellularType != cellularType {
strongSelf.currentCellularType = cellularType
if let currentNetworkType = strongSelf.currentNetworkType {
let networkType = NetworkType(internalType: currentNetworkType, cellularType: cellularType)
if strongSelf.networkType != networkType {
strongSelf.networkType = networkType
strongSelf.updated(networkType)
}
}
}
}
})
#endif
let networkTypeDisposable = MetaDisposable()
self.networkTypeDisposable = networkTypeDisposable
networkTypeDisposable.set((Reachability.networkType
|> deliverOn(queue)).start(next: { [weak self] networkStatus in
guard let strongSelf = self else {
return
}
if strongSelf.currentNetworkType != networkStatus {
strongSelf.currentNetworkType = networkStatus
let networkType: NetworkType
#if os(iOS)
networkType = NetworkType(internalType: networkStatus, cellularType: strongSelf.currentCellularType)
#else
networkType = NetworkType(internalType: networkStatus)
#endif
if strongSelf.networkType != networkType {
strongSelf.networkType = networkType
updated(networkType)
}
}
}))
}
func stop() {
self.networkTypeDisposable?.dispose()
#if os(iOS)
if let observer = self.cellularTypeObserver {
NotificationCenter.default.removeObserver(observer, name: NSNotification.Name.CTServiceRadioAccessTechnologyDidChange, object: nil)
}
#endif
}
}
func currentNetworkType() -> Signal<NetworkType, NoError> {
return Signal { subscriber in
let queue = Queue()
let disposable = MetaDisposable()
queue.async {
let impl = QueueLocalObject(queue: queue, generate: {
return NetworkTypeManagerImpl(queue: queue, updated: { value in
subscriber.putNext(value)
})
})
disposable.set(ActionDisposable {
impl.with({ impl in
impl.stop()
})
})
}
return disposable
}
}
@@ -0,0 +1,129 @@
import Foundation
import SwiftSignalKit
import MtProtoKit
public enum ProxyServerStatus: Equatable {
case checking
case notAvailable
case available(Double)
}
private final class ProxyServerItemContext {
private var disposable: Disposable?
var value: ProxyServerStatus = .checking
init(queue: Queue, context: MTContext, datacenterId: Int, server: ProxyServerSettings, updated: @escaping (ProxyServerStatus) -> Void) {
self.disposable = (Signal<ProxyServerStatus, NoError> { subscriber in
let disposable = MTProxyConnectivity.pingProxy(with: context, datacenterId: datacenterId, settings: server.mtProxySettings).start(next: { next in
if let next = next as? MTProxyConnectivityStatus {
if !next.reachable {
subscriber.putNext(.notAvailable)
} else {
subscriber.putNext(.available(next.roundTripTime))
}
}
})
return ActionDisposable {
disposable?.dispose()
}
} |> runOn(queue)).start(next: { status in
updated(status)
})
}
deinit {
self.disposable?.dispose()
}
}
final class ProxyServersStatusesImpl {
private let queue: Queue
private var contexts: [ProxyServerSettings: ProxyServerItemContext] = [:]
private var serversDisposable: Disposable?
private var currentValues: [ProxyServerSettings: ProxyServerStatus] = [:] {
didSet {
self.values.set(.single(self.currentValues))
}
}
let values = Promise<[ProxyServerSettings: ProxyServerStatus]>([:])
init(queue: Queue, network: Network, servers: Signal<[ProxyServerSettings], NoError>) {
self.queue = queue
self.serversDisposable = (servers
|> deliverOn(self.queue)).start(next: { [weak self] servers in
if let strongSelf = self {
let validKeys = Set<ProxyServerSettings>(servers)
for key in validKeys {
if strongSelf.contexts[key] == nil {
let context = ProxyServerItemContext(queue: strongSelf.queue, context: network.context, datacenterId: network.datacenterId, server: key, updated: { value in
queue.async {
if let strongSelf = self {
strongSelf.contexts[key]?.value = value
strongSelf.updateValues()
}
}
})
strongSelf.contexts[key] = context
}
}
var removeKeys: [ProxyServerSettings] = []
for (key, _) in strongSelf.contexts {
if !validKeys.contains(key) {
removeKeys.append(key)
}
}
for key in removeKeys {
let _ = strongSelf.contexts.removeValue(forKey: key)
}
if !removeKeys.isEmpty {
strongSelf.updateValues()
}
}
})
}
deinit {
self.serversDisposable?.dispose()
}
private func updateValues() {
assert(self.queue.isCurrent())
var values: [ProxyServerSettings: ProxyServerStatus] = [:]
for (key, context) in self.contexts {
values[key] = context.value
}
self.currentValues = values
}
}
public final class ProxyServersStatuses {
private let impl: QueueLocalObject<ProxyServersStatusesImpl>
public init(network: Network, servers: Signal<[ProxyServerSettings], NoError>) {
let queue = Queue()
self.impl = QueueLocalObject(queue: queue, generate: {
return ProxyServersStatusesImpl(queue: queue, network: network, servers: servers)
})
}
public func statuses() -> Signal<[ProxyServerSettings: ProxyServerStatus], NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.values.get().start(next: { value in
subscriber.putNext(value)
}))
}
return ActionDisposable {
self.impl.with({ _ in })
disposable.dispose()
}
}
}
}
@@ -0,0 +1,46 @@
import Foundation
import Postbox
public final class ChatUpdatingMessageMedia: Equatable {
public let text: String
public let entities: TextEntitiesMessageAttribute?
public let disableUrlPreview: Bool
public let media: RequestEditMessageMedia
public let invertMediaAttribute: InvertMediaMessageAttribute?
public let progress: Float
init(text: String, entities: TextEntitiesMessageAttribute?, disableUrlPreview: Bool, media: RequestEditMessageMedia, invertMediaAttribute: InvertMediaMessageAttribute?, progress: Float) {
self.text = text
self.entities = entities
self.disableUrlPreview = disableUrlPreview
self.media = media
self.invertMediaAttribute = invertMediaAttribute
self.progress = progress
}
public static func ==(lhs: ChatUpdatingMessageMedia, rhs: ChatUpdatingMessageMedia) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.entities != rhs.entities {
return false
}
if lhs.disableUrlPreview != rhs.disableUrlPreview {
return false
}
if lhs.media != rhs.media {
return false
}
if (lhs.invertMediaAttribute == nil) != (rhs.invertMediaAttribute == nil) {
return false
}
if lhs.progress != rhs.progress {
return false
}
return true
}
func withProgress(_ progress: Float) -> ChatUpdatingMessageMedia {
return ChatUpdatingMessageMedia(text: self.text, entities: self.entities, disableUrlPreview: self.disableUrlPreview, media: self.media, invertMediaAttribute: self.invertMediaAttribute, progress: progress)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,405 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramApi
public final class PeerMediaUploadingItem: Equatable {
public enum ProgressValue {
case progress(Float)
case done(Api.Updates)
}
public enum Error {
case generic
case flood
}
public enum Content: Equatable {
case wallpaper(wallpaper: TelegramWallpaper, forBoth: Bool)
}
public let content: Content
public let messageId: EngineMessage.Id?
public let progress: Float
init(content: Content, messageId: EngineMessage.Id?, progress: Float) {
self.content = content
self.messageId = messageId
self.progress = progress
}
public static func ==(lhs: PeerMediaUploadingItem, rhs: PeerMediaUploadingItem) -> Bool {
if lhs.content != rhs.content {
return false
}
if lhs.messageId != rhs.messageId {
return false
}
if lhs.progress != rhs.progress {
return false
}
return true
}
func withMessageId(_ messageId: EngineMessage.Id) -> PeerMediaUploadingItem {
return PeerMediaUploadingItem(content: self.content, messageId: messageId, progress: self.progress)
}
func withProgress(_ progress: Float) -> PeerMediaUploadingItem {
return PeerMediaUploadingItem(content: self.content, messageId: self.messageId, progress: progress)
}
}
private func uploadPeerMedia(postbox: Postbox, network: Network, stateManager: AccountStateManager, peerId: EnginePeer.Id, content: PeerMediaUploadingItem.Content) -> Signal<PeerMediaUploadingItem.ProgressValue, PeerMediaUploadingItem.Error> {
switch content {
case let .wallpaper(wallpaper, forBoth):
if case let .image(representations, settings) = wallpaper, let resource = representations.last?.resource as? LocalFileMediaResource {
return _internal_uploadWallpaper(postbox: postbox, network: network, resource: resource, settings: settings, forChat: true)
|> mapError { error -> PeerMediaUploadingItem.Error in
return .generic
}
|> mapToSignal { value -> Signal<PeerMediaUploadingItem.ProgressValue, PeerMediaUploadingItem.Error> in
switch value {
case let .progress(progress):
return .single(.progress(progress))
case let .complete(result):
if case let .file(file) = result {
postbox.mediaBox.copyResourceData(from: resource.id, to: file.file.resource.id, synchronous: true)
for representation in file.file.previewRepresentations {
postbox.mediaBox.copyResourceData(from: resource.id, to: representation.resource.id, synchronous: true)
}
}
return _internal_setChatWallpaper(postbox: postbox, network: network, stateManager: stateManager, peerId: peerId, wallpaper: result, forBoth: forBoth, applyUpdates: false)
|> mapError { error -> PeerMediaUploadingItem.Error in
switch error {
case .generic:
return .generic
case .flood:
return .flood
}
}
|> map { updates -> PeerMediaUploadingItem.ProgressValue in
return .done(updates)
}
}
}
} else {
return _internal_setChatWallpaper(postbox: postbox, network: network, stateManager: stateManager, peerId: peerId, wallpaper: wallpaper, forBoth: forBoth, applyUpdates: false)
|> mapError { error -> PeerMediaUploadingItem.Error in
switch error {
case .generic:
return .generic
case .flood:
return .flood
}
}
|> map { updates -> PeerMediaUploadingItem.ProgressValue in
return .done(updates)
}
}
}
}
private func generatePeerMediaMessage(network: Network, accountPeerId: EnginePeer.Id, transaction: Transaction, peerId: PeerId, content: PeerMediaUploadingItem.Content) -> StoreMessage {
var randomId: Int64 = 0
arc4random_buf(&randomId, 8)
var timestamp = Int32(network.context.globalTime())
switch peerId.namespace {
case Namespaces.Peer.CloudChannel, Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudUser:
if let topIndex = transaction.getTopPeerMessageIndex(peerId: peerId, namespace: Namespaces.Message.Cloud) {
timestamp = max(timestamp, topIndex.timestamp)
}
default:
break
}
var flags = StoreMessageFlags()
flags.insert(.Unsent)
flags.insert(.Sending)
var attributes: [MessageAttribute] = []
attributes.append(OutgoingMessageInfoAttribute(uniqueId: randomId, flags: [], acknowledged: false, correlationId: nil, bubbleUpEmojiOrStickersets: [], partialReference: nil))
var media: [Media] = []
switch content {
case let .wallpaper(wallpaper, forBoth):
media.append(TelegramMediaAction(action: .setChatWallpaper(wallpaper: wallpaper, forBoth: forBoth)))
}
return StoreMessage(peerId: peerId, namespace: Namespaces.Message.Local, customStableId: nil, globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: accountPeerId, text: "", attributes: attributes, media: media)
}
private final class PendingPeerMediaUploadContext {
var value: PeerMediaUploadingItem
let disposable = MetaDisposable()
init(value: PeerMediaUploadingItem) {
self.value = value
}
}
private final class PendingPeerMediaUploadManagerImpl {
let queue: Queue
let postbox: Postbox
let network: Network
let stateManager: AccountStateManager
let accountPeerId: EnginePeer.Id
let sentMessageEventPipe: ValuePipe<EnginePeer.Id>
private var uploadingPeerMediaValue: [EnginePeer.Id: PeerMediaUploadingItem] = [:] {
didSet {
if self.uploadingPeerMediaValue != oldValue {
self.uploadingPeerMediaPromise.set(.single(self.uploadingPeerMediaValue))
}
}
}
private let uploadingPeerMediaPromise = Promise<[EnginePeer.Id: PeerMediaUploadingItem]>()
fileprivate var uploadingPeerMedia: Signal<[EnginePeer.Id: PeerMediaUploadingItem], NoError> {
return self.uploadingPeerMediaPromise.get()
}
private var contexts: [PeerId: PendingPeerMediaUploadContext] = [:]
init(queue: Queue, postbox: Postbox, network: Network, stateManager: AccountStateManager, accountPeerId: EnginePeer.Id) {
self.queue = queue
self.postbox = postbox
self.network = network
self.stateManager = stateManager
self.accountPeerId = accountPeerId
self.sentMessageEventPipe = ValuePipe()
self.uploadingPeerMediaPromise.set(.single(self.uploadingPeerMediaValue))
}
deinit {
for (_, context) in self.contexts {
context.disposable.dispose()
}
}
private func updateValues() {
self.uploadingPeerMediaValue = self.contexts.mapValues { context in
return context.value
}
}
func add(peerId: EnginePeer.Id, content: PeerMediaUploadingItem.Content) {
if let context = self.contexts[peerId] {
self.contexts.removeValue(forKey: peerId)
context.disposable.dispose()
}
let postbox = self.postbox
let network = self.network
let stateManager = self.stateManager
let accountPeerId = self.accountPeerId
let queue = self.queue
let context = PendingPeerMediaUploadContext(value: PeerMediaUploadingItem(content: content, messageId: nil, progress: 0.0))
self.contexts[peerId] = context
context.disposable.set(
(self.postbox.transaction({ transaction -> EngineMessage.Id? in
let storeMessage = generatePeerMediaMessage(network: network, accountPeerId: accountPeerId, transaction: transaction, peerId: peerId, content: content)
let globallyUniqueIdToMessageId = transaction.addMessages([storeMessage], location: .Random)
guard let globallyUniqueId = storeMessage.globallyUniqueId, let messageId = globallyUniqueIdToMessageId[globallyUniqueId] else {
return nil
}
return messageId
})
|> deliverOn(queue)).start(next: { [weak self, weak context] messageId in
guard let strongSelf = self, let initialContext = context else {
return
}
if let context = strongSelf.contexts[peerId], context === initialContext {
guard let messageId = messageId else {
strongSelf.contexts.removeValue(forKey: peerId)
context.disposable.dispose()
strongSelf.updateValues()
return
}
context.value = context.value.withMessageId(messageId)
strongSelf.updateValues()
strongSelf.sentMessageEventPipe.putNext(peerId)
context.disposable.set((uploadPeerMedia(postbox: postbox, network: network, stateManager: stateManager, peerId: peerId, content: content)
|> deliverOn(queue)).start(next: { [weak self, weak context] value in
guard let strongSelf = self, let initialContext = context else {
return
}
if let context = strongSelf.contexts[peerId], context === initialContext {
switch value {
case let .done(result):
context.disposable.set(
(postbox.transaction({ transaction -> Message? in
return transaction.getMessage(messageId)
})
|> deliverOn(queue)
).start(next: { [weak self, weak context] message in
guard let strongSelf = self, let initialContext = context else {
return
}
if let context = strongSelf.contexts[peerId], context === initialContext {
guard let message = message else {
strongSelf.contexts.removeValue(forKey: peerId)
context.disposable.dispose()
strongSelf.updateValues()
return
}
context.disposable.set(
(applyUpdateMessage(
postbox: postbox,
stateManager: stateManager,
message: message,
cacheReferenceKey: nil,
result: result,
accountPeerId: accountPeerId,
pendingMessageEvent: { _ in }
)
|> deliverOn(queue)).start(completed: { [weak self, weak context] in
guard let strongSelf = self, let initialContext = context else {
return
}
if let context = strongSelf.contexts[peerId], context === initialContext {
strongSelf.contexts.removeValue(forKey: peerId)
context.disposable.dispose()
strongSelf.updateValues()
}
})
)
}
})
)
strongSelf.updateValues()
case let .progress(progress):
context.value = context.value.withProgress(progress)
strongSelf.updateValues()
}
}
}, error: { [weak self, weak context] error in
guard let strongSelf = self, let initialContext = context else {
return
}
if let context = strongSelf.contexts[peerId], context === initialContext {
strongSelf.contexts.removeValue(forKey: peerId)
if let messageId = context.value.messageId {
context.disposable.set(strongSelf.postbox.transaction({ transaction in
transaction.deleteMessages([messageId], forEachMedia: nil)
}).start())
} else {
context.disposable.dispose()
}
strongSelf.updateValues()
}
}))
}
})
)
}
func cancel(peerId: EnginePeer.Id) {
if let context = self.contexts[peerId] {
self.contexts.removeValue(forKey: peerId)
if let messageId = context.value.messageId {
context.disposable.set(self.postbox.transaction({ transaction in
transaction.deleteMessages([messageId], forEachMedia: nil)
}).start())
} else {
context.disposable.dispose()
}
self.updateValues()
}
}
func uploadProgress(messageId: EngineMessage.Id) -> Signal<Float?, NoError> {
return self.uploadingPeerMedia
|> map { uploadingPeerMedia in
if let item = uploadingPeerMedia[messageId.peerId], item.messageId == messageId {
return item.progress
} else {
return nil
}
}
|> distinctUntilChanged
}
public func sentMessageEvents(peerId: EnginePeer.Id) -> Signal<Void, NoError> {
return self.sentMessageEventPipe.signal()
|> mapToSignal { eventPeerId -> Signal<Void, NoError> in
if eventPeerId == peerId {
return .single(Void())
} else {
return .complete()
}
}
}
}
public final class PendingPeerMediaUploadManager {
private let queue = Queue()
private let impl: QueueLocalObject<PendingPeerMediaUploadManagerImpl>
public var uploadingPeerMedia: Signal<[EnginePeer.Id: PeerMediaUploadingItem], NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.uploadingPeerMedia.start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
init(postbox: Postbox, network: Network, stateManager: AccountStateManager, accountPeerId: EnginePeer.Id) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return PendingPeerMediaUploadManagerImpl(queue: queue, postbox: postbox, network: network, stateManager: stateManager, accountPeerId: accountPeerId)
})
}
public func add(peerId: EnginePeer.Id, content: PeerMediaUploadingItem.Content) {
self.impl.with { impl in
impl.add(peerId: peerId, content: content)
}
}
public func cancel(peerId: EnginePeer.Id) {
self.impl.with { impl in
impl.cancel(peerId: peerId)
}
}
public func uploadProgress(messageId: EngineMessage.Id) -> Signal<Float?, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.uploadProgress(messageId: messageId).start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
public func sentMessageEvents(peerId: EnginePeer.Id) -> Signal<Void, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.sentMessageEvents(peerId: peerId).start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
}
@@ -0,0 +1,177 @@
import Foundation
import SwiftSignalKit
import Postbox
private final class PendingUpdateMessageContext {
var value: ChatUpdatingMessageMedia
let disposable: Disposable
init(value: ChatUpdatingMessageMedia, disposable: Disposable) {
self.value = value
self.disposable = disposable
}
}
private final class PendingUpdateMessageManagerImpl {
let queue: Queue
let postbox: Postbox
let network: Network
let stateManager: AccountStateManager
let messageMediaPreuploadManager: MessageMediaPreuploadManager
let mediaReferenceRevalidationContext: MediaReferenceRevalidationContext
var transformOutgoingMessageMedia: TransformOutgoingMessageMedia?
private var updatingMessageMediaValue: [MessageId: ChatUpdatingMessageMedia] = [:] {
didSet {
if self.updatingMessageMediaValue != oldValue {
self.updatingMessageMediaPromise.set(.single(self.updatingMessageMediaValue))
}
}
}
private let updatingMessageMediaPromise = Promise<[MessageId: ChatUpdatingMessageMedia]>()
var updatingMessageMedia: Signal<[MessageId: ChatUpdatingMessageMedia], NoError> {
return self.updatingMessageMediaPromise.get()
}
private var contexts: [MessageId: PendingUpdateMessageContext] = [:]
private let errorsPipe = ValuePipe<(MessageId, RequestEditMessageError)>()
var errors: Signal<(MessageId, RequestEditMessageError), NoError> {
return self.errorsPipe.signal()
}
init(queue: Queue, postbox: Postbox, network: Network, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext) {
self.queue = queue
self.postbox = postbox
self.network = network
self.stateManager = stateManager
self.messageMediaPreuploadManager = messageMediaPreuploadManager
self.mediaReferenceRevalidationContext = mediaReferenceRevalidationContext
self.updatingMessageMediaPromise.set(.single(self.updatingMessageMediaValue))
}
deinit {
for (_, context) in self.contexts {
context.disposable.dispose()
}
}
private func updateValues() {
self.updatingMessageMediaValue = self.contexts.mapValues { context in
return context.value
}
}
func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, invertMediaAttribute: InvertMediaMessageAttribute?, disableUrlPreview: Bool) {
if let context = self.contexts[messageId] {
self.contexts.removeValue(forKey: messageId)
context.disposable.dispose()
}
let disposable = MetaDisposable()
let context = PendingUpdateMessageContext(value: ChatUpdatingMessageMedia(text: text, entities: entities, disableUrlPreview: disableUrlPreview, media: media, invertMediaAttribute: invertMediaAttribute, progress: 0.0), disposable: disposable)
self.contexts[messageId] = context
let queue = self.queue
disposable.set((requestEditMessage(accountPeerId: self.stateManager.accountPeerId, postbox: self.postbox, network: self.network, stateManager: self.stateManager, transformOutgoingMessageMedia: self.transformOutgoingMessageMedia, messageMediaPreuploadManager: self.messageMediaPreuploadManager, mediaReferenceRevalidationContext: self.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleInfoAttribute: nil, invertMediaAttribute: invertMediaAttribute)
|> deliverOn(self.queue)).start(next: { [weak self, weak context] value in
queue.async {
guard let strongSelf = self, let initialContext = context else {
return
}
if let context = strongSelf.contexts[messageId], context === initialContext {
switch value {
case .done:
strongSelf.contexts.removeValue(forKey: messageId)
context.disposable.dispose()
strongSelf.updateValues()
case let .progress(progress):
context.value = context.value.withProgress(progress)
strongSelf.updateValues()
}
}
}
}, error: { [weak self, weak context] error in
queue.async {
guard let strongSelf = self, let initialContext = context else {
return
}
if let context = strongSelf.contexts[messageId], context === initialContext {
strongSelf.contexts.removeValue(forKey: messageId)
context.disposable.dispose()
strongSelf.updateValues()
}
strongSelf.errorsPipe.putNext((messageId, error))
}
}))
}
func cancel(messageId: MessageId) {
if let context = self.contexts[messageId] {
self.contexts.removeValue(forKey: messageId)
context.disposable.dispose()
self.updateValues()
}
}
}
public final class PendingUpdateMessageManager {
private let queue = Queue()
private let impl: QueueLocalObject<PendingUpdateMessageManagerImpl>
var transformOutgoingMessageMedia: TransformOutgoingMessageMedia? {
didSet {
let transformOutgoingMessageMedia = self.transformOutgoingMessageMedia
self.impl.with { impl in
impl.transformOutgoingMessageMedia = transformOutgoingMessageMedia
}
}
}
public var updatingMessageMedia: Signal<[MessageId: ChatUpdatingMessageMedia], NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.updatingMessageMedia.start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
public var errors: Signal<(MessageId, RequestEditMessageError), NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
disposable.set(impl.errors.start(next: { value in
subscriber.putNext(value)
}))
}
return disposable
}
}
init(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return PendingUpdateMessageManagerImpl(queue: queue, postbox: postbox, network: network, stateManager: stateManager, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext)
})
}
public func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute? = nil, invertMediaAttribute: InvertMediaMessageAttribute? = nil, disableUrlPreview: Bool = false) {
self.impl.with { impl in
impl.add(messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, invertMediaAttribute: invertMediaAttribute, disableUrlPreview: disableUrlPreview)
}
}
public func cancel(messageId: MessageId) {
self.impl.with { impl in
impl.cancel(messageId: messageId)
}
}
}
@@ -0,0 +1,474 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
// MARK: - GLEGram
#if canImport(SGDeletedMessages)
import SGDeletedMessages
#endif
#if canImport(SGSimpleSettings)
import SGSimpleSettings
#endif
public enum RequestEditMessageMedia : Equatable {
case keep
case update(AnyMediaReference)
}
public enum RequestEditMessageResult {
case progress(Float)
case done(Bool)
}
private enum RequestEditMessageInternalError {
case error(RequestEditMessageError)
case invalidReference
}
public enum RequestEditMessageError {
case generic
case restricted
case textTooLong
case invalidGrouping
}
func _internal_requestEditMessage(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool, scheduleInfoAttribute: OutgoingScheduleInfoMessageAttribute?, invertMediaAttribute: InvertMediaMessageAttribute?) -> Signal<RequestEditMessageResult, RequestEditMessageError> {
return requestEditMessage(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, stateManager: account.stateManager, transformOutgoingMessageMedia: account.transformOutgoingMessageMedia, messageMediaPreuploadManager: account.messageMediaPreuploadManager, mediaReferenceRevalidationContext: account.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleInfoAttribute: scheduleInfoAttribute, invertMediaAttribute: invertMediaAttribute)
}
func requestEditMessage(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool, scheduleInfoAttribute: OutgoingScheduleInfoMessageAttribute?, invertMediaAttribute: InvertMediaMessageAttribute?) -> Signal<RequestEditMessageResult, RequestEditMessageError> {
return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, invertMediaAttribute: invertMediaAttribute, disableUrlPreview: disableUrlPreview, scheduleInfoAttribute: scheduleInfoAttribute, forceReupload: false)
|> `catch` { error -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> in
if case .invalidReference = error {
return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, invertMediaAttribute: invertMediaAttribute, disableUrlPreview: disableUrlPreview, scheduleInfoAttribute: scheduleInfoAttribute, forceReupload: true)
} else {
return .fail(error)
}
}
|> mapError { error -> RequestEditMessageError in
switch error {
case let .error(error):
return error
default:
return .generic
}
}
}
private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, invertMediaAttribute: InvertMediaMessageAttribute?, disableUrlPreview: Bool, scheduleInfoAttribute: OutgoingScheduleInfoMessageAttribute?, forceReupload: Bool) -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> {
let uploadedMedia: Signal<PendingMessageUploadedContentResult?, NoError>
switch media {
case .keep:
uploadedMedia = .single(.progress(PendingMessageUploadedContentProgress(progress: 0.0)))
|> then(.single(nil))
case let .update(media):
let generateUploadSignal: (Bool) -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>? = { forceReupload in
let augmentedMedia = augmentMediaWithReference(media)
var attributes: [MessageAttribute] = []
if let webpagePreviewAttribute = webpagePreviewAttribute {
attributes.append(webpagePreviewAttribute)
}
if let invertMediaAttribute {
attributes.append(invertMediaAttribute)
}
return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, forceNoBigParts: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: attributes, mediaReference: nil, explicitPartialReference: nil)
}
if let todo = media.media as? TelegramMediaTodo {
var flags: Int32 = 0
if todo.flags.contains(.othersCanAppend) {
flags |= 1 << 0
}
if todo.flags.contains(.othersCanComplete) {
flags |= 1 << 1
}
let inputTodo = Api.InputMedia.inputMediaTodo(.init(todo: .todoList(.init(flags: flags, title: .textWithEntities(.init(text: todo.text, entities: apiEntitiesFromMessageTextEntities(todo.textEntities, associatedPeers: SimpleDictionary()))), list: todo.items.map { $0.apiItem }))))
uploadedMedia = .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputTodo, text), reuploadInfo: nil, cacheReferenceKey: nil)))
}
else if let uploadSignal = generateUploadSignal(forceReupload) {
uploadedMedia = .single(.progress(PendingMessageUploadedContentProgress(progress: 0.027)))
|> then(uploadSignal)
|> map { result -> PendingMessageUploadedContentResult? in
switch result {
case let .progress(value):
return .progress(PendingMessageUploadedContentProgress(progress: max(value.progress, 0.027)))
case let .content(content):
return .content(content)
}
}
|> `catch` { _ -> Signal<PendingMessageUploadedContentResult?, NoError> in
return .single(nil)
}
} else {
uploadedMedia = .single(nil)
}
}
return uploadedMedia
|> mapError { _ -> RequestEditMessageInternalError in }
|> mapToSignal { uploadedMediaResult -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> in
var pendingMediaContent: PendingMessageUploadedContent?
if let uploadedMediaResult = uploadedMediaResult {
switch uploadedMediaResult {
case let .progress(value):
return .single(.progress(value.progress))
case let .content(content):
pendingMediaContent = content.content
}
}
return postbox.transaction { transaction -> (Peer?, Message?, SimpleDictionary<PeerId, Peer>) in
guard let message = transaction.getMessage(messageId) else {
return (nil, nil, SimpleDictionary())
}
for (_, file) in inlineStickers {
transaction.storeMediaIfNotPresent(media: file)
}
if text.isEmpty {
for media in message.media {
switch media {
case _ as TelegramMediaImage, _ as TelegramMediaFile, _ as TelegramMediaTodo:
break
default:
if let _ = scheduleInfoAttribute {
break
} else {
return (nil, nil, SimpleDictionary())
}
}
}
}
var peers = SimpleDictionary<PeerId, Peer>()
if let entities = entities {
for peerId in entities.associatedPeerIds {
if let peer = transaction.getPeer(peerId) {
peers[peer.id] = peer
}
}
}
return (transaction.getPeer(messageId.peerId), message, peers)
}
|> mapError { _ -> RequestEditMessageInternalError in }
|> mapToSignal { peer, message, associatedPeers -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> in
if let peer = peer, let message = message, let inputPeer = apiInputPeer(peer) {
var flags: Int32 = 1 << 11
var apiEntities: [Api.MessageEntity]?
if let entities = entities {
apiEntities = apiTextAttributeEntities(entities, associatedPeers: associatedPeers)
flags |= Int32(1 << 3)
}
if disableUrlPreview {
flags |= Int32(1 << 1)
}
var inputMedia: Api.InputMedia? = nil
if let pendingMediaContent = pendingMediaContent {
switch pendingMediaContent {
case let .media(media, _):
inputMedia = media
default:
break
}
}
if let _ = inputMedia {
flags |= Int32(1 << 14)
}
var effectiveScheduleTime: Int32?
var effectiveScheduleRepeatPeriod: Int32?
if messageId.namespace == Namespaces.Message.ScheduledCloud {
if let scheduleTime = scheduleInfoAttribute?.scheduleTime {
effectiveScheduleTime = scheduleTime
} else {
effectiveScheduleTime = message.timestamp
}
flags |= Int32(1 << 15)
if let scheduleInfoAttribute {
effectiveScheduleRepeatPeriod = scheduleInfoAttribute.repeatPeriod ?? 0
flags |= Int32(1 << 18)
}
}
if let _ = invertMediaAttribute {
flags |= Int32(1 << 16)
}
var quickReplyShortcutId: Int32?
if messageId.namespace == Namespaces.Message.QuickReplyCloud {
quickReplyShortcutId = Int32(clamping: message.threadId ?? 0)
flags |= Int32(1 << 17)
}
return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: text, media: inputMedia, replyMarkup: nil, entities: apiEntities, scheduleDate: effectiveScheduleTime, scheduleRepeatPeriod: effectiveScheduleRepeatPeriod, quickReplyShortcutId: quickReplyShortcutId))
|> map { result -> Api.Updates? in
return result
}
|> `catch` { error -> Signal<Api.Updates?, MTRpcError> in
if error.errorDescription == "MESSAGE_NOT_MODIFIED" {
return .single(nil)
} else {
return .fail(error)
}
}
|> mapError { error -> RequestEditMessageInternalError in
if error.errorDescription.hasPrefix("FILEREF_INVALID") || error.errorDescription.hasPrefix("FILE_REFERENCE_") {
return .invalidReference
} else if error.errorDescription.hasSuffix("_TOO_LONG") {
return .error(.textTooLong)
} else if error.errorDescription.hasPrefix("MEDIA_GROUPED_INVALID") {
return .error(.invalidGrouping)
} else if error.errorDescription.hasPrefix("CHAT_SEND_") && error.errorDescription.hasSuffix("_FORBIDDEN") {
return .error(.restricted)
}
return .error(.generic)
}
|> mapToSignal { result -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> in
if let result = result {
return postbox.transaction { transaction -> RequestEditMessageResult in
var toMedia: Media?
if let message = result.messages.first.flatMap({ StoreMessage(apiMessage: $0, accountPeerId: accountPeerId, peerIsForum: peer.isForumOrMonoForum) }) {
toMedia = message.media.first
}
if case let .update(fromMedia) = media, let toMedia = toMedia {
applyMediaResourceChanges(from: fromMedia.media, to: toMedia, postbox: postbox, force: true)
}
switch result {
case let .updates(updatesData):
let (updates, users, chats) = (updatesData.updates, updatesData.users, updatesData.chats)
for update in updates {
switch update {
case .updateEditMessage(let data):
let message = data.message
let peers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: peers)
if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForumOrMonoForum), case let .Id(id) = message.id {
transaction.updateMessage(id, update: { previousMessage in
var updatedFlags = message.flags
var updatedLocalTags = message.localTags
if previousMessage.localTags.contains(.OutgoingLiveLocation) {
updatedLocalTags.insert(.OutgoingLiveLocation)
}
if previousMessage.flags.contains(.Incoming) {
updatedFlags.insert(.Incoming)
} else {
updatedFlags.remove(.Incoming)
}
var updatedMedia = message.media
if let previousPaidContent = previousMessage.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent, case .full = previousPaidContent.extendedMedia.first {
updatedMedia = previousMessage.media
}
// MARK: - GLEGram - Save original text on edit (like Nicegram)
#if canImport(SGDeletedMessages)
#if canImport(SGSimpleSettings)
if SGSimpleSettings.shared.saveEditHistory {
return .update(message
.withUpdatedLocalTags(updatedLocalTags)
.withUpdatedFlags(updatedFlags)
.withUpdatedMedia(updatedMedia)
.updatingSGDeletedAttributeOnEdit(previousMessage: previousMessage)
)
}
#endif
#endif
return .update(message.withUpdatedLocalTags(updatedLocalTags).withUpdatedFlags(updatedFlags).withUpdatedMedia(updatedMedia))
})
}
case .updateNewMessage(let data):
let message = data.message
let peers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: peers)
if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForumOrMonoForum), case let .Id(id) = message.id {
transaction.updateMessage(id, update: { previousMessage in
var updatedFlags = message.flags
var updatedLocalTags = message.localTags
if previousMessage.localTags.contains(.OutgoingLiveLocation) {
updatedLocalTags.insert(.OutgoingLiveLocation)
}
if previousMessage.flags.contains(.Incoming) {
updatedFlags.insert(.Incoming)
} else {
updatedFlags.remove(.Incoming)
}
var updatedMedia = message.media
if let previousPaidContent = previousMessage.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent, case .full = previousPaidContent.extendedMedia.first {
updatedMedia = previousMessage.media
}
return .update(message.withUpdatedLocalTags(updatedLocalTags).withUpdatedFlags(updatedFlags).withUpdatedMedia(updatedMedia))
})
}
case .updateEditChannelMessage(let data):
let message = data.message
let peers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: peers)
if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForumOrMonoForum), case let .Id(id) = message.id {
transaction.updateMessage(id, update: { previousMessage in
var updatedFlags = message.flags
var updatedLocalTags = message.localTags
if previousMessage.localTags.contains(.OutgoingLiveLocation) {
updatedLocalTags.insert(.OutgoingLiveLocation)
}
if previousMessage.flags.contains(.Incoming) {
updatedFlags.insert(.Incoming)
} else {
updatedFlags.remove(.Incoming)
}
var updatedMedia = message.media
if let previousPaidContent = previousMessage.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent, case .full = previousPaidContent.extendedMedia.first {
updatedMedia = previousMessage.media
}
return .update(message.withUpdatedLocalTags(updatedLocalTags).withUpdatedFlags(updatedFlags).withUpdatedMedia(updatedMedia))
})
}
case .updateNewChannelMessage(let data):
let message = data.message
let peers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: peers)
if let message = StoreMessage(apiMessage: message, accountPeerId: accountPeerId, peerIsForum: peer.isForumOrMonoForum), case let .Id(id) = message.id {
transaction.updateMessage(id, update: { previousMessage in
var updatedFlags = message.flags
var updatedLocalTags = message.localTags
if previousMessage.localTags.contains(.OutgoingLiveLocation) {
updatedLocalTags.insert(.OutgoingLiveLocation)
}
if previousMessage.flags.contains(.Incoming) {
updatedFlags.insert(.Incoming)
} else {
updatedFlags.remove(.Incoming)
}
var updatedMedia = message.media
if let previousPaidContent = previousMessage.media.first(where: { $0 is TelegramMediaPaidContent }) as? TelegramMediaPaidContent, case .full = previousPaidContent.extendedMedia.first {
updatedMedia = previousMessage.media
}
return .update(message.withUpdatedLocalTags(updatedLocalTags).withUpdatedFlags(updatedFlags).withUpdatedMedia(updatedMedia))
})
}
default:
break
}
}
default:
break
}
stateManager.addUpdates(result)
return .done(true)
}
|> mapError { _ -> RequestEditMessageInternalError in
}
} else {
return .single(.done(false))
}
}
} else {
return .single(.done(false))
}
}
}
}
func _internal_requestEditLiveLocation(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId, stop: Bool, coordinate: (latitude: Double, longitude: Double, accuracyRadius: Int32?)?, heading: Int32?, proximityNotificationRadius: Int32?, extendPeriod: Int32?) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> (Api.InputPeer, TelegramMediaMap)? in
guard let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else {
return nil
}
guard let message = transaction.getMessage(messageId) else {
return nil
}
for media in message.media {
if let media = media as? TelegramMediaMap {
return (inputPeer, media)
}
}
return nil
}
|> mapToSignal { inputPeerAndMedia -> Signal<Void, NoError> in
guard let (inputPeer, media) = inputPeerAndMedia else {
return .complete()
}
let inputMedia: Api.InputMedia
if let liveBroadcastingTimeout = media.liveBroadcastingTimeout, !stop {
var flags: Int32 = 1 << 1
let inputGeoPoint: Api.InputGeoPoint
if let coordinate = coordinate {
var geoFlags: Int32 = 0
if let _ = coordinate.accuracyRadius {
geoFlags |= 1 << 0
}
inputGeoPoint = .inputGeoPoint(.init(flags: geoFlags, lat: coordinate.latitude, long: coordinate.longitude, accuracyRadius: coordinate.accuracyRadius.flatMap({ Int32($0) })))
} else {
var geoFlags: Int32 = 0
if let _ = media.accuracyRadius {
geoFlags |= 1 << 0
}
inputGeoPoint = .inputGeoPoint(.init(flags: geoFlags, lat: media.latitude, long: media.longitude, accuracyRadius: media.accuracyRadius.flatMap({ Int32($0) })))
}
if let _ = heading {
flags |= 1 << 2
}
if let _ = proximityNotificationRadius {
flags |= 1 << 3
}
let period: Int32
if let extendPeriod {
if extendPeriod == liveLocationIndefinitePeriod {
period = extendPeriod
} else {
period = liveBroadcastingTimeout + extendPeriod
}
} else {
period = liveBroadcastingTimeout
}
inputMedia = .inputMediaGeoLive(.init(flags: flags, geoPoint: inputGeoPoint, heading: heading, period: period, proximityNotificationRadius: proximityNotificationRadius))
} else {
inputMedia = .inputMediaGeoLive(.init(flags: 1 << 0, geoPoint: .inputGeoPoint(.init(flags: 0, lat: media.latitude, long: media.longitude, accuracyRadius: nil)), heading: nil, period: nil, proximityNotificationRadius: nil))
}
return network.request(Api.functions.messages.editMessage(flags: 1 << 14, peer: inputPeer, id: messageId.id, message: nil, media: inputMedia, replyMarkup: nil, entities: nil, scheduleDate: nil, scheduleRepeatPeriod: nil, quickReplyShortcutId: nil))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates = updates {
stateManager.addUpdates(updates)
}
if coordinate == nil && proximityNotificationRadius == nil && extendPeriod == nil {
return postbox.transaction { transaction -> Void in
transaction.updateMessage(messageId, update: { currentMessage in
var storeForwardInfo: StoreMessageForwardInfo?
if let forwardInfo = currentMessage.forwardInfo {
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
}
var updatedLocalTags = currentMessage.localTags
updatedLocalTags.remove(.OutgoingLiveLocation)
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: updatedLocalTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: currentMessage.attributes, media: currentMessage.media))
})
}
} else {
return .complete()
}
}
}
}
@@ -0,0 +1,793 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum StandaloneMedia {
case image(Data)
case file(data: Data, mimeType: String, attributes: [TelegramMediaFileAttribute])
}
private enum StandaloneMessageContent {
case text(String)
case media(Api.InputMedia, String)
}
private enum StandaloneSendMessageEvent {
case result(StandaloneMessageContent)
case progress(Float)
}
public enum StandaloneSendMessageError {
case generic
}
public enum StandaloneSendMessageStatus {
case progress(Float)
case done
}
public struct StandaloneSendMessagesError {
public var peerId: PeerId
public var reason: PendingMessageFailureReason?
init(
peerId: PeerId,
reason: PendingMessageFailureReason?
) {
self.peerId = peerId
self.reason = reason
}
}
public struct StandaloneSendEnqueueMessage {
public struct Text {
public var string: String
public var entities: [MessageTextEntity]
public init(
string: String,
entities: [MessageTextEntity]
) {
self.string = string
self.entities = entities
}
}
public struct Image {
public var representation: TelegramMediaImageRepresentation
public init(
representation: TelegramMediaImageRepresentation
) {
self.representation = representation
}
}
public struct Forward {
public var sourceId: MessageId
public var threadId: Int64?
public init(
sourceId: MessageId,
threadId: Int64?
) {
self.sourceId = sourceId
self.threadId = threadId
}
}
public struct ForwardOptions {
public var hideNames: Bool
public var hideCaptions: Bool
public init(
hideNames: Bool,
hideCaptions: Bool
) {
self.hideNames = hideNames
self.hideCaptions = hideCaptions
}
}
public enum Content {
case text(text: Text)
case image(image: Image, text: Text)
case map(map: TelegramMediaMap)
case arbitraryMedia(media: AnyMediaReference, text: Text)
case forward(forward: Forward)
}
public var content: Content
public var replyToMessageId: MessageId?
public var forwardOptions: ForwardOptions?
public var isSilent: Bool = false
public var groupingKey: Int64? = nil
public var sendPaidMessageStars: StarsAmount? = nil
public init(
content: Content,
replyToMessageId: MessageId?
) {
self.content = content
self.replyToMessageId = replyToMessageId
}
}
public func standaloneSendEnqueueMessages(
accountPeerId: PeerId,
postbox: Postbox,
network: Network,
stateManager: AccountStateManager,
auxiliaryMethods: AccountAuxiliaryMethods,
peerId: PeerId,
threadId: Int64?,
messages: [StandaloneSendEnqueueMessage]
) -> Signal<StandaloneSendMessageStatus, StandaloneSendMessagesError> {
struct MessageResult {
var result: PendingMessageUploadedContentResult
var media: [Media]
var attributes: [MessageAttribute]
}
let signals: [Signal<MessageResult, PendingMessageUploadError>] = messages.map { message in
var attributes: [MessageAttribute] = []
var text: String = ""
var media: [Media] = []
switch message.content {
case let .text(textValue):
text = textValue.string
if !textValue.entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: textValue.entities))
}
case let .image(image, textValue):
media.append(TelegramMediaImage(
imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: MediaId.Id.random(in: Int64.min ... Int64.max)),
representations: [image.representation],
immediateThumbnailData: nil,
reference: nil,
partialReference: nil,
flags: []
))
text = textValue.string
if !textValue.entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: textValue.entities))
}
case let .map(mapValue):
media.append(mapValue)
case let .arbitraryMedia(mediaValue, textValue):
media.append(mediaValue.media)
text = textValue.string
if !textValue.entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: textValue.entities))
}
case let .forward(forwardValue):
attributes.append(ForwardSourceInfoAttribute(messageId: forwardValue.sourceId))
}
if let replyToMessageId = message.replyToMessageId {
attributes.append(ReplyMessageAttribute(messageId: replyToMessageId, threadMessageId: nil, quote: nil, isQuote: false, todoItemId: nil))
}
if let forwardOptions = message.forwardOptions {
attributes.append(ForwardOptionsMessageAttribute(hideNames: forwardOptions.hideNames, hideCaptions: forwardOptions.hideCaptions))
}
if message.isSilent {
attributes.append(NotificationInfoMessageAttribute(flags: .muted))
}
if let sendPaidMessageStars = message.sendPaidMessageStars {
attributes.append(PaidStarsMessageAttribute(stars: sendPaidMessageStars, postponeSending: false))
}
let content = messageContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: { _, _, _, _ in
return .single(nil)
}, messageMediaPreuploadManager: MessageMediaPreuploadManager(), revalidationContext: MediaReferenceRevalidationContext(), forceReupload: false, isGrouped: false, passFetchProgress: true, forceNoBigParts: false, peerId: peerId, messageId: nil, attributes: attributes, text: text, media: media)
let contentResult: Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>
switch content {
case let .signal(value, _):
contentResult = value
case let .immediate(value, _):
contentResult = .single(value)
}
return contentResult
|> map { contentResult in
return MessageResult(result: contentResult, media: media, attributes: attributes)
}
}
return combineLatest(signals)
|> mapError { _ -> StandaloneSendMessagesError in
return StandaloneSendMessagesError(peerId: peerId, reason: nil)
}
|> mapToSignal { contentResults -> Signal<StandaloneSendMessageStatus, StandaloneSendMessagesError> in
var progressSum: Float = 0.0
var allResults: [(result: PendingMessageUploadedContentAndReuploadInfo, media: [Media], attributes: [MessageAttribute])] = []
var allDone = true
for result in contentResults {
switch result.result {
case let .progress(value):
allDone = false
progressSum += value.progress
case let .content(content):
allResults.append((content, result.media, result.attributes))
}
}
if allDone {
var sendSignals: [Signal<Never, StandaloneSendMessagesError>] = []
for (content, media, attributes) in allResults {
var text: String = ""
switch content.content {
case let .text(textValue):
text = textValue
case let .media(_, textValue):
text = textValue
default:
break
}
sendSignals.append(sendUploadedMessageContent(
auxiliaryMethods: auxiliaryMethods,
postbox: postbox,
network: network,
stateManager: stateManager,
accountPeerId: stateManager.accountPeerId,
peerId: peerId,
content: content,
text: text,
attributes: attributes,
media: media,
threadId: threadId
))
}
return combineLatest(sendSignals)
|> ignoreValues
|> map { _ -> StandaloneSendMessageStatus in
}
|> then(.single(.done))
} else {
return .single(.progress(progressSum / max(1.0, Float(contentResults.count))))
}
}
}
private func sendUploadedMessageContent(
auxiliaryMethods: AccountAuxiliaryMethods,
postbox: Postbox,
network: Network,
stateManager: AccountStateManager,
accountPeerId: PeerId,
peerId: PeerId,
content: PendingMessageUploadedContentAndReuploadInfo,
text: String,
attributes: [MessageAttribute],
media: [Media],
threadId: Int64?
) -> Signal<Never, StandaloneSendMessagesError> {
return postbox.transaction { transaction -> Signal<Never, StandaloneSendMessagesError> in
if peerId.namespace == Namespaces.Peer.SecretChat {
var secretFile: SecretChatOutgoingFile?
switch content.content {
case let .secretMedia(file, size, key):
if let fileReference = SecretChatOutgoingFileReference(file) {
secretFile = SecretChatOutgoingFile(reference: fileReference, size: size, key: key)
}
default:
break
}
var layer: SecretChatLayer?
let state = transaction.getPeerChatState(peerId) as? SecretChatState
if let state = state {
switch state.embeddedState {
case .terminated, .handshake:
break
case .basicLayer:
layer = .layer8
case let .sequenceBasedLayer(sequenceState):
layer = sequenceState.layerNegotiationState.activeLayer.secretChatLayer
}
}
if let state = state, let layer = layer {
let messageContents = StandaloneSecretMessageContents(
id: Int64.random(in: Int64.min ... Int64.max),
text: text,
attributes: attributes,
media: media.first,
file: secretFile
)
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .sendStandaloneMessage(layer: layer, contents: messageContents), state: state)
if updatedState != state {
transaction.setPeerChatState(peerId, state: updatedState)
}
return managedSecretChatOutgoingOperations(
auxiliaryMethods: auxiliaryMethods,
postbox: postbox,
network: network,
accountPeerId: accountPeerId,
mode: .standaloneComplete(peerId: peerId)
)
|> castError(StandaloneSendMessagesError.self)
|> ignoreValues
} else {
return .fail(StandaloneSendMessagesError(peerId: peerId, reason: .none))
}
} else if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
var uniqueId: Int64 = 0
var forwardSourceInfoAttribute: ForwardSourceInfoAttribute?
var messageEntities: [Api.MessageEntity]?
var replyMessageId: Int32?
var topMsgId: Int32?
var monoforumPeerId: Api.InputPeer?
if let threadId {
if let channel = peer as? TelegramChannel, channel.flags.contains(.isMonoforum) {
if let monoforumTargetPeer = transaction.getPeer(PeerId(threadId)) {
monoforumPeerId = apiInputPeer(monoforumTargetPeer)
}
} else {
replyMessageId = Int32(clamping: threadId)
topMsgId = Int32(clamping: threadId)
}
}
var replyToStoryId: StoryId?
var replyTodoItemId: Int32?
var scheduleTime: Int32?
var scheduleRepeatPeriod: Int32?
var videoTimestamp: Int32?
var sendAsPeerId: PeerId?
var bubbleUpEmojiOrStickersets = false
var allowPaidStars: Int64?
var suggestedPost: Api.SuggestedPost?
var flags: Int32 = 0
for attribute in attributes {
if let replyAttribute = attribute as? ReplyMessageAttribute {
replyMessageId = replyAttribute.messageId.id
replyTodoItemId = replyAttribute.todoItemId
} else if let attribute = attribute as? ReplyStoryAttribute {
replyToStoryId = attribute.storyId
} else if let outgoingInfo = attribute as? OutgoingMessageInfoAttribute {
uniqueId = outgoingInfo.uniqueId
bubbleUpEmojiOrStickersets = !outgoingInfo.bubbleUpEmojiOrStickersets.isEmpty
} else if let attribute = attribute as? ForwardSourceInfoAttribute {
forwardSourceInfoAttribute = attribute
} else if let attribute = attribute as? TextEntitiesMessageAttribute {
var associatedPeers = SimpleDictionary<PeerId, Peer>()
for attributePeerId in attribute.associatedPeerIds {
if let peer = transaction.getPeer(attributePeerId) {
associatedPeers[peer.id] = peer
}
}
messageEntities = apiTextAttributeEntities(attribute, associatedPeers: associatedPeers)
} else if let attribute = attribute as? OutgoingContentInfoMessageAttribute {
if attribute.flags.contains(.disableLinkPreviews) {
flags |= Int32(1 << 1)
}
} else if let attribute = attribute as? NotificationInfoMessageAttribute {
if attribute.flags.contains(.muted) {
flags |= Int32(1 << 5)
}
} else if let attribute = attribute as? OutgoingScheduleInfoMessageAttribute {
flags |= Int32(1 << 10)
scheduleTime = attribute.scheduleTime
if let repeatPeriod = attribute.repeatPeriod {
flags |= Int32(1 << 24)
scheduleRepeatPeriod = repeatPeriod
}
} else if let attribute = attribute as? SendAsMessageAttribute {
sendAsPeerId = attribute.peerId
} else if let attribute = attribute as? ForwardVideoTimestampAttribute {
flags |= Int32(1 << 20)
videoTimestamp = attribute.timestamp
} else if let attribute = attribute as? PaidStarsMessageAttribute {
allowPaidStars = attribute.stars.value
} else if let attribute = attribute as? SuggestedPostMessageAttribute {
suggestedPost = attribute.apiSuggestedPost(fixMinTime: Int32(Date().timeIntervalSince1970 + 10))
}
}
if uniqueId == 0 {
uniqueId = Int64.random(in: Int64.min ... Int64.max)
}
if case .forward = content.content {
} else {
flags |= (1 << 7)
if let _ = replyMessageId {
flags |= Int32(1 << 0)
}
if let _ = messageEntities {
flags |= Int32(1 << 3)
}
}
var sendAsInputPeer: Api.InputPeer?
if let sendAsPeerId = sendAsPeerId, let sendAsPeer = transaction.getPeer(sendAsPeerId), let inputPeer = apiInputPeerOrSelf(sendAsPeer, accountPeerId: accountPeerId) {
sendAsInputPeer = inputPeer
flags |= (1 << 13)
}
if let _ = allowPaidStars {
flags |= 1 << 21
}
let dependencyTag: PendingMessageRequestDependencyTag? = nil//(messageId: messageId)
let sendMessageRequest: Signal<NetworkRequestResult<Api.Updates>, MTRpcError>
switch content.content {
case .text:
if bubbleUpEmojiOrStickersets {
flags |= Int32(1 << 15)
}
var replyTo: Api.InputReplyTo?
if let replyMessageId {
flags |= 1 << 0
var replyFlags: Int32 = 0
if topMsgId != nil {
replyFlags |= 1 << 0
}
if monoforumPeerId != nil {
replyFlags |= 1 << 5
}
if let _ = replyTodoItemId {
replyFlags |= 1 << 6
}
replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId))
} else if let replyToStoryId {
if let inputPeer = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputPeer) {
flags |= 1 << 0
replyTo = .inputReplyToStory(.init(peer: inputPeer, storyId: replyToStoryId.id))
}
} else if let monoforumPeerId {
replyTo = .inputReplyToMonoForum(.init(monoforumPeerId: monoforumPeerId))
}
if suggestedPost != nil {
flags |= 1 << 22
}
sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost), info: .acknowledgement, tag: dependencyTag)
case let .media(inputMedia, text):
if bubbleUpEmojiOrStickersets {
flags |= Int32(1 << 15)
}
var replyTo: Api.InputReplyTo?
if let replyMessageId = replyMessageId {
flags |= 1 << 0
var replyFlags: Int32 = 0
if topMsgId != nil {
replyFlags |= 1 << 0
}
if monoforumPeerId != nil {
replyFlags |= 1 << 5
}
if let _ = replyTodoItemId {
replyFlags |= 1 << 6
}
replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: monoforumPeerId, todoItemId: replyTodoItemId))
} else if let replyToStoryId = replyToStoryId {
if let inputPeer = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputPeer) {
flags |= 1 << 0
replyTo = .inputReplyToStory(.init(peer: inputPeer, storyId: replyToStoryId.id))
}
}
if suggestedPost != nil {
flags |= 1 << 22
}
sendMessageRequest = network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost), tag: dependencyTag)
|> map(NetworkRequestResult.result)
case let .forward(sourceInfo):
if topMsgId != nil {
flags |= Int32(1 << 9)
}
if let forwardSourceInfoAttribute = forwardSourceInfoAttribute, let sourcePeer = transaction.getPeer(forwardSourceInfoAttribute.messageId.peerId), let sourceInputPeer = apiInputPeer(sourcePeer) {
sendMessageRequest = network.request(Api.functions.messages.forwardMessages(flags: flags, fromPeer: sourceInputPeer, id: [sourceInfo.messageId.id], randomId: [uniqueId], toPeer: inputPeer, topMsgId: topMsgId, replyTo: nil, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, videoTimestamp: videoTimestamp, allowPaidStars: allowPaidStars, suggestedPost: nil), tag: dependencyTag)
|> map(NetworkRequestResult.result)
} else {
sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal"))
}
case let .chatContextResult(chatContextResult):
if chatContextResult.hideVia {
flags |= Int32(1 << 11)
}
var replyTo: Api.InputReplyTo?
if let replyMessageId = replyMessageId {
flags |= 1 << 0
var replyFlags: Int32 = 0
if topMsgId != nil {
replyFlags |= 1 << 0
}
if monoforumPeerId != nil {
replyFlags |= 1 << 5
}
replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: topMsgId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: monoforumPeerId, todoItemId: nil))
} else if let replyToStoryId = replyToStoryId {
if let inputPeer = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputPeer) {
flags |= 1 << 0
replyTo = .inputReplyToStory(.init(peer: inputPeer, storyId: replyToStoryId.id))
}
}
sendMessageRequest = network.request(Api.functions.messages.sendInlineBotResult(flags: flags, peer: inputPeer, replyTo: replyTo, randomId: uniqueId, queryId: chatContextResult.queryId, id: chatContextResult.id, scheduleDate: scheduleTime, sendAs: sendAsInputPeer, quickReplyShortcut: nil, allowPaidStars: allowPaidStars))
|> map(NetworkRequestResult.result)
case .messageScreenshot:
let replyTo: Api.InputReplyTo
if let replyMessageId = replyMessageId {
let replyFlags: Int32 = 0
replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: nil, todoItemId: nil))
} else if let replyToStoryId = replyToStoryId {
if let inputPeer = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputPeer) {
flags |= 1 << 0
replyTo = .inputReplyToStory(.init(peer: inputPeer, storyId: replyToStoryId.id))
} else {
let replyFlags: Int32 = 0
replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: 0, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: nil, todoItemId: nil))
}
} else {
let replyFlags: Int32 = 0
replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: 0, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: nil, todoItemId: nil))
}
sendMessageRequest = network.request(Api.functions.messages.sendScreenshotNotification(peer: inputPeer, replyTo: replyTo, randomId: uniqueId))
|> map(NetworkRequestResult.result)
case .secretMedia:
assertionFailure()
sendMessageRequest = .fail(MTRpcError(errorCode: 400, errorDescription: "internal"))
}
return sendMessageRequest
|> mapToSignal { result -> Signal<Never, MTRpcError> in
switch result {
case .progress:
return .complete()
case .acknowledged:
return .complete()
case let .result(result):
stateManager.addUpdates(result)
return .complete()
}
}
|> mapError { error -> StandaloneSendMessagesError in
if error.errorDescription.hasPrefix("FILEREF_INVALID") || error.errorDescription.hasPrefix("FILE_REFERENCE_") {
return StandaloneSendMessagesError(peerId: peerId, reason: nil)
} else if let failureReason = sendMessageReasonForError(error.errorDescription) {
return StandaloneSendMessagesError(peerId: peerId, reason: failureReason)
}
return StandaloneSendMessagesError(peerId: peerId, reason: nil)
}
} else {
return .complete()
}
}
|> castError(StandaloneSendMessagesError.self)
|> switchToLatest
}
public func standaloneSendMessage(account: Account, peerId: PeerId, text: String, attributes: [MessageAttribute], media: StandaloneMedia?, replyToMessageId: MessageId?, threadId: Int32? = nil) -> Signal<Float, StandaloneSendMessageError> {
let content: Signal<StandaloneSendMessageEvent, StandaloneSendMessageError>
if let media = media {
switch media {
case let .image(data):
content = uploadedImage(account: account, data: data)
|> mapError { _ -> StandaloneSendMessageError in return .generic }
|> map { next -> StandaloneSendMessageEvent in
switch next {
case let .progress(progress):
return .progress(progress)
case let .result(media):
return .result(.media(media, text))
}
}
case let .file(data, mimeType, attributes):
content = uploadedFile(account: account, data: data, mimeType: mimeType, attributes: attributes)
|> mapError { _ -> StandaloneSendMessageError in return .generic }
|> map { next -> StandaloneSendMessageEvent in
switch next {
case let .progress(progress):
return .progress(progress)
case let .result(media):
return .result(.media(media, text))
}
}
}
} else {
content = .single(.result(.text(text)))
}
return content
|> mapToSignal { event -> Signal<Float, StandaloneSendMessageError> in
switch event {
case let .progress(progress):
return .single(progress)
case let .result(result):
let sendContent = sendMessageContent(account: account, peerId: peerId, attributes: attributes, content: result, threadId: threadId) |> map({ _ -> Float in return 1.0 })
return .single(1.0) |> then(sendContent |> mapError { _ -> StandaloneSendMessageError in })
}
}
}
private func sendMessageContent(account: Account, peerId: PeerId, attributes: [MessageAttribute], content: StandaloneMessageContent, threadId: Int32?) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
if peerId.namespace == Namespaces.Peer.SecretChat {
return .complete()
} else if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
var uniqueId: Int64 = Int64.random(in: Int64.min ... Int64.max)
//var forwardSourceInfoAttribute: ForwardSourceInfoAttribute?
var messageEntities: [Api.MessageEntity]?
var replyMessageId: Int32?
var replyToStoryId: StoryId?
var scheduleTime: Int32?
var scheduleRepeatPeriod: Int32?
var sendAsPeerId: PeerId?
var allowPaidStars: Int64?
var suggestedPost: Api.SuggestedPost?
var flags: Int32 = 0
flags |= (1 << 7)
for attribute in attributes {
if let replyAttribute = attribute as? ReplyMessageAttribute {
replyMessageId = replyAttribute.messageId.id
} else if let attribute = attribute as? ReplyStoryAttribute {
replyToStoryId = attribute.storyId
} else if let outgoingInfo = attribute as? OutgoingMessageInfoAttribute {
uniqueId = outgoingInfo.uniqueId
} else if let _ = attribute as? ForwardSourceInfoAttribute {
//forwardSourceInfoAttribute = attribute
} else if let attribute = attribute as? TextEntitiesMessageAttribute {
messageEntities = apiTextAttributeEntities(attribute, associatedPeers: SimpleDictionary())
} else if let attribute = attribute as? OutgoingContentInfoMessageAttribute {
if attribute.flags.contains(.disableLinkPreviews) {
flags |= Int32(1 << 1)
}
} else if let attribute = attribute as? OutgoingScheduleInfoMessageAttribute {
flags |= Int32(1 << 10)
scheduleTime = attribute.scheduleTime
if let repeatPeriod = attribute.repeatPeriod {
flags |= Int32(1 << 24)
scheduleRepeatPeriod = repeatPeriod
}
} else if let attribute = attribute as? SendAsMessageAttribute {
sendAsPeerId = attribute.peerId
} else if let attribute = attribute as? PaidStarsMessageAttribute {
allowPaidStars = attribute.stars.value
} else if let attribute = attribute as? SuggestedPostMessageAttribute {
suggestedPost = attribute.apiSuggestedPost(fixMinTime: Int32(Date().timeIntervalSince1970 + 10))
}
}
if let _ = messageEntities {
flags |= Int32(1 << 3)
}
var sendAsInputPeer: Api.InputPeer?
if let sendAsPeerId = sendAsPeerId, let sendAsPeer = transaction.getPeer(sendAsPeerId), let inputPeer = apiInputPeerOrSelf(sendAsPeer, accountPeerId: account.peerId) {
sendAsInputPeer = inputPeer
flags |= (1 << 13)
}
if let _ = allowPaidStars {
flags |= 1 << 21
}
let sendMessageRequest: Signal<Api.Updates, NoError>
switch content {
case let .text(text):
var replyTo: Api.InputReplyTo?
if let replyMessageId = replyMessageId {
flags |= 1 << 0
let replyFlags: Int32 = 0
replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: nil, todoItemId: nil))
} else if let replyToStoryId = replyToStoryId {
if let inputPeer = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputPeer) {
flags |= 1 << 0
replyTo = .inputReplyToStory(.init(peer: inputPeer, storyId: replyToStoryId.id))
}
} else if let threadId {
flags |= 1 << 0
replyTo = .inputReplyToMessage(.init(flags: flags, replyToMsgId: threadId, topMsgId: threadId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: nil, todoItemId: nil))
}
sendMessageRequest = account.network.request(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars, suggestedPost: nil))
|> `catch` { _ -> Signal<Api.Updates, NoError> in
return .complete()
}
case let .media(inputMedia, text):
var replyTo: Api.InputReplyTo?
if let replyMessageId = replyMessageId {
flags |= 1 << 0
let replyFlags: Int32 = 0
replyTo = .inputReplyToMessage(.init(flags: replyFlags, replyToMsgId: replyMessageId, topMsgId: nil, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: nil, todoItemId: nil))
} else if let replyToStoryId = replyToStoryId {
if let inputPeer = transaction.getPeer(replyToStoryId.peerId).flatMap(apiInputPeer) {
flags |= 1 << 0
replyTo = .inputReplyToStory(.init(peer: inputPeer, storyId: replyToStoryId.id))
}
} else if let threadId {
flags |= 1 << 0
replyTo = .inputReplyToMessage(.init(flags: flags, replyToMsgId: threadId, topMsgId: threadId, replyToPeerId: nil, quoteText: nil, quoteEntities: nil, quoteOffset: nil, monoforumPeerId: nil, todoItemId: nil))
}
if suggestedPost != nil {
flags |= 1 << 22
}
sendMessageRequest = account.network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, scheduleRepeatPeriod: scheduleRepeatPeriod, sendAs: sendAsInputPeer, quickReplyShortcut: nil, effect: nil, allowPaidStars: allowPaidStars, suggestedPost: suggestedPost))
|> `catch` { _ -> Signal<Api.Updates, NoError> in
return .complete()
}
}
return sendMessageRequest
|> mapToSignal { result -> Signal<Void, NoError> in
return .complete()
}
|> `catch` { _ -> Signal<Void, NoError> in
}
} else {
return .complete()
}
}
|> switchToLatest
}
private enum UploadMediaEvent {
case progress(Float)
case result(Api.InputMedia)
}
private func uploadedImage(account: Account, data: Data) -> Signal<UploadMediaEvent, StandaloneSendMessageError> {
return multipartUpload(network: account.network, postbox: account.postbox, source: .data(data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false)
|> mapError { _ -> StandaloneSendMessageError in return .generic }
|> map { next -> UploadMediaEvent in
switch next {
case let .inputFile(inputFileData):
let inputFile = inputFileData
return .result(Api.InputMedia.inputMediaUploadedPhoto(.init(flags: 0, file: inputFile, stickers: nil, ttlSeconds: nil)))
case .inputSecretFile:
preconditionFailure()
case let .progress(progress):
return .progress(progress)
}
}
}
private func uploadedFile(account: Account, data: Data, mimeType: String, attributes: [TelegramMediaFileAttribute]) -> Signal<UploadMediaEvent, PendingMessageUploadError> {
return multipartUpload(network: account.network, postbox: account.postbox, source: .data(data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: statsCategoryForFileWithAttributes(attributes), userContentType: nil), hintFileSize: Int64(data.count), hintFileIsLarge: false, forceNoBigParts: false)
|> mapError { _ -> PendingMessageUploadError in return .generic }
|> map { next -> UploadMediaEvent in
switch next {
case let .inputFile(inputFileData):
let inputFile = inputFileData
return .result(Api.InputMedia.inputMediaUploadedDocument(.init(flags: 0, file: inputFile, thumb: nil, mimeType: mimeType, attributes: inputDocumentAttributesFromFileAttributes(attributes), stickers: nil, videoCover: nil, videoTimestamp: nil, ttlSeconds: nil)))
case .inputSecretFile:
preconditionFailure()
case let .progress(progress):
return .progress(progress)
}
}
}
@@ -0,0 +1,215 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
public enum StandaloneUploadMediaError {
case generic
}
public struct StandaloneUploadSecretFile {
let file: Api.InputEncryptedFile
let size: Int32
let key: SecretFileEncryptionKey
}
public enum StandaloneUploadMediaThumbnailResult {
case pending
case file(Api.InputFile)
case none
var file: Api.InputFile? {
if case let .file(file) = self {
return file
} else {
return nil
}
}
}
public enum StandaloneUploadMediaResult {
case media(AnyMediaReference)
}
public enum StandaloneUploadMediaEvent {
case progress(Float)
case result(StandaloneUploadMediaResult)
}
private func uploadedThumbnail(network: Network, postbox: Postbox, data: Data) -> Signal<Api.InputFile?, StandaloneUploadMediaError> {
return multipartUpload(network: network, postbox: postbox, source: .data(data), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false)
|> mapError { _ -> StandaloneUploadMediaError in return .generic }
|> mapToSignal { result -> Signal<Api.InputFile?, StandaloneUploadMediaError> in
switch result {
case .progress:
return .complete()
case let .inputFile(inputFile):
return .single(inputFile)
case .inputSecretFile:
return .single(nil)
}
}
}
public func standaloneUploadedImage(postbox: Postbox, network: Network, peerId: PeerId, text: String, data: Data, thumbnailData: Data? = nil, dimensions: PixelDimensions) -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> {
return multipartUpload(network: network, postbox: postbox, source: .data(data), encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false)
|> mapError { _ -> StandaloneUploadMediaError in return .generic }
|> mapToSignal { next -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in
switch next {
case let .inputFile(inputFile):
return postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapError { _ -> StandaloneUploadMediaError in }
|> mapToSignal { inputPeer -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in
if let inputPeer = inputPeer {
return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedPhoto(.init(flags: 0, file: inputFile, stickers: nil, ttlSeconds: nil))))
|> mapError { _ -> StandaloneUploadMediaError in return .generic }
|> mapToSignal { media -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in
switch media {
case let .messageMediaPhoto(messageMediaPhotoData):
let photo = messageMediaPhotoData.photo
if let photo = photo {
if let mediaImage = telegramMediaImageFromApiPhoto(photo) {
return .single(.result(.media(.standalone(media: mediaImage))))
}
}
default:
break
}
return .fail(.generic)
}
} else {
return .fail(.generic)
}
}
case let .inputSecretFile(file, _, key):
return postbox.transaction { transaction -> Api.InputEncryptedChat? in
if let peer = transaction.getPeer(peerId) as? TelegramSecretChat {
return Api.InputEncryptedChat.inputEncryptedChat(.init(chatId: Int32(peer.id.id._internalGetInt64Value()), accessHash: peer.accessHash))
}
return nil
}
|> castError(StandaloneUploadMediaError.self)
|> mapToSignal { inputChat -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in
guard let inputChat = inputChat else {
return .fail(.generic)
}
return network.request(Api.functions.messages.uploadEncryptedFile(peer: inputChat, file: file))
|> mapError { _ -> StandaloneUploadMediaError in return .generic
}
|> mapToSignal { result -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in
switch result {
case let .encryptedFile(encryptedFileData):
let (id, accessHash, size, dcId) = (encryptedFileData.id, encryptedFileData.accessHash, encryptedFileData.size, encryptedFileData.dcId)
return .single(.result(.media(.standalone(media: TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: Int64.random(in: Int64.min ... Int64.max)), representations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: Int64(data.count), datacenterId: Int(dcId), key: key), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: [])))))
case .encryptedFileEmpty:
return .fail(.generic)
}
}
}
case let .progress(progress):
return .single(.progress(progress))
}
}
}
public func standaloneUploadedFile(postbox: Postbox, network: Network, peerId: PeerId, text: String, source: MultipartUploadSource, thumbnailData: Data? = nil, mimeType: String, attributes: [TelegramMediaFileAttribute], hintFileIsLarge: Bool) -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> {
let upload = multipartUpload(network: network, postbox: postbox, source: source, encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: statsCategoryForFileWithAttributes(attributes), userContentType: nil), hintFileSize: nil, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: false)
|> mapError { _ -> StandaloneUploadMediaError in return .generic }
let uploadThumbnail: Signal<StandaloneUploadMediaThumbnailResult, StandaloneUploadMediaError>
if let thumbnailData = thumbnailData {
uploadThumbnail = .single(.pending)
|> then(
uploadedThumbnail(network: network, postbox: postbox, data: thumbnailData)
|> mapError { _ -> StandaloneUploadMediaError in return .generic }
|> map { result in
if let result = result {
return .file(result)
} else {
return .none
}
}
)
} else {
uploadThumbnail = .single(.none)
}
return combineLatest(upload, uploadThumbnail)
|> mapToSignal { result, thumbnail in
switch result {
case let .progress(progress):
return .single(.progress(progress))
default:
switch thumbnail {
case .pending:
return .complete()
default:
switch result {
case let .inputFile(inputFile):
return postbox.transaction { transaction -> Api.InputPeer? in
return transaction.getPeer(peerId).flatMap(apiInputPeer)
}
|> mapError { _ -> StandaloneUploadMediaError in }
|> mapToSignal { inputPeer -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in
if let inputPeer = inputPeer {
var flags: Int32 = 0
let thumbnailFile = thumbnail.file
if let _ = thumbnailFile {
flags |= 1 << 2
}
return network.request(Api.functions.messages.uploadMedia(flags: 0, businessConnectionId: nil, peer: inputPeer, media: Api.InputMedia.inputMediaUploadedDocument(.init(flags: flags, file: inputFile, thumb: thumbnailFile, mimeType: mimeType, attributes: inputDocumentAttributesFromFileAttributes(attributes), stickers: nil, videoCover: nil, videoTimestamp: nil, ttlSeconds: nil))))
|> mapError { _ -> StandaloneUploadMediaError in return .generic }
|> mapToSignal { media -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in
switch media {
case let .messageMediaDocument(messageMediaDocumentData):
let (document, altDocuments) = (messageMediaDocumentData.document, messageMediaDocumentData.altDocuments)
if let document = document {
if let mediaFile = telegramMediaFileFromApiDocument(document, altDocuments: altDocuments) {
return .single(.result(.media(.standalone(media: mediaFile))))
}
}
default:
break
}
return .fail(.generic)
}
} else {
return .fail(.generic)
}
}
case let .inputSecretFile(file, _, key):
return postbox.transaction { transaction -> Api.InputEncryptedChat? in
if let peer = transaction.getPeer(peerId) as? TelegramSecretChat {
return Api.InputEncryptedChat.inputEncryptedChat(.init(chatId: Int32(peer.id.id._internalGetInt64Value()), accessHash: peer.accessHash))
}
return nil
}
|> castError(StandaloneUploadMediaError.self)
|> mapToSignal { inputChat -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in
guard let inputChat = inputChat else {
return .fail(.generic)
}
return network.request(Api.functions.messages.uploadEncryptedFile(peer: inputChat, file: file))
|> mapError { _ -> StandaloneUploadMediaError in return .generic }
|> mapToSignal { result -> Signal<StandaloneUploadMediaEvent, StandaloneUploadMediaError> in
switch result {
case let .encryptedFile(encryptedFileData):
let (id, accessHash, size, dcId) = (encryptedFileData.id, encryptedFileData.accessHash, encryptedFileData.size, encryptedFileData.dcId)
let media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: size, datacenterId: Int(dcId), key: key), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: size, attributes: attributes, alternativeRepresentations: [])
return .single(.result(.media(.standalone(media: media))))
case .encryptedFileEmpty:
return .fail(.generic)
}
}
}
case .progress:
return .never()
}
}
}
}
}
@@ -0,0 +1,363 @@
import Foundation
import Postbox
import MtProtoKit
private func messageKey(key: SecretChatKey, msgKey: UnsafeRawPointer, mode: SecretChatEncryptionMode) -> (aesKey: Data, aesIv: Data) {
switch mode {
case .v1:
let x: Int = 0
var sha1AData = Data()
sha1AData.count = 16 + 32
sha1AData.withUnsafeMutableBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
memcpy(bytes, msgKey, 16)
memcpy(bytes.advanced(by: 16), key.key.memory.advanced(by: x), 32)
}
let sha1A = MTSha1(sha1AData)
var sha1BData = Data()
sha1BData.count = 16 + 16 + 16
sha1BData.withUnsafeMutableBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
memcpy(bytes, key.key.memory.advanced(by: 32 + x), 16)
memcpy(bytes.advanced(by: 16), msgKey, 16)
memcpy(bytes.advanced(by: 16 + 16), key.key.memory.advanced(by: 48 + x), 16)
}
let sha1B = MTSha1(sha1BData)
var sha1CData = Data()
sha1CData.count = 32 + 16
sha1CData.withUnsafeMutableBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
memcpy(bytes, key.key.memory.advanced(by: 64 + x), 32)
memcpy(bytes.advanced(by: 32), msgKey, 16)
}
let sha1C = MTSha1(sha1CData)
var sha1DData = Data()
sha1DData.count = 16 + 32
sha1DData.withUnsafeMutableBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
memcpy(bytes, msgKey, 16)
memcpy(bytes.advanced(by: 16), key.key.memory.advanced(by: 96 + x), 32)
}
let sha1D = MTSha1(sha1DData)
var aesKey = Data()
aesKey.count = 8 + 12 + 12
aesKey.withUnsafeMutableBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
sha1A.withUnsafeBytes { sha1A -> Void in
memcpy(bytes, sha1A.baseAddress!.assumingMemoryBound(to: UInt8.self), 8)
}
sha1B.withUnsafeBytes { sha1B -> Void in
memcpy(bytes.advanced(by: 8), sha1B.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: 8), 12)
}
sha1C.withUnsafeBytes { sha1C -> Void in
memcpy(bytes.advanced(by: 8 + 12), sha1C.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: 4), 12)
}
}
var aesIv = Data()
aesIv.count = 12 + 8 + 4 + 8
aesIv.withUnsafeMutableBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
sha1A.withUnsafeBytes { sha1A -> Void in
memcpy(bytes, sha1A.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: 8), 12)
}
sha1B.withUnsafeBytes { sha1B -> Void in
memcpy(bytes.advanced(by: 12), sha1B.baseAddress!.assumingMemoryBound(to: UInt8.self), 8)
}
sha1C.withUnsafeBytes { sha1C -> Void in
memcpy(bytes.advanced(by: 12 + 8), sha1C.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: 16), 4)
}
sha1D.withUnsafeBytes { sha1D -> Void in
memcpy(bytes.advanced(by: 12 + 8 + 4), sha1D.baseAddress!.assumingMemoryBound(to: UInt8.self), 8)
}
}
return (aesKey, aesIv)
case let .v2(role):
var xValue: Int
switch role {
case .creator:
xValue = 0
case .participant:
xValue = 8
}
var sha256_a_data = Data()
sha256_a_data.append(msgKey.assumingMemoryBound(to: UInt8.self), count: 16)
sha256_a_data.append(key.key.memory.assumingMemoryBound(to: UInt8.self).advanced(by: xValue), count: 36)
let sha256_a = MTSha256(sha256_a_data)
var sha256_b_data = Data()
sha256_b_data.append(key.key.memory.assumingMemoryBound(to: UInt8.self).advanced(by: 40 + xValue), count: 36)
sha256_b_data.append(msgKey.assumingMemoryBound(to: UInt8.self), count: 16)
let sha256_b = MTSha256(sha256_b_data)
var aesKey = Data()
aesKey.append(sha256_a.subdata(in: 0 ..< (0 + 8)))
aesKey.append(sha256_b.subdata(in: 8 ..< (8 + 16)))
aesKey.append(sha256_a.subdata(in: 24 ..< (24 + 8)))
var aesIv = Data()
aesIv.append(sha256_b.subdata(in: 0 ..< (0 + 8)))
aesIv.append(sha256_a.subdata(in: 8 ..< (8 + 16)))
aesIv.append(sha256_b.subdata(in: 24 ..< (24 + 8)))
return (aesKey, aesIv)
}
}
private func constTimeIsEqual(data1: Data, data2: Data) -> Bool {
if data1.count != data2.count {
return false
}
var isEqual = true
for i in 0 ..< data1.count {
if data1[i] != data2[i] {
isEqual = false
}
}
return isEqual
}
func withDecryptedMessageContents(parameters: SecretChatEncryptionParameters, data: MemoryBuffer) -> MemoryBuffer? {
assert(parameters.key.key.length == 256)
if data.length < 4 + 16 + 16 {
return nil
}
let msgKey = data.memory.advanced(by: 8)
switch parameters.mode {
case .v1:
let (aesKey, aesIv) = messageKey(key: parameters.key, msgKey: msgKey, mode: parameters.mode)
let decryptedData = MTAesDecrypt(Data(bytes: data.memory.advanced(by: 8 + 16), count: data.length - (8 + 16)), aesKey, aesIv)!
if decryptedData.count < 4 * 3 {
return nil
}
var payloadLength: Int32 = 0
decryptedData.withUnsafeBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
memcpy(&payloadLength, bytes, 4)
}
let paddingLength = decryptedData.count - (Int(payloadLength) + 4)
if Int(payloadLength) > decryptedData.count - 4 || paddingLength > 16 {
return nil
}
let calculatedMsgKeyData = MTSubdataSha1(decryptedData, 0, UInt(payloadLength) + 4)
let msgKeyMatches = calculatedMsgKeyData.withUnsafeBytes { rawBytes -> Bool in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
return memcmp(bytes.advanced(by: calculatedMsgKeyData.count - 16), msgKey, 16) == 0
}
if !msgKeyMatches {
return nil
}
let result = decryptedData.withUnsafeBytes { rawBytes -> Data in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
return Data(bytes: bytes.advanced(by: 4), count: Int(payloadLength))
}
return MemoryBuffer(data: result)
case let .v2(role):
let senderRole: SecretChatRole
switch role {
case .creator:
senderRole = .participant
case .participant:
senderRole = .creator
}
let (aesKey, aesIv) = messageKey(key: parameters.key, msgKey: msgKey, mode: .v2(role: senderRole))
let decryptedData = MTAesDecrypt(Data(bytes: data.memory.advanced(by: 8 + 16), count: data.length - (8 + 16)), aesKey, aesIv)!
if decryptedData.count < 4 * 3 {
return nil
}
var payloadLength: Int32 = 0
decryptedData.withUnsafeBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
memcpy(&payloadLength, bytes, 4)
}
let paddingLength = decryptedData.count - (Int(payloadLength) + 4)
let xValue: Int
switch role {
case .creator:
xValue = 8
case .participant:
xValue = 0
}
var keyLargeData = Data()
keyLargeData.append(parameters.key.key.memory.assumingMemoryBound(to: UInt8.self).advanced(by: 88 + xValue), count: 32)
keyLargeData.append(decryptedData)
let keyLarge = MTSha256(keyLargeData)
let localMessageKey = keyLarge.subdata(in: 8 ..< (8 + 16))
let msgKeyData = Data(bytes: msgKey.assumingMemoryBound(to: UInt8.self), count: 16)
if Int(payloadLength) <= 0 || Int(payloadLength) > decryptedData.count - 4 || paddingLength < 12 || paddingLength > 1024 {
if !constTimeIsEqual(data1: localMessageKey, data2: msgKeyData) {
Logger.shared.log("SecretChatEncryption", "message key doesn't match (length check)")
}
return nil
}
if !constTimeIsEqual(data1: localMessageKey, data2: msgKeyData) {
Logger.shared.log("SecretChatEncryption", "message key doesn't match")
return nil
}
let result = decryptedData.withUnsafeBytes { rawBytes -> Data in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
return Data(bytes: bytes.advanced(by: 4), count: Int(payloadLength))
}
return MemoryBuffer(data: result)
}
}
enum SecretChatEncryptionMode {
case v1
case v2(role: SecretChatRole)
}
struct SecretChatEncryptionParameters {
let key: SecretChatKey
let mode: SecretChatEncryptionMode
}
func encryptedMessageContents(parameters: SecretChatEncryptionParameters, data: MemoryBuffer) -> Data {
var payloadLength: Int32 = Int32(data.length)
var payloadData = Data()
withUnsafeBytes(of: &payloadLength, { bytes -> Void in
payloadData.append(bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4)
})
payloadData.append(data.memory.assumingMemoryBound(to: UInt8.self), count: data.length)
switch parameters.mode {
case .v1:
var msgKey = MTSha1(payloadData)
msgKey.replaceSubrange(0 ..< (msgKey.count - 16), with: Data())
let randomBuf = malloc(16)!
defer {
free(randomBuf)
}
let randomBytes = randomBuf.assumingMemoryBound(to: UInt8.self)
arc4random_buf(randomBuf, 16)
var randomIndex = 0
while payloadData.count % 16 != 0 {
payloadData.append(randomBytes.advanced(by: randomIndex), count: 1)
randomIndex += 1
}
let (aesKey, aesIv) = msgKey.withUnsafeBytes { rawBytes -> (Data, Data) in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
return messageKey(key: parameters.key, msgKey: bytes, mode: parameters.mode)
}
let encryptedData = MTAesEncrypt(payloadData, aesKey, aesIv)!
var encryptedPayload = Data()
var keyFingerprint: Int64 = parameters.key.fingerprint
withUnsafeBytes(of: &keyFingerprint, { bytes -> Void in
encryptedPayload.append(bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 8)
})
encryptedPayload.append(msgKey)
encryptedPayload.append(encryptedData)
return encryptedPayload
case let .v2(role):
var randomBytes = Data(count: 128)
let randomBytesCount = randomBytes.count
randomBytes.withUnsafeMutableBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: Int8.self)
arc4random_buf(bytes, randomBytesCount)
}
var decryptedData = payloadData
var take = 0
while take < 12 {
decryptedData.append(randomBytes.subdata(in: take ..< (take + 1)))
take += 1
}
while decryptedData.count % 16 != 0 {
decryptedData.append(randomBytes.subdata(in: take ..< (take + 1)))
take += 1
}
var remainingCount = Int(arc4random_uniform(UInt32(72 + 1 - take)))
while remainingCount % 16 != 0 {
remainingCount -= 1
}
for _ in 0 ..< remainingCount {
decryptedData.append(randomBytes.subdata(in: take ..< (take + 1)))
take += 1
}
var xValue: Int
switch role {
case .creator:
xValue = 0
case .participant:
xValue = 8
}
var keyData = Data()
keyData.append(parameters.key.key.memory.assumingMemoryBound(to: UInt8.self).advanced(by: 88 + xValue), count: 32)
keyData.append(decryptedData)
let keyLarge = MTSha256(keyData)
let msgKey = keyLarge.subdata(in: 8 ..< (8 + 16))
let (aesKey, aesIv) = msgKey.withUnsafeBytes { rawBytes -> (Data, Data) in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
return messageKey(key: parameters.key, msgKey: bytes, mode: parameters.mode)
}
let encryptedData = MTAesEncrypt(decryptedData, aesKey, aesIv)!
var encryptedPayload = Data()
var keyFingerprint: Int64 = parameters.key.fingerprint
withUnsafeBytes(of: &keyFingerprint, { bytes -> Void in
encryptedPayload.append(bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 8)
})
encryptedPayload.append(msgKey)
encryptedPayload.append(encryptedData)
return encryptedPayload
}
}
@@ -0,0 +1,35 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
func validatedEncryptionConfig(postbox: Postbox, network: Network) -> Signal<SecretChatEncryptionConfig, NoError> {
return network.request(Api.functions.messages.getDhConfig(version: 0, randomLength: 0))
|> retryRequest
|> mapToSignal { result -> Signal<SecretChatEncryptionConfig, NoError> in
switch result {
case let .dhConfig(dhConfigData):
let (g, p, version) = (dhConfigData.g, dhConfigData.p, dhConfigData.version)
if !MTCheckIsSafeG(UInt32(g)) {
Logger.shared.log("SecretChatEncryptionConfig", "Invalid g")
return .complete()
}
if !MTCheckMod(network.encryptionProvider, p.makeData(), UInt32(g), network.context.keychain) {
Logger.shared.log("SecretChatEncryptionConfig", "Invalid p or g")
return .complete()
}
if !MTCheckIsSafePrime(network.encryptionProvider, p.makeData(), network.context.keychain) {
Logger.shared.log("SecretChatEncryptionConfig", "Invalid p")
return .never()
}
return .single(SecretChatEncryptionConfig(g: g, p: MemoryBuffer(p), version: version))
case .dhConfigNotModified:
assertionFailure()
return .never()
}
}
}
@@ -0,0 +1,16 @@
import Foundation
import Postbox
import TelegramApi
extension SecretChatFileReference {
convenience init?(_ file: Api.EncryptedFile) {
switch file {
case let .encryptedFile(encryptedFileData):
let (id, accessHash, size, dcId, keyFingerprint) = (encryptedFileData.id, encryptedFileData.accessHash, encryptedFileData.size, encryptedFileData.dcId, encryptedFileData.keyFingerprint)
self.init(id: id, accessHash: accessHash, size: size, datacenterId: dcId, keyFingerprint: keyFingerprint)
case .encryptedFileEmpty:
return nil
}
}
}
@@ -0,0 +1,26 @@
import Foundation
import Postbox
import TelegramApi
private func keyFingerprintFromBytes(_ bytes: Buffer) -> Int64 {
if let memory = bytes.data, bytes.size >= 4 {
var fingerprint: Int64 = 0
memcpy(&fingerprint, memory, 8)
return fingerprint
}
return 0
}
extension SecretChatIncomingEncryptedOperation {
convenience init(message: Api.EncryptedMessage) {
switch message {
case let .encryptedMessage(encryptedMessageData):
let (randomId, chatId, date, bytes, file) = (encryptedMessageData.randomId, encryptedMessageData.chatId, encryptedMessageData.date, encryptedMessageData.bytes, encryptedMessageData.file)
self.init(peerId: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(Int64(chatId))), globallyUniqueId: randomId, timestamp: date, type: .message, keyFingerprint: keyFingerprintFromBytes(bytes), contents: MemoryBuffer(bytes), mediaFileReference: SecretChatFileReference(file))
case let .encryptedMessageService(encryptedMessageServiceData):
let (randomId, chatId, date, bytes) = (encryptedMessageServiceData.randomId, encryptedMessageServiceData.chatId, encryptedMessageServiceData.date, encryptedMessageServiceData.bytes)
self.init(peerId: PeerId(namespace: Namespaces.Peer.SecretChat, id: PeerId.Id._internalFromInt64Value(Int64(chatId))), globallyUniqueId: randomId, timestamp: date, type: .service, keyFingerprint: keyFingerprintFromBytes(bytes), contents: MemoryBuffer(bytes), mediaFileReference: nil)
}
}
}
@@ -0,0 +1,63 @@
import Foundation
import Postbox
import SwiftSignalKit
private let topSupportedLayer: SecretChatSequenceBasedLayer = .layer144
func secretChatCommonSupportedLayer(remoteLayer: Int32) -> SecretChatSequenceBasedLayer {
switch remoteLayer {
case 46:
return .layer46
case 73:
return .layer73
case 101:
return .layer101
case 144:
return .layer144
default:
return topSupportedLayer
}
}
func secretChatAddReportCurrentLayerSupportOperationAndUpdateRequestedLayer(transaction: Transaction, peerId: PeerId, state: SecretChatState) -> SecretChatState {
switch state.embeddedState {
case .basicLayer:
var updatedState = state
updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: .layer8, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), layerSupport: topSupportedLayer.rawValue), state: updatedState)
return updatedState
case let .sequenceBasedLayer(sequenceState):
var updatedState = state
updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: sequenceState.layerNegotiationState.activeLayer.secretChatLayer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), layerSupport: topSupportedLayer.rawValue), state: updatedState)
updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceState.withUpdatedLayerNegotiationState(sequenceState.layerNegotiationState.withUpdatedLocallyRequestedLayer(topSupportedLayer.rawValue))))
return updatedState
default:
return state
}
}
func secretChatCheckLayerNegotiationIfNeeded(transaction: Transaction, peerId: PeerId, state: SecretChatState) -> SecretChatState {
switch state.embeddedState {
case let .sequenceBasedLayer(sequenceState):
if sequenceState.layerNegotiationState.activeLayer != topSupportedLayer {
var updatedState = state
if let remotelyRequestedLayer = sequenceState.layerNegotiationState.remotelyRequestedLayer {
let updatedSequenceState = sequenceState.withUpdatedLayerNegotiationState(sequenceState.layerNegotiationState.withUpdatedActiveLayer(secretChatCommonSupportedLayer(remoteLayer: remotelyRequestedLayer)))
updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(updatedSequenceState))
}
if (sequenceState.layerNegotiationState.locallyRequestedLayer ?? 0) < topSupportedLayer.rawValue {
updatedState = secretChatAddReportCurrentLayerSupportOperationAndUpdateRequestedLayer(transaction: transaction, peerId: peerId, state: updatedState)
}
return updatedState
} else {
return state
}
case .basicLayer:
return state
default:
return state
}
}
@@ -0,0 +1,33 @@
import Foundation
import Postbox
import TelegramApi
extension SecretChatOutgoingFileReference {
init?(_ apiFile: Api.InputEncryptedFile) {
switch apiFile {
case let .inputEncryptedFile(inputEncryptedFileData):
let (id, accessHash) = (inputEncryptedFileData.id, inputEncryptedFileData.accessHash)
self = .remote(id: id, accessHash: accessHash)
case let .inputEncryptedFileBigUploaded(inputEncryptedFileBigUploadedData):
let (id, parts, keyFingerprint) = (inputEncryptedFileBigUploadedData.id, inputEncryptedFileBigUploadedData.parts, inputEncryptedFileBigUploadedData.keyFingerprint)
self = .uploadedLarge(id: id, partCount: parts, keyFingerprint: keyFingerprint)
case let .inputEncryptedFileUploaded(inputEncryptedFileUploadedData):
let (id, parts, md5Checksum, keyFingerprint) = (inputEncryptedFileUploadedData.id, inputEncryptedFileUploadedData.parts, inputEncryptedFileUploadedData.md5Checksum, inputEncryptedFileUploadedData.keyFingerprint)
self = .uploadedRegular(id: id, partCount: parts, md5Digest: md5Checksum, keyFingerprint: keyFingerprint)
case .inputEncryptedFileEmpty:
return nil
}
}
var apiInputFile: Api.InputEncryptedFile {
switch self {
case let .remote(id, accessHash):
return .inputEncryptedFile(.init(id: id, accessHash: accessHash))
case let .uploadedRegular(id, partCount, md5Digest, keyFingerprint):
return .inputEncryptedFileUploaded(.init(id: id, parts: partCount, md5Checksum: md5Digest, keyFingerprint: keyFingerprint))
case let .uploadedLarge(id, partCount, keyFingerprint):
return .inputEncryptedFileBigUploaded(.init(id: id, parts: partCount, keyFingerprint: keyFingerprint))
}
}
}
@@ -0,0 +1,135 @@
import Foundation
import Postbox
import MtProtoKit
import EncryptionProvider
private let keyUseCountThreshold: Int32 = 100
func secretChatInitiateRekeySessionIfNeeded(transaction: Transaction, peerId: PeerId, state: SecretChatState) -> SecretChatState {
switch state.embeddedState {
case let .sequenceBasedLayer(sequenceState):
if let _ = sequenceState.rekeyState {
return state
}
let tagLocalIndex = transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing)
let canonicalIndex = sequenceState.canonicalOutgoingOperationIndex(tagLocalIndex)
if let key = state.keychain.latestKey(validForSequenceBasedCanonicalIndex: canonicalIndex), key.useCount >= keyUseCountThreshold {
let sessionId = Int64.random(in: Int64.min ... Int64.max)
let aBytes = malloc(256)!
let _ = SecRandomCopyBytes(nil, 256, aBytes.assumingMemoryBound(to: UInt8.self))
let a = MemoryBuffer(memory: aBytes, capacity: 256, length: 256, freeWhenDone: true)
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SecretChatOutgoingOperation(contents: .pfsRequestKey(layer: sequenceState.layerNegotiationState.activeLayer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), rekeySessionId: sessionId, a: a), mutable: true, delivered: false))
return state.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceState.withUpdatedRekeyState(SecretChatRekeySessionState(id: sessionId, data: .requesting))))
}
default:
break
}
return state
}
func secretChatAdvanceRekeySessionIfNeeded(encryptionProvider: EncryptionProvider, transaction: Transaction, peerId: PeerId, state: SecretChatState, action: SecretChatRekeyServiceAction) -> SecretChatState {
switch state.embeddedState {
case let .sequenceBasedLayer(sequenceState):
switch action {
case let .pfsAbortSession(rekeySessionId):
if let rekeySession = sequenceState.rekeyState, rekeySession.id == rekeySessionId {
return state.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceState.withUpdatedRekeyState(nil)))
}
case let .pfsAcceptKey(rekeySessionId, gB, remoteKeyFingerprint):
if let rekeySession = sequenceState.rekeyState, rekeySession.id == rekeySessionId {
switch rekeySession.data {
case let .requested(a, config):
//var gValue: Int32 = config.g.byteSwapped
let p = config.p.makeData()
let aData = a.makeData()
if !MTCheckIsSafeGAOrB(encryptionProvider, gB.makeData(), p) {
return state.withUpdatedEmbeddedState(.terminated)
}
var key = MTExp(encryptionProvider, gB.makeData(), aData, p)!
if key.count > 256 {
key.count = 256
} else {
while key.count < 256 {
key.insert(0, at: 0)
}
}
let keyHash = MTSha1(key)
var keyFingerprint: Int64 = 0
keyHash.withUnsafeBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
memcpy(&keyFingerprint, bytes.advanced(by: keyHash.count - 8), 8)
}
assert(remoteKeyFingerprint == keyFingerprint)
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SecretChatOutgoingOperation(contents: .pfsCommitKey(layer: sequenceState.layerNegotiationState.activeLayer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), rekeySessionId: rekeySession.id, keyFingerprint: keyFingerprint), mutable: true, delivered: false))
let keyValidityOperationIndex = transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing)
let keyValidityOperationCanonicalIndex = sequenceState.canonicalOutgoingOperationIndex(keyValidityOperationIndex)
return state.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceState.withUpdatedRekeyState(nil))).withUpdatedKeychain(state.keychain.withUpdatedKey(fingerprint: keyFingerprint, { _ in
return SecretChatKey(fingerprint: keyFingerprint, key: MemoryBuffer(data: key), validity: .sequenceBasedIndexRange(fromCanonicalIndex: keyValidityOperationCanonicalIndex), useCount: 0)
}))
default:
assertionFailure()
break
}
}
case let .pfsCommitKey(rekeySessionId, keyFingerprint):
if let rekeySession = sequenceState.rekeyState, rekeySession.id == rekeySessionId {
if case let .accepted(key, localKeyFingerprint) = rekeySession.data, keyFingerprint == localKeyFingerprint {
let keyValidityOperationIndex = transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing)
let keyValidityOperationCanonicalIndex = sequenceState.canonicalOutgoingOperationIndex(keyValidityOperationIndex)
let updatedState = state.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceState.withUpdatedRekeyState(nil))).withUpdatedKeychain(state.keychain.withUpdatedKey(fingerprint: keyFingerprint, { _ in
return SecretChatKey(fingerprint: keyFingerprint, key: key, validity: .sequenceBasedIndexRange(fromCanonicalIndex: keyValidityOperationCanonicalIndex), useCount: 0)
}))
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SecretChatOutgoingOperation(contents: .noop(layer: sequenceState.layerNegotiationState.activeLayer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max)), mutable: true, delivered: false))
return updatedState
} else {
assertionFailure()
}
} else {
assertionFailure()
}
case let .pfsRequestKey(rekeySessionId, gA):
var acceptSession = true
if let rekeySession = sequenceState.rekeyState {
switch rekeySession.data {
case .requesting, .requested:
if rekeySessionId < rekeySession.id {
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SecretChatOutgoingOperation(contents: .pfsAbortSession(layer: sequenceState.layerNegotiationState.activeLayer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), rekeySessionId: rekeySession.id), mutable: true, delivered: false))
} else {
acceptSession = false
}
case .accepting, .accepted:
break
}
}
if acceptSession {
let bBytes = malloc(256)!
let _ = SecRandomCopyBytes(nil, 256, bBytes.assumingMemoryBound(to: UInt8.self))
let b = MemoryBuffer(memory: bBytes, capacity: 256, length: 256, freeWhenDone: true)
let rekeySession = SecretChatRekeySessionState(id: rekeySessionId, data: .accepting)
transaction.operationLogAddEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SecretChatOutgoingOperation(contents: .pfsAcceptKey(layer: sequenceState.layerNegotiationState.activeLayer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), rekeySessionId: rekeySession.id, gA: gA, b: b), mutable: true, delivered: false))
return state.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceState.withUpdatedRekeyState(rekeySession)))
}
}
default:
break
}
return state
}
@@ -0,0 +1,40 @@
import Foundation
import Postbox
import SwiftSignalKit
func _internal_setSecretChatMessageAutoremoveTimeoutInteractively(account: Account, peerId: PeerId, timeout: Int32?) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Void in
_internal_setSecretChatMessageAutoremoveTimeoutInteractively(transaction: transaction, account: account, peerId: peerId, timeout: timeout)
}
}
func _internal_setSecretChatMessageAutoremoveTimeoutInteractively(transaction: Transaction, account: Account, peerId: PeerId, timeout: Int32?) {
if let peer = transaction.getPeer(peerId) as? TelegramSecretChat, let state = transaction.getPeerChatState(peerId) as? SecretChatState {
if state.messageAutoremoveTimeout != timeout {
let updatedPeer = peer.withUpdatedMessageAutoremoveTimeout(timeout)
let updatedState = state.withUpdatedMessageAutoremoveTimeout(timeout)
if !updatedPeer.isEqual(peer) {
updatePeersCustom(transaction: transaction, peers: [updatedPeer], update: { $1 })
}
if updatedState != state {
transaction.setPeerChatState(peerId, state: updatedState)
}
let _ = enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: [(true, .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.messageAutoremoveTimeoutUpdated(period: timeout == nil ? 0 : timeout!, autoSettingSource: nil))), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))])
}
}
}
func _internal_addSecretChatMessageScreenshot(account: Account, peerId: PeerId) -> Signal<Void, NoError> {
return account.postbox.transaction { transaction -> Void in
if let _ = transaction.getPeer(peerId) as? TelegramSecretChat, let state = transaction.getPeerChatState(peerId) as? SecretChatState {
switch state.embeddedState {
case .handshake, .terminated:
return
default:
break
}
let _ = enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: [(true, .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaAction(action: TelegramMediaActionType.historyScreenshot)), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []))])
}
}
}
@@ -0,0 +1,144 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
import EncryptionProvider
struct SecretChatRequestData {
let g: Int32
let p: MemoryBuffer
let a: MemoryBuffer
}
func updateSecretChat(encryptionProvider: EncryptionProvider, accountPeerId: PeerId, transaction: Transaction, mediaBox: MediaBox, chat: Api.EncryptedChat, requestData: SecretChatRequestData?) {
let currentPeer = transaction.getPeer(chat.peerId) as? TelegramSecretChat
let currentState = transaction.getPeerChatState(chat.peerId) as? SecretChatState
let settings = transaction.getPreferencesEntry(key: PreferencesKeys.secretChatSettings)?.get(SecretChatSettings.self) ?? SecretChatSettings.defaultSettings
assert((currentPeer == nil) == (currentState == nil))
switch chat {
case let .encryptedChat(encryptedChatData):
let (adminId, gAOrB) = (encryptedChatData.adminId, encryptedChatData.gAOrB)
if let currentPeer = currentPeer, let currentState = currentState, adminId == accountPeerId.id._internalGetInt64Value() {
if case let .handshake(handshakeState) = currentState.embeddedState, case let .requested(_, p, a) = handshakeState {
let pData = p.makeData()
let aData = a.makeData()
if !MTCheckIsSafeGAOrB(encryptionProvider, gAOrB.makeData(), pData) {
var updatedState = currentState
updatedState = updatedState.withUpdatedEmbeddedState(.terminated)
transaction.setPeerChatState(chat.peerId, state: updatedState)
return
}
var key = MTExp(encryptionProvider, gAOrB.makeData(), aData, pData)!
if key.count > 256 {
key.count = 256
} else {
while key.count < 256 {
key.insert(0, at: 0)
}
}
let keyHash = MTSha1(key)
var keyFingerprint: Int64 = 0
keyHash.withUnsafeBytes { rawBytes -> Void in
let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
memcpy(&keyFingerprint, bytes.advanced(by: keyHash.count - 8), 8)
}
var updatedState = currentState
updatedState = updatedState.withUpdatedKeychain(SecretChatKeychain(keys: [SecretChatKey(fingerprint: keyFingerprint, key: MemoryBuffer(data: key), validity: .indefinite, useCount: 0)]))
updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: .layer73, locallyRequestedLayer: nil, remotelyRequestedLayer: nil), rekeyState: nil, baseIncomingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: currentPeer.id, tag: OperationLogTags.SecretIncomingDecrypted), baseOutgoingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: currentPeer.id, tag: OperationLogTags.SecretOutgoing), topProcessedCanonicalIncomingOperationIndex: nil)))
updatedState = updatedState.withUpdatedKeyFingerprint(SecretChatKeyFingerprint(sha1: SecretChatKeySha1Fingerprint(digest: sha1Digest(key)), sha256: SecretChatKeySha256Fingerprint(digest: sha256Digest(key))))
updatedState = secretChatAddReportCurrentLayerSupportOperationAndUpdateRequestedLayer(transaction: transaction, peerId: currentPeer.id, state: updatedState)
transaction.setPeerChatState(currentPeer.id, state: updatedState)
updatePeersCustom(transaction: transaction, peers: [currentPeer.withUpdatedEmbeddedState(updatedState.embeddedState.peerState)], update: { _, updated in
return updated
})
} else {
Logger.shared.log("State", "got encryptedChat, but chat is not in handshake state")
}
} else {
Logger.shared.log("State", "got encryptedChat, but peer or state don't exist or account is not creator")
}
case let .encryptedChatDiscarded(encryptedChatDiscardedData):
let flags = encryptedChatDiscardedData.flags
if let currentPeer = currentPeer, let currentState = currentState {
let isRemoved = (flags & (1 << 0)) != 0
let state = currentState.withUpdatedEmbeddedState(.terminated)
let peer = currentPeer.withUpdatedEmbeddedState(state.embeddedState.peerState)
updatePeersCustom(transaction: transaction, peers: [peer], update: { _, updated in return updated })
transaction.setPeerChatState(peer.id, state: state)
transaction.operationLogRemoveAllEntries(peerId: peer.id, tag: OperationLogTags.SecretOutgoing)
if isRemoved {
let peerId = currentPeer.id
_internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, threadId: nil, namespaces: .all)
transaction.updatePeerChatListInclusion(peerId, inclusion: .notIncluded)
transaction.removeOrderedItemListItem(collectionId: Namespaces.OrderedItemList.RecentlySearchedPeerIds, itemId: RecentPeerItemId(peerId).rawValue)
}
} else {
Logger.shared.log("State", "got encryptedChatDiscarded, but peer doesn't exist")
}
case .encryptedChatEmpty:
break
case let .encryptedChatRequested(encryptedChatRequestedData):
let (folderId, accessHash, date, adminId, participantId, gA) = (encryptedChatRequestedData.folderId, encryptedChatRequestedData.accessHash, encryptedChatRequestedData.date, encryptedChatRequestedData.adminId, encryptedChatRequestedData.participantId, encryptedChatRequestedData.gA)
if currentPeer == nil && participantId == accountPeerId.id._internalGetInt64Value() {
if settings.acceptOnThisDevice {
let state = SecretChatState(role: .participant, embeddedState: .handshake(.accepting), keychain: SecretChatKeychain(keys: []), keyFingerprint: nil, messageAutoremoveTimeout: nil)
let bBytes = malloc(256)!
let randomStatus = SecRandomCopyBytes(nil, 256, bBytes.assumingMemoryBound(to: UInt8.self))
let b = MemoryBuffer(memory: bBytes, capacity: 256, length: 256, freeWhenDone: true)
if randomStatus == 0 {
let updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: chat.peerId, operation: .initialHandshakeAccept(gA: MemoryBuffer(gA), accessHash: accessHash, b: b), state: state)
transaction.setPeerChatState(chat.peerId, state: updatedState)
let peer = TelegramSecretChat(id: chat.peerId, creationDate: date, regularPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(adminId)), accessHash: accessHash, role: updatedState.role, embeddedState: updatedState.embeddedState.peerState, messageAutoremoveTimeout: nil)
updatePeersCustom(transaction: transaction, peers: [peer], update: { _, updated in return updated })
if folderId != nil {
transaction.updatePeerChatListInclusion(peer.id, inclusion: .ifHasMessagesOrOneOf(groupId: Namespaces.PeerGroup.archive, pinningIndex: nil, minTimestamp: date))
}
transaction.resetIncomingReadStates([peer.id: [
Namespaces.Message.SecretIncoming: .indexBased(maxIncomingReadIndex: MessageIndex.lowerBound(peerId: peer.id), maxOutgoingReadIndex: MessageIndex.lowerBound(peerId: peer.id), count: 0, markedUnread: false),
Namespaces.Message.Local: .indexBased(maxIncomingReadIndex: MessageIndex.lowerBound(peerId: peer.id), maxOutgoingReadIndex: MessageIndex.lowerBound(peerId: peer.id), count: 0, markedUnread: false)
]
])
} else {
assertionFailure()
}
} else {
Logger.shared.log("State", "accepting secret chats disabled on this device")
}
} else {
Logger.shared.log("State", "got encryptedChatRequested, but peer already exists or this account is creator")
}
case let .encryptedChatWaiting(encryptedChatWaitingData):
let (accessHash, date, adminId, participantId) = (encryptedChatWaitingData.accessHash, encryptedChatWaitingData.date, encryptedChatWaitingData.adminId, encryptedChatWaitingData.participantId)
if let requestData = requestData, currentPeer == nil && adminId == accountPeerId.id._internalGetInt64Value() {
let state = SecretChatState(role: .creator, embeddedState: .handshake(.requested(g: requestData.g, p: requestData.p, a: requestData.a)), keychain: SecretChatKeychain(keys: []), keyFingerprint: nil, messageAutoremoveTimeout: nil)
let peer = TelegramSecretChat(id: chat.peerId, creationDate: date, regularPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(participantId)), accessHash: accessHash, role: state.role, embeddedState: state.embeddedState.peerState, messageAutoremoveTimeout: nil)
updatePeersCustom(transaction: transaction, peers: [peer], update: { _, updated in return updated })
transaction.setPeerChatState(peer.id, state: state)
transaction.resetIncomingReadStates([peer.id: [
Namespaces.Message.SecretIncoming: .indexBased(maxIncomingReadIndex: MessageIndex.lowerBound(peerId: peer.id), maxOutgoingReadIndex: MessageIndex.lowerBound(peerId: peer.id), count: 0, markedUnread: false),
Namespaces.Message.Local: .indexBased(maxIncomingReadIndex: MessageIndex.lowerBound(peerId: peer.id), maxOutgoingReadIndex: MessageIndex.lowerBound(peerId: peer.id), count: 0, markedUnread: false)
]
])
} else {
Logger.shared.log("State", "got encryptedChatWaiting, but peer already exists or this account is not creator")
}
}
}
@@ -0,0 +1,54 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
public func updateAutodownloadSettingsInteractively(accountManager: AccountManager<TelegramAccountManagerTypes>, _ f: @escaping (AutodownloadSettings) -> AutodownloadSettings) -> Signal<Void, NoError> {
return accountManager.transaction { transaction -> Void in
transaction.updateSharedData(SharedDataKeys.autodownloadSettings, { entry in
let currentSettings: AutodownloadSettings
if let entry = entry?.get(AutodownloadSettings.self) {
currentSettings = entry
} else {
currentSettings = AutodownloadSettings.defaultSettings
}
return PreferencesEntry(f(currentSettings))
})
}
}
extension AutodownloadPresetSettings {
init(apiAutodownloadSettings: Api.AutoDownloadSettings) {
switch apiAutodownloadSettings {
case let .autoDownloadSettings(autoDownloadSettingsData):
let (flags, photoSizeMax, videoSizeMax, fileSizeMax, videoUploadMaxbitrate, _, _) = (autoDownloadSettingsData.flags, autoDownloadSettingsData.photoSizeMax, autoDownloadSettingsData.videoSizeMax, autoDownloadSettingsData.fileSizeMax, autoDownloadSettingsData.videoUploadMaxbitrate, autoDownloadSettingsData.smallQueueActiveOperationsMax, autoDownloadSettingsData.largeQueueActiveOperationsMax)
self.init(disabled: (flags & (1 << 0)) != 0, photoSizeMax: Int64(photoSizeMax), videoSizeMax: videoSizeMax, fileSizeMax: fileSizeMax, preloadLargeVideo: (flags & (1 << 1)) != 0, lessDataForPhoneCalls: (flags & (1 << 3)) != 0, videoUploadMaxbitrate: videoUploadMaxbitrate)
}
}
}
extension AutodownloadSettings {
init(apiAutodownloadSettings: Api.account.AutoDownloadSettings) {
switch apiAutodownloadSettings {
case let .autoDownloadSettings(autoDownloadSettingsData):
let (low, medium, high) = (autoDownloadSettingsData.low, autoDownloadSettingsData.medium, autoDownloadSettingsData.high)
self.init(lowPreset: AutodownloadPresetSettings(apiAutodownloadSettings: low), mediumPreset: AutodownloadPresetSettings(apiAutodownloadSettings: medium), highPreset: AutodownloadPresetSettings(apiAutodownloadSettings: high))
}
}
}
func apiAutodownloadPresetSettings(_ autodownloadPresetSettings: AutodownloadPresetSettings) -> Api.AutoDownloadSettings {
var flags: Int32 = 0
if autodownloadPresetSettings.disabled {
flags |= (1 << 0)
}
if autodownloadPresetSettings.preloadLargeVideo {
flags |= (1 << 1)
}
if autodownloadPresetSettings.lessDataForPhoneCalls {
flags |= (1 << 3)
}
return .autoDownloadSettings(Api.AutoDownloadSettings.Cons_autoDownloadSettings(flags: flags, photoSizeMax: Int32(autodownloadPresetSettings.photoSizeMax), videoSizeMax: autodownloadPresetSettings.videoSizeMax, fileSizeMax: autodownloadPresetSettings.fileSizeMax, videoUploadMaxbitrate: autodownloadPresetSettings.videoUploadMaxbitrate, smallQueueActiveOperationsMax: 0, largeQueueActiveOperationsMax: 0))
}
@@ -0,0 +1,31 @@
import Foundation
import Postbox
import SwiftSignalKit
public func updateCacheStorageSettingsInteractively(accountManager: AccountManager<TelegramAccountManagerTypes>, _ f: @escaping (CacheStorageSettings) -> CacheStorageSettings) -> Signal<Void, NoError> {
return accountManager.transaction { transaction -> Void in
transaction.updateSharedData(SharedDataKeys.cacheStorageSettings, { entry in
let currentSettings: CacheStorageSettings
if let entry = entry?.get(CacheStorageSettings.self) {
currentSettings = entry
} else {
currentSettings = CacheStorageSettings.defaultSettings
}
return PreferencesEntry(f(currentSettings))
})
}
}
public func updateAccountSpecificCacheStorageSettingsInteractively(postbox: Postbox, _ f: @escaping (AccountSpecificCacheStorageSettings) -> AccountSpecificCacheStorageSettings) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
transaction.updatePreferencesEntry(key: PreferencesKeys.accountSpecificCacheStorageSettings, { entry in
let currentSettings: AccountSpecificCacheStorageSettings
if let entry = entry?.get(AccountSpecificCacheStorageSettings.self) {
currentSettings = entry
} else {
currentSettings = AccountSpecificCacheStorageSettings.defaultSettings
}
return PreferencesEntry(f(currentSettings))
})
}
}
@@ -0,0 +1,20 @@
import Foundation
import Postbox
import SwiftSignalKit
import MtProtoKit
public func updateContentPrivacySettings(postbox: Postbox, _ f: @escaping (ContentPrivacySettings) -> ContentPrivacySettings) -> Signal<Void, NoError> {
return postbox.transaction { transaction -> Void in
var updated: ContentPrivacySettings?
transaction.updatePreferencesEntry(key: PreferencesKeys.contentPrivacySettings, { current in
if let current = current?.get(ContentPrivacySettings.self) {
updated = f(current)
return PreferencesEntry(updated)
} else {
updated = f(ContentPrivacySettings.defaultSettings)
return PreferencesEntry(updated)
}
})
}
}
@@ -0,0 +1,91 @@
import Foundation
import Postbox
import TelegramApi
import SwiftSignalKit
public struct ContentSettings: Equatable {
public static var `default` = ContentSettings(ignoreContentRestrictionReasons: [], addContentRestrictionReasons: [], appConfiguration: AppConfiguration.defaultValue)
public var ignoreContentRestrictionReasons: Set<String>
public var addContentRestrictionReasons: [String]
public var appConfiguration: AppConfiguration
public init(ignoreContentRestrictionReasons: Set<String>, addContentRestrictionReasons: [String], appConfiguration: AppConfiguration) {
self.ignoreContentRestrictionReasons = ignoreContentRestrictionReasons
self.addContentRestrictionReasons = addContentRestrictionReasons
self.appConfiguration = appConfiguration
}
}
extension ContentSettings {
init(appConfiguration: AppConfiguration) {
var reasons: [String] = []
var addContentRestrictionReasons: [String] = []
if let data = appConfiguration.data {
if let reasonsData = data["ignore_restriction_reasons"] as? [String] {
reasons = reasonsData
}
if let addContentRestrictionReasonsData = data["restriction_add_platforms"] as? [String] {
addContentRestrictionReasons = addContentRestrictionReasonsData
}
}
// MARK: Swiftgram
reasons += appConfiguration.sgWebSettings.user.expandedContentReasons()
self.init(ignoreContentRestrictionReasons: Set(reasons), addContentRestrictionReasons: addContentRestrictionReasons, appConfiguration: appConfiguration)
}
}
public func getContentSettings(transaction: Transaction) -> ContentSettings {
let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration)?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
return ContentSettings(appConfiguration: appConfiguration)
}
public func getContentSettings(postbox: Postbox) -> Signal<ContentSettings, NoError> {
return postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|> map { view -> ContentSettings in
let appConfiguration: AppConfiguration = view.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? AppConfiguration.defaultValue
return ContentSettings(appConfiguration: appConfiguration)
}
|> distinctUntilChanged
}
public struct ContentSettingsConfiguration: Equatable {
public static var `default` = ContentSettingsConfiguration(sensitiveContentEnabled: false, canAdjustSensitiveContent: false)
public var sensitiveContentEnabled: Bool
public var canAdjustSensitiveContent: Bool
public init(sensitiveContentEnabled: Bool, canAdjustSensitiveContent: Bool) {
self.sensitiveContentEnabled = sensitiveContentEnabled
self.canAdjustSensitiveContent = canAdjustSensitiveContent
}
}
public func contentSettingsConfiguration(network: Network) -> Signal<ContentSettingsConfiguration, NoError> {
return network.request(Api.functions.account.getContentSettings())
|> map { result -> ContentSettingsConfiguration in
switch result {
case let .contentSettings(contentSettingsData):
let flags = contentSettingsData.flags
return ContentSettingsConfiguration(sensitiveContentEnabled: (flags & (1 << 0)) != 0, canAdjustSensitiveContent: (flags & (1 << 1)) != 0)
}
}
|> `catch` { _ -> Signal<ContentSettingsConfiguration, NoError> in
return .single(.default)
}
}
public func updateRemoteContentSettingsConfiguration(postbox: Postbox, network: Network, sensitiveContentEnabled: Bool) -> Signal<Never, NoError> {
var flags: Int32 = 0
if sensitiveContentEnabled {
flags |= 1 << 0
}
return network.request(Api.functions.account.setContentSettings(flags: flags))
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Never, NoError> in
return updateAppConfigurationOnce(postbox: postbox, network: network)
|> ignoreValues
}
}
@@ -0,0 +1,54 @@
import Foundation
import Postbox
import TelegramApi
extension MessageNotificationSettings {
init(apiSettings: Api.PeerNotifySettings) {
switch apiSettings {
case let .peerNotifySettings(peerNotifySettingsData):
let (showPreviews, muteUntil, iosSound, desktopSound, storiesMuted, storiesHideSender, storiesIosSound, storiesDesktopSound) = (peerNotifySettingsData.showPreviews, peerNotifySettingsData.muteUntil, peerNotifySettingsData.iosSound, peerNotifySettingsData.otherSound, peerNotifySettingsData.storiesMuted, peerNotifySettingsData.storiesHideSender, peerNotifySettingsData.storiesIosSound, peerNotifySettingsData.storiesOtherSound)
let sound: Api.NotificationSound?
let storiesSound: Api.NotificationSound?
#if os(iOS)
sound = iosSound
storiesSound = storiesIosSound
#elseif os(macOS)
sound = desktopSound
storiesSound = storiesDesktopSound
#endif
let displayPreviews: Bool
if let showPreviews = showPreviews, case .boolFalse = showPreviews {
displayPreviews = false
} else {
displayPreviews = true
}
let storiesMutedValue: PeerStoryNotificationSettings.Mute
if let storiesMuted = storiesMuted {
storiesMutedValue = storiesMuted == .boolTrue ? .muted : .unmuted
} else {
storiesMutedValue = .default
}
var storiesHideSenderValue: PeerStoryNotificationSettings.HideSender
if let storiesHideSender = storiesHideSender {
storiesHideSenderValue = storiesHideSender == .boolTrue ? .hide : .show
} else {
storiesHideSenderValue = .default
}
self = MessageNotificationSettings(
enabled: muteUntil == 0,
displayPreviews: displayPreviews,
sound: PeerMessageSound(apiSound: sound ?? .notificationSoundDefault),
storySettings: PeerStoryNotificationSettings(
mute: storiesMutedValue,
hideSender: storiesHideSenderValue,
sound: PeerMessageSound(apiSound: sound ?? .notificationSoundDefault)
)
)
}
}
}
@@ -0,0 +1,17 @@
import Foundation
import Postbox
public func currentLimitsConfiguration(transaction: Transaction) -> LimitsConfiguration {
if let entry = transaction.getPreferencesEntry(key: PreferencesKeys.limitsConfiguration)?.get(LimitsConfiguration.self) {
return entry
} else {
return LimitsConfiguration.defaultValue
}
}
func updateLimitsConfiguration(transaction: Transaction, configuration: LimitsConfiguration) {
if currentLimitsConfiguration(transaction: transaction) != configuration {
transaction.setPreferencesEntry(key: PreferencesKeys.limitsConfiguration, value: PreferencesEntry(configuration))
}
}
@@ -0,0 +1,25 @@
import Foundation
import Postbox
import SwiftSignalKit
import MtProtoKit
public func updateLoggingSettings(accountManager: AccountManager<TelegramAccountManagerTypes>, _ f: @escaping (LoggingSettings) -> LoggingSettings) -> Signal<Void, NoError> {
return accountManager.transaction { transaction -> Void in
var updated: LoggingSettings?
transaction.updateSharedData(SharedDataKeys.loggingSettings, { current in
if let current = current?.get(LoggingSettings.self) {
updated = f(current)
return PreferencesEntry(updated)
} else {
updated = f(LoggingSettings.defaultSettings)
return PreferencesEntry(updated)
}
})
if let updated = updated {
Logger.shared.logToFile = updated.logToFile
Logger.shared.logToConsole = updated.logToConsole
Logger.shared.redactSensitiveData = updated.redactSensitiveData
}
}
}

Some files were not shown because too many files have changed in this diff Show More