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
+25
View File
@@ -0,0 +1,25 @@
fastlane/README.md
fastlane/report.xml
fastlane/test_output/*
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.xcscmblueprint
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
.DS_Store
*.dSYM
*.dSYM.zip
*.ipa
*/xcuserdata/*
TelegramCore.xcodeproj/*
+49
View File
@@ -0,0 +1,49 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
sgdeps = [
"//Swiftgram/SwiftSoup:SwiftSoup",
"//Swiftgram/SGSimpleSettings:SGSimpleSettings",
"//Swiftgram/SGTranslationLangFix:SGTranslationLangFix",
"//Swiftgram/SGWebSettingsScheme:SGWebSettingsScheme",
"//Swiftgram/SGGHSettingsScheme:SGGHSettingsScheme",
"//Swiftgram/SGConfig:SGConfig",
"//Swiftgram/SGLogging:SGLogging",
# MARK: GLEGram
"//GLEGram/SGDeletedMessages:SGDeletedMessages",
]
sgsrc = [
"//Swiftgram/SGIQTP:SGIQTP",
]
swift_library(
name = "TelegramCore",
module_name = "TelegramCore",
srcs = sgsrc + glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = sgdeps + [
"//submodules/TelegramApi:TelegramApi",
"//submodules/MtProtoKit:MtProtoKit",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Postbox:Postbox",
"//submodules/CloudData:CloudData",
"//submodules/EncryptionProvider:EncryptionProvider",
"//submodules/CryptoUtils:CryptoUtils",
"//submodules/NetworkLogging:NetworkLogging",
"//submodules/Reachability:Reachability",
"//submodules/ManagedFile:ManagedFile",
"//submodules/Utils/RangeSet:RangeSet",
"//submodules/Utils/DarwinDirStat",
"//submodules/Emoji",
"//submodules/TelegramCore/FlatBuffers",
"//submodules/TelegramCore/FlatSerialization",
"//submodules/MurMurHash32"
],
visibility = [
"//visibility:public",
],
)
+17
View File
@@ -0,0 +1,17 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "FlatBuffers",
module_name = "FlatBuffers",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-suppress-warnings",
],
deps = [
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,27 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "FlatBuffers",
platforms: [.macOS(.v10_13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "FlatBuffers",
targets: ["FlatBuffers"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "FlatBuffers",
dependencies: [],
path: "Sources"),
]
)
@@ -0,0 +1,542 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// `ByteBuffer` is the interface that stores the data for a `Flatbuffers` object
/// it allows users to write and read data directly from memory thus the use of its
/// functions should be used
@frozen
public struct ByteBuffer {
/// Storage is a container that would hold the memory pointer to solve the issue of
/// deallocating the memory that was held by (memory: UnsafeMutableRawPointer)
@usableFromInline
final class Storage {
// This storage doesn't own the memory, therefore, we won't deallocate on deinit.
private let unowned: Bool
/// pointer to the start of the buffer object in memory
var memory: UnsafeMutableRawPointer
/// Capacity of UInt8 the buffer can hold
var capacity: Int
@usableFromInline
init(count: Int, alignment: Int) {
memory = UnsafeMutableRawPointer.allocate(
byteCount: count,
alignment: alignment)
capacity = count
unowned = false
}
@usableFromInline
init(memory: UnsafeMutableRawPointer, capacity: Int, unowned: Bool) {
self.memory = memory
self.capacity = capacity
self.unowned = unowned
}
deinit {
if !unowned {
memory.deallocate()
}
}
@usableFromInline
func copy(from ptr: UnsafeRawPointer, count: Int) {
assert(
!unowned,
"copy should NOT be called on a buffer that is built by assumingMemoryBound")
memory.copyMemory(from: ptr, byteCount: count)
}
@usableFromInline
func initialize(for size: Int) {
assert(
!unowned,
"initalize should NOT be called on a buffer that is built by assumingMemoryBound")
memset(memory, 0, size)
}
/// Reallocates the buffer incase the object to be written doesnt fit in the current buffer
/// - Parameter size: Size of the current object
@usableFromInline
func reallocate(_ size: Int, writerSize: Int, alignment: Int) {
let currentWritingIndex = capacity &- writerSize
while capacity <= writerSize &+ size {
capacity = capacity << 1
}
/// solution take from Apple-NIO
capacity = capacity.convertToPowerofTwo
let newData = UnsafeMutableRawPointer.allocate(
byteCount: capacity,
alignment: alignment)
memset(newData, 0, capacity &- writerSize)
memcpy(
newData.advanced(by: capacity &- writerSize),
memory.advanced(by: currentWritingIndex),
writerSize)
memory.deallocate()
memory = newData
}
}
@usableFromInline var _storage: Storage
/// The size of the elements written to the buffer + their paddings
private var _writerSize: Int = 0
/// Alignment of the current memory being written to the buffer
var alignment = 1
/// Current Index which is being used to write to the buffer, it is written from the end to the start of the buffer
var writerIndex: Int { _storage.capacity &- _writerSize }
/// Reader is the position of the current Writer Index (capacity - size)
public var reader: Int { writerIndex }
/// Current size of the buffer
public var size: UOffset { UOffset(_writerSize) }
/// Public Pointer to the buffer object in memory. This should NOT be modified for any reason
public var memory: UnsafeMutableRawPointer { _storage.memory }
/// Current capacity for the buffer
public var capacity: Int { _storage.capacity }
/// Crash if the trying to read an unaligned buffer instead of allowing users to read them.
public let allowReadingUnalignedBuffers: Bool
/// Constructor that creates a Flatbuffer object from a UInt8
/// - Parameter
/// - bytes: Array of UInt8
/// - allowReadingUnalignedBuffers: allow reading from unaligned buffer
public init(
bytes: [UInt8],
allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false)
{
var b = bytes
_storage = Storage(count: bytes.count, alignment: alignment)
_writerSize = _storage.capacity
allowReadingUnalignedBuffers = allowUnalignedBuffers
b.withUnsafeMutableBytes { bufferPointer in
_storage.copy(from: bufferPointer.baseAddress!, count: bytes.count)
}
}
#if !os(WASI)
/// Constructor that creates a Flatbuffer from the Swift Data type object
/// - Parameter
/// - data: Swift data Object
/// - allowReadingUnalignedBuffers: allow reading from unaligned buffer
public init(
data: Data,
allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false)
{
var b = data
_storage = Storage(count: data.count, alignment: alignment)
_writerSize = _storage.capacity
allowReadingUnalignedBuffers = allowUnalignedBuffers
b.withUnsafeMutableBytes { bufferPointer in
_storage.copy(from: bufferPointer.baseAddress!, count: data.count)
}
}
#endif
/// Constructor that creates a Flatbuffer instance with a size
/// - Parameter:
/// - size: Length of the buffer
/// - allowReadingUnalignedBuffers: allow reading from unaligned buffer
init(initialSize size: Int) {
let size = size.convertToPowerofTwo
_storage = Storage(count: size, alignment: alignment)
_storage.initialize(for: size)
allowReadingUnalignedBuffers = false
}
#if swift(>=5.0) && !os(WASI)
/// Constructor that creates a Flatbuffer object from a ContiguousBytes
/// - Parameters:
/// - contiguousBytes: Binary stripe to use as the buffer
/// - count: amount of readable bytes
/// - allowReadingUnalignedBuffers: allow reading from unaligned buffer
public init<Bytes: ContiguousBytes>(
contiguousBytes: Bytes,
count: Int,
allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false)
{
_storage = Storage(count: count, alignment: alignment)
_writerSize = _storage.capacity
allowReadingUnalignedBuffers = allowUnalignedBuffers
contiguousBytes.withUnsafeBytes { buf in
_storage.copy(from: buf.baseAddress!, count: buf.count)
}
}
#endif
/// Constructor that creates a Flatbuffer from unsafe memory region without copying
/// - Parameter:
/// - assumingMemoryBound: The unsafe memory region
/// - capacity: The size of the given memory region
/// - allowReadingUnalignedBuffers: allow reading from unaligned buffer
public init(
assumingMemoryBound memory: UnsafeMutableRawPointer,
capacity: Int,
allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false)
{
_storage = Storage(memory: memory, capacity: capacity, unowned: true)
_writerSize = capacity
allowReadingUnalignedBuffers = allowUnalignedBuffers
}
/// Creates a copy of the buffer that's being built by calling sizedBuffer
/// - Parameters:
/// - memory: Current memory of the buffer
/// - count: count of bytes
/// - allowReadingUnalignedBuffers: allow reading from unaligned buffer
init(
memory: UnsafeMutableRawPointer,
count: Int,
allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false)
{
_storage = Storage(count: count, alignment: alignment)
_storage.copy(from: memory, count: count)
_writerSize = _storage.capacity
allowReadingUnalignedBuffers = allowUnalignedBuffers
}
/// Creates a copy of the existing flatbuffer, by copying it to a different memory.
/// - Parameters:
/// - memory: Current memory of the buffer
/// - count: count of bytes
/// - removeBytes: Removes a number of bytes from the current size
/// - allowReadingUnalignedBuffers: allow reading from unaligned buffer
init(
memory: UnsafeMutableRawPointer,
count: Int,
removing removeBytes: Int,
allowReadingUnalignedBuffers allowUnalignedBuffers: Bool = false)
{
_storage = Storage(count: count, alignment: alignment)
_storage.copy(from: memory, count: count)
_writerSize = removeBytes
allowReadingUnalignedBuffers = allowUnalignedBuffers
}
/// Fills the buffer with padding by adding to the writersize
/// - Parameter padding: Amount of padding between two to be serialized objects
@inline(__always)
@usableFromInline
mutating func fill(padding: Int) {
assert(padding >= 0, "Fill should be larger than or equal to zero")
ensureSpace(size: padding)
_writerSize = _writerSize &+ (MemoryLayout<UInt8>.size &* padding)
}
/// Adds an array of type Scalar to the buffer memory
/// - Parameter elements: An array of Scalars
@inline(__always)
@usableFromInline
mutating func push<T: Scalar>(elements: [T]) {
elements.withUnsafeBytes { ptr in
ensureSpace(size: ptr.count)
memcpy(
_storage.memory.advanced(by: writerIndex &- ptr.count),
ptr.baseAddress!,
ptr.count)
_writerSize = _writerSize &+ ptr.count
}
}
/// Adds an array of type Scalar to the buffer memory
/// - Parameter elements: An array of Scalars
@inline(__always)
@usableFromInline
mutating func push<T: NativeStruct>(elements: [T]) {
elements.withUnsafeBytes { ptr in
ensureSpace(size: ptr.count)
memcpy(
_storage.memory.advanced(by: writerIndex &- ptr.count),
ptr.baseAddress!,
ptr.count)
_writerSize = _writerSize &+ ptr.count
}
}
/// Adds a `ContiguousBytes` to buffer memory
/// - Parameter value: bytes to copy
#if swift(>=5.0) && !os(WASI)
@inline(__always)
@usableFromInline
mutating func push(bytes: ContiguousBytes) {
bytes.withUnsafeBytes { ptr in
ensureSpace(size: ptr.count)
memcpy(
_storage.memory.advanced(by: writerIndex &- ptr.count),
ptr.baseAddress!,
ptr.count)
_writerSize = _writerSize &+ ptr.count
}
}
#endif
/// Adds an object of type NativeStruct into the buffer
/// - Parameters:
/// - value: Object that will be written to the buffer
/// - size: size to subtract from the WriterIndex
@usableFromInline
@inline(__always)
mutating func push<T: NativeStruct>(struct value: T, size: Int) {
ensureSpace(size: size)
withUnsafePointer(to: value) {
memcpy(
_storage.memory.advanced(by: writerIndex &- size),
$0,
size)
_writerSize = _writerSize &+ size
}
}
/// Adds an object of type Scalar into the buffer
/// - Parameters:
/// - value: Object that will be written to the buffer
/// - len: Offset to subtract from the WriterIndex
@inline(__always)
@usableFromInline
mutating func push<T: Scalar>(value: T, len: Int) {
ensureSpace(size: len)
withUnsafePointer(to: value) {
memcpy(
_storage.memory.advanced(by: writerIndex &- len),
$0,
len)
_writerSize = _writerSize &+ len
}
}
/// Adds a string to the buffer using swift.utf8 object
/// - Parameter str: String that will be added to the buffer
/// - Parameter len: length of the string
@inline(__always)
@usableFromInline
mutating func push(string str: String, len: Int) {
ensureSpace(size: len)
if str.utf8
.withContiguousStorageIfAvailable({ self.push(bytes: $0, len: len) }) !=
nil
{
} else {
let utf8View = str.utf8
for c in utf8View.reversed() {
push(value: c, len: 1)
}
}
}
/// Writes a string to Bytebuffer using UTF8View
/// - Parameters:
/// - bytes: Pointer to the view
/// - len: Size of string
@usableFromInline
@inline(__always)
mutating func push(
bytes: UnsafeBufferPointer<String.UTF8View.Element>,
len: Int) -> Bool
{
memcpy(
_storage.memory.advanced(by: writerIndex &- len),
bytes.baseAddress!,
len)
_writerSize = _writerSize &+ len
return true
}
/// Write stores an object into the buffer directly or indirectly.
///
/// Direct: ignores the capacity of buffer which would mean we are referring to the direct point in memory
/// indirect: takes into respect the current capacity of the buffer (capacity - index), writing to the buffer from the end
/// - Parameters:
/// - value: Value that needs to be written to the buffer
/// - index: index to write to
/// - direct: Should take into consideration the capacity of the buffer
@inline(__always)
func write<T>(value: T, index: Int, direct: Bool = false) {
var index = index
if !direct {
index = _storage.capacity &- index
}
assert(index < _storage.capacity, "Write index is out of writing bound")
assert(index >= 0, "Writer index should be above zero")
withUnsafePointer(to: value) {
memcpy(
_storage.memory.advanced(by: index),
$0,
MemoryLayout<T>.size)
}
}
/// Makes sure that buffer has enouch space for each of the objects that will be written into it
/// - Parameter size: size of object
@discardableResult
@usableFromInline
@inline(__always)
mutating func ensureSpace(size: Int) -> Int {
if size &+ _writerSize > _storage.capacity {
_storage.reallocate(size, writerSize: _writerSize, alignment: alignment)
}
assert(size < FlatBufferMaxSize, "Buffer can't grow beyond 2 Gigabytes")
return size
}
/// pops the written VTable if it's already written into the buffer
/// - Parameter size: size of the `VTable`
@usableFromInline
@inline(__always)
mutating func pop(_ size: Int) {
assert(
(_writerSize &- size) > 0,
"New size should NOT be a negative number")
memset(_storage.memory.advanced(by: writerIndex), 0, _writerSize &- size)
_writerSize = size
}
/// Clears the current size of the buffer
@inline(__always)
mutating public func clearSize() {
_writerSize = 0
}
/// Clears the current instance of the buffer, replacing it with new memory
@inline(__always)
mutating public func clear() {
_writerSize = 0
alignment = 1
_storage.initialize(for: _storage.capacity)
}
/// Reads an object from the buffer
/// - Parameters:
/// - def: Type of the object
/// - position: the index of the object in the buffer
@inline(__always)
public func read<T>(def: T.Type, position: Int) -> T {
if allowReadingUnalignedBuffers {
return _storage.memory.advanced(by: position).loadUnaligned(as: T.self)
}
return _storage.memory.advanced(by: position).load(as: T.self)
}
/// Reads a slice from the memory assuming a type of T
/// - Parameters:
/// - index: index of the object to be read from the buffer
/// - count: count of bytes in memory
@inline(__always)
public func readSlice<T>(
index: Int,
count: Int) -> [T]
{
assert(
index + count <= _storage.capacity,
"Reading out of bounds is illegal")
let start = _storage.memory.advanced(by: index)
.assumingMemoryBound(to: T.self)
let array = UnsafeBufferPointer(start: start, count: count)
return Array(array)
}
#if !os(WASI)
/// Reads a string from the buffer and encodes it to a swift string
/// - Parameters:
/// - index: index of the string in the buffer
/// - count: length of the string
/// - type: Encoding of the string
@inline(__always)
public func readString(
at index: Int,
count: Int,
type: String.Encoding = .utf8) -> String?
{
assert(
index + count <= _storage.capacity,
"Reading out of bounds is illegal")
let start = _storage.memory.advanced(by: index)
.assumingMemoryBound(to: UInt8.self)
let bufprt = UnsafeBufferPointer(start: start, count: count)
return String(bytes: Array(bufprt), encoding: type)
}
#else
/// Reads a string from the buffer and encodes it to a swift string
/// - Parameters:
/// - index: index of the string in the buffer
/// - count: length of the string
@inline(__always)
public func readString(
at index: Int,
count: Int) -> String?
{
assert(
index + count <= _storage.capacity,
"Reading out of bounds is illegal")
let start = _storage.memory.advanced(by: index)
.assumingMemoryBound(to: UInt8.self)
let bufprt = UnsafeBufferPointer(start: start, count: count)
return String(cString: bufprt.baseAddress!)
}
#endif
/// Creates a new Flatbuffer object that's duplicated from the current one
/// - Parameter removeBytes: the amount of bytes to remove from the current Size
@inline(__always)
public func duplicate(removing removeBytes: Int = 0) -> ByteBuffer {
assert(removeBytes > 0, "Can NOT remove negative bytes")
assert(
removeBytes < _storage.capacity,
"Can NOT remove more bytes than the ones allocated")
return ByteBuffer(
memory: _storage.memory,
count: _storage.capacity,
removing: _writerSize &- removeBytes)
}
/// Returns the written bytes into the ``ByteBuffer``
public var underlyingBytes: [UInt8] {
let cp = capacity &- writerIndex
let start = memory.advanced(by: writerIndex)
.bindMemory(to: UInt8.self, capacity: cp)
let ptr = UnsafeBufferPointer<UInt8>(start: start, count: cp)
return Array(ptr)
}
/// SkipPrefix Skips the first 4 bytes in case one of the following
/// functions are called `getPrefixedSizeCheckedRoot` & `getPrefixedSizeRoot`
/// which allows us to skip the first 4 bytes instead of recreating the buffer
@discardableResult
@usableFromInline
@inline(__always)
mutating func skipPrefix() -> Int32 {
_writerSize = _writerSize &- MemoryLayout<Int32>.size
return read(def: Int32.self, position: 0)
}
}
extension ByteBuffer: CustomDebugStringConvertible {
public var debugDescription: String {
"""
buffer located at: \(_storage.memory), with capacity of \(_storage.capacity)
{ writerSize: \(_writerSize), readerSize: \(reader), writerIndex: \(
writerIndex) }
"""
}
}
@@ -0,0 +1,114 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// A boolean to see if the system is littleEndian
let isLitteEndian: Bool = {
let number: UInt32 = 0x12345678
return number == number.littleEndian
}()
/// Constant for the file id length
let FileIdLength = 4
/// Type aliases
public typealias Byte = UInt8
public typealias UOffset = UInt32
public typealias SOffset = Int32
public typealias VOffset = UInt16
/// Maximum size for a buffer
public let FlatBufferMaxSize = UInt32
.max << ((MemoryLayout<SOffset>.size * 8 - 1) - 1)
/// Protocol that All Scalars should conform to
///
/// Scalar is used to conform all the numbers that can be represented in a FlatBuffer. It's used to write/read from the buffer.
public protocol Scalar: Equatable {
associatedtype NumericValue
var convertedEndian: NumericValue { get }
}
extension Scalar where Self: Verifiable {}
extension Scalar where Self: FixedWidthInteger {
/// Converts the value from BigEndian to LittleEndian
///
/// Converts values to little endian on machines that work with BigEndian, however this is NOT TESTED yet.
public var convertedEndian: NumericValue {
self as! Self.NumericValue
}
}
extension Double: Scalar, Verifiable {
public typealias NumericValue = UInt64
public var convertedEndian: UInt64 {
bitPattern.littleEndian
}
}
extension Float32: Scalar, Verifiable {
public typealias NumericValue = UInt32
public var convertedEndian: UInt32 {
bitPattern.littleEndian
}
}
extension Bool: Scalar, Verifiable {
public var convertedEndian: UInt8 {
self == true ? 1 : 0
}
public typealias NumericValue = UInt8
}
extension Int: Scalar, Verifiable {
public typealias NumericValue = Int
}
extension Int8: Scalar, Verifiable {
public typealias NumericValue = Int8
}
extension Int16: Scalar, Verifiable {
public typealias NumericValue = Int16
}
extension Int32: Scalar, Verifiable {
public typealias NumericValue = Int32
}
extension Int64: Scalar, Verifiable {
public typealias NumericValue = Int64
}
extension UInt8: Scalar, Verifiable {
public typealias NumericValue = UInt8
}
extension UInt16: Scalar, Verifiable {
public typealias NumericValue = UInt16
}
extension UInt32: Scalar, Verifiable {
public typealias NumericValue = UInt32
}
extension UInt64: Scalar, Verifiable {
public typealias NumericValue = UInt64
}
public func FlatBuffersVersion_24_12_23() {}
@@ -0,0 +1,55 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// Enum is a protocol that all flatbuffers enums should conform to
/// Since it allows us to get the actual `ByteSize` and `Value` from
/// a swift enum.
public protocol Enum {
/// associatedtype that the type of the enum should conform to
associatedtype T: Scalar & Verifiable
/// Size of the current associatedtype in the enum
static var byteSize: Int { get }
/// The current value the enum hosts
var value: T { get }
}
extension Enum where Self: Verifiable {
/// Verifies that the current value is which the bounds of the buffer, and if
/// the current `Value` is aligned properly
/// - Parameters:
/// - verifier: Verifier that hosts the buffer
/// - position: Current position within the buffer
/// - type: The type of the object to be verified
/// - Throws: Errors coming from `inBuffer` function
public static func verify<T>(
_ verifier: inout Verifier,
at position: Int,
of type: T.Type) throws where T: Verifiable
{
try verifier.inBuffer(position: position, of: type.self)
}
}
/// UnionEnum is a Protocol that allows us to create Union type of enums
/// and their value initializers. Since an `init` was required by
/// the verifier
public protocol UnionEnum: Enum {
init?(value: T) throws
}
@@ -0,0 +1,925 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// ``FlatBufferBuilder`` builds a `FlatBuffer` through manipulating its internal state.
///
/// This is done by creating a ``ByteBuffer`` that hosts the incoming data and
/// has a hardcoded growth limit of `2GiB` which is set by the Flatbuffers standards.
///
/// ```swift
/// var builder = FlatBufferBuilder()
/// ```
/// The builder should be always created as a variable, since it would be passed into the writers
///
@frozen
public struct FlatBufferBuilder {
/// Storage for the Vtables used in the buffer are stored in here, so they would be written later in EndTable
@usableFromInline internal var _vtableStorage = VTableStorage()
/// Flatbuffer data will be written into
@usableFromInline internal var _bb: ByteBuffer
/// Reference Vtables that were already written to the buffer
private var _vtables: [UOffset] = []
/// A check if the buffer is being written into by a different table
private var isNested = false
/// Dictonary that stores a map of all the strings that were written to the buffer
private var stringOffsetMap: [String: Offset] = [:]
/// A check to see if finish(::) was ever called to retreive data object
private var finished = false
/// A check to see if the buffer should serialize Default values
private var serializeDefaults: Bool
/// Current alignment for the buffer
var _minAlignment: Int = 0 {
didSet {
_bb.alignment = _minAlignment
}
}
/// Gives a read access to the buffer's size
public var size: UOffset { _bb.size }
#if !os(WASI)
/// Data representation of the buffer
///
/// Should only be used after ``finish(offset:addPrefix:)`` is called
public var data: Data {
assert(finished, "Data shouldn't be called before finish()")
return Data(
bytes: _bb.memory.advanced(by: _bb.writerIndex),
count: _bb.capacity &- _bb.writerIndex)
}
#endif
/// Returns the underlying bytes in the ``ByteBuffer``
///
/// Note: This should be used with caution.
public var fullSizedByteArray: [UInt8] {
let ptr = UnsafeBufferPointer(
start: _bb.memory.assumingMemoryBound(to: UInt8.self),
count: _bb.capacity)
return Array(ptr)
}
/// Returns the written bytes into the ``ByteBuffer``
///
/// Should only be used after ``finish(offset:addPrefix:)`` is called
public var sizedByteArray: [UInt8] {
assert(finished, "Data shouldn't be called before finish()")
return _bb.underlyingBytes
}
/// Returns the original ``ByteBuffer``
///
/// Returns the current buffer that was just created
/// with the offsets, and data written to it.
public var buffer: ByteBuffer { _bb }
/// Returns a newly created sized ``ByteBuffer``
///
/// returns a new buffer that is sized to the data written
/// to the main buffer
public var sizedBuffer: ByteBuffer {
assert(finished, "Data shouldn't be called before finish()")
return ByteBuffer(
memory: _bb.memory.advanced(by: _bb.reader),
count: Int(_bb.size))
}
// MARK: - Init
/// Initialize the buffer with a size
/// - Parameters:
/// - initialSize: Initial size for the buffer
/// - force: Allows default to be serialized into the buffer
///
/// This initializes a new builder with an initialSize that would initialize
/// a new ``ByteBuffer``. ``FlatBufferBuilder`` by default doesnt serialize defaults
/// however the builder can be force by passing true for `serializeDefaults`
public init(
initialSize: Int32 = 1024,
serializeDefaults force: Bool = false)
{
assert(initialSize > 0, "Size should be greater than zero!")
guard isLitteEndian else {
fatalError(
"Reading/Writing a buffer in big endian machine is not supported on swift")
}
serializeDefaults = force
_bb = ByteBuffer(initialSize: Int(initialSize))
}
/// Clears the builder and the buffer from the written data.
mutating public func clear() {
_minAlignment = 0
isNested = false
stringOffsetMap.removeAll(keepingCapacity: true)
_vtables.removeAll(keepingCapacity: true)
_vtableStorage.clear()
_bb.clear()
}
// MARK: - Create Tables
/// Checks if the required fields were serialized into the buffer
/// - Parameters:
/// - table: offset for the table
/// - fields: Array of all the important fields to be serialized
///
/// *NOTE: Never call this function, this is only supposed to be called
/// by the generated code*
@inline(__always)
mutating public func require(table: Offset, fields: [Int32]) {
for index in stride(from: 0, to: fields.count, by: 1) {
let start = _bb.capacity &- Int(table.o)
let startTable = start &- Int(_bb.read(def: Int32.self, position: start))
let isOkay = _bb.read(
def: VOffset.self,
position: startTable &+ Int(fields[index])) != 0
assert(isOkay, "Flatbuffers requires the following field")
}
}
/// Finished the buffer by adding the file id and then calling finish
/// - Parameters:
/// - offset: Offset of the table
/// - fileId: Takes the fileId
/// - prefix: if false it wont add the size of the buffer
///
/// ``finish(offset:fileId:addPrefix:)`` should be called at the end of creating
/// a table
/// ```swift
/// var root = SomeObject
/// .createObject(&builder,
/// name: nameOffset)
/// builder.finish(
/// offset: root,
/// fileId: "ax1a",
/// addPrefix: true)
/// ```
/// File id would append a file id name at the end of the written bytes before,
/// finishing the buffer.
///
/// Whereas, if `addPrefix` is true, the written bytes would
/// include the size of the current buffer.
mutating public func finish(
offset: Offset,
fileId: String,
addPrefix prefix: Bool = false)
{
let size = MemoryLayout<UOffset>.size
preAlign(
len: size &+ (prefix ? size : 0) &+ FileIdLength,
alignment: _minAlignment)
assert(fileId.count == FileIdLength, "Flatbuffers requires file id to be 4")
_bb.push(string: fileId, len: 4)
finish(offset: offset, addPrefix: prefix)
}
/// Finished the buffer by adding the file id, offset, and prefix to it.
/// - Parameters:
/// - offset: Offset of the table
/// - prefix: if false it wont add the size of the buffer
///
/// ``finish(offset:addPrefix:)`` should be called at the end of creating
/// a table
/// ```swift
/// var root = SomeObject
/// .createObject(&builder,
/// name: nameOffset)
/// builder.finish(
/// offset: root,
/// addPrefix: true)
/// ```
/// If `addPrefix` is true, the written bytes would
/// include the size of the current buffer.
mutating public func finish(
offset: Offset,
addPrefix prefix: Bool = false)
{
notNested()
let size = MemoryLayout<UOffset>.size
preAlign(len: size &+ (prefix ? size : 0), alignment: _minAlignment)
push(element: refer(to: offset.o))
if prefix { push(element: _bb.size) }
_vtableStorage.clear()
finished = true
}
/// ``startTable(with:)`` will let the builder know, that a new object is being serialized.
///
/// The function will fatalerror if called while there is another object being serialized.
/// ```swift
/// let start = Monster
/// .startMonster(&fbb)
/// ```
/// - Parameter numOfFields: Number of elements to be written to the buffer
/// - Returns: Offset of the newly started table
@inline(__always)
mutating public func startTable(with numOfFields: Int) -> UOffset {
notNested()
isNested = true
_vtableStorage.start(count: numOfFields)
return _bb.size
}
/// ``endTable(at:)`` will let the ``FlatBufferBuilder`` know that the
/// object that's written to it is completed
///
/// This would be called after all the elements are serialized,
/// it will add the current vtable into the ``ByteBuffer``.
/// The functions will `fatalError` in case the object is called
/// without ``startTable(with:)``, or the object has exceeded the limit of 2GB.
///
/// - Parameter startOffset:Start point of the object written
/// - returns: The root of the table
mutating public func endTable(at startOffset: UOffset) -> UOffset {
assert(isNested, "Calling endtable without calling starttable")
let sizeofVoffset = MemoryLayout<VOffset>.size
let vTableOffset = push(element: SOffset(0))
let tableObjectSize = vTableOffset &- startOffset
assert(tableObjectSize < 0x10000, "Buffer can't grow beyond 2 Gigabytes")
let _max = Int(_vtableStorage.maxOffset) &+ sizeofVoffset
_bb.fill(padding: _max)
_bb.write(
value: VOffset(tableObjectSize),
index: _bb.writerIndex &+ sizeofVoffset,
direct: true)
_bb.write(value: VOffset(_max), index: _bb.writerIndex, direct: true)
var itr = 0
while itr < _vtableStorage.writtenIndex {
let loaded = _vtableStorage.load(at: itr)
itr = itr &+ _vtableStorage.size
guard loaded.offset != 0 else { continue }
let _index = (_bb.writerIndex &+ Int(loaded.position))
_bb.write(
value: VOffset(vTableOffset &- loaded.offset),
index: _index,
direct: true)
}
_vtableStorage.clear()
let vt_use = _bb.size
var isAlreadyAdded: Int?
let vt2 = _bb.memory.advanced(by: _bb.writerIndex)
let len2 = vt2.load(fromByteOffset: 0, as: Int16.self)
for index in stride(from: 0, to: _vtables.count, by: 1) {
let position = _bb.capacity &- Int(_vtables[index])
let vt1 = _bb.memory.advanced(by: position)
let len1 = _bb.read(def: Int16.self, position: position)
if len2 != len1 || 0 != memcmp(vt1, vt2, Int(len2)) { continue }
isAlreadyAdded = Int(_vtables[index])
break
}
if let offset = isAlreadyAdded {
let vTableOff = Int(vTableOffset)
let space = _bb.capacity &- vTableOff
_bb.write(value: Int32(offset &- vTableOff), index: space, direct: true)
_bb.pop(_bb.capacity &- space)
} else {
_bb.write(value: Int32(vt_use &- vTableOffset), index: Int(vTableOffset))
_vtables.append(_bb.size)
}
isNested = false
return vTableOffset
}
// MARK: - Builds Buffer
/// Asserts to see if the object is not nested
@inline(__always)
@usableFromInline
mutating internal func notNested() {
assert(!isNested, "Object serialization must not be nested")
}
/// Changes the minimuim alignment of the buffer
/// - Parameter size: size of the current alignment
@inline(__always)
@usableFromInline
mutating internal func minAlignment(size: Int) {
if size > _minAlignment {
_minAlignment = size
}
}
/// Gets the padding for the current element
/// - Parameters:
/// - bufSize: Current size of the buffer + the offset of the object to be written
/// - elementSize: Element size
@inline(__always)
@usableFromInline
mutating internal func padding(
bufSize: UInt32,
elementSize: UInt32) -> UInt32
{
((~bufSize) &+ 1) & (elementSize - 1)
}
/// Prealigns the buffer before writting a new object into the buffer
/// - Parameters:
/// - len:Length of the object
/// - alignment: Alignment type
@inline(__always)
@usableFromInline
mutating internal func preAlign(len: Int, alignment: Int) {
minAlignment(size: alignment)
_bb.fill(padding: Int(padding(
bufSize: _bb.size &+ UOffset(len),
elementSize: UOffset(alignment))))
}
/// Prealigns the buffer before writting a new object into the buffer
/// - Parameters:
/// - len: Length of the object
/// - type: Type of the object to be written
@inline(__always)
@usableFromInline
mutating internal func preAlign<T: Scalar>(len: Int, type: T.Type) {
preAlign(len: len, alignment: MemoryLayout<T>.size)
}
/// Refers to an object that's written in the buffer
/// - Parameter off: the objects index value
@inline(__always)
@usableFromInline
mutating internal func refer(to off: UOffset) -> UOffset {
let size = MemoryLayout<UOffset>.size
preAlign(len: size, alignment: size)
return _bb.size &- off &+ UInt32(size)
}
/// Tracks the elements written into the buffer
/// - Parameters:
/// - offset: The offset of the element witten
/// - position: The position of the element
@inline(__always)
@usableFromInline
mutating internal func track(offset: UOffset, at position: VOffset) {
_vtableStorage.add(loc: (offset: offset, position: position))
}
// MARK: - Inserting Vectors
/// ``startVector(_:elementSize:)`` creates a new vector within buffer
///
/// The function checks if there is a current object being written, if
/// the check passes it creates a buffer alignment of `length * elementSize`
/// ```swift
/// builder.startVector(
/// int32Values.count, elementSize: 4)
/// ```
///
/// - Parameters:
/// - len: Length of vector to be created
/// - elementSize: Size of object type to be written
@inline(__always)
mutating public func startVector(_ len: Int, elementSize: Int) {
notNested()
isNested = true
preAlign(len: len &* elementSize, type: UOffset.self)
preAlign(len: len &* elementSize, alignment: elementSize)
}
/// ``endVector(len:)`` ends the currently created vector
///
/// Calling ``endVector(len:)`` requires the length, of the current
/// vector. The length would be pushed to indicate the count of numbers
/// within the vector. If ``endVector(len:)`` is called without
/// ``startVector(_:elementSize:)`` it asserts.
///
/// ```swift
/// let vectorOffset = builder.
/// endVector(len: int32Values.count)
/// ```
///
/// - Parameter len: Length of the buffer
/// - Returns: Returns the current ``Offset`` in the ``ByteBuffer``
@inline(__always)
mutating public func endVector(len: Int) -> Offset {
assert(isNested, "Calling endVector without calling startVector")
isNested = false
return Offset(offset: push(element: Int32(len)))
}
/// Creates a vector of type ``Scalar`` into the ``ByteBuffer``
///
/// ``createVector(_:)-4swl0`` writes a vector of type Scalars into
/// ``ByteBuffer``. This is a convenient method instead of calling,
/// ``startVector(_:elementSize:)`` and then ``endVector(len:)``
/// ```swift
/// let vectorOffset = builder.
/// createVector([1, 2, 3, 4])
/// ```
///
/// The underlying implementation simply calls ``createVector(_:size:)-4lhrv``
///
/// - Parameter elements: elements to be written into the buffer
/// - returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector<T: Scalar>(_ elements: [T]) -> Offset {
createVector(elements, size: elements.count)
}
/// Creates a vector of type Scalar in the buffer
///
/// ``createVector(_:)-4swl0`` writes a vector of type Scalars into
/// ``ByteBuffer``. This is a convenient method instead of calling,
/// ``startVector(_:elementSize:)`` and then ``endVector(len:)``
/// ```swift
/// let vectorOffset = builder.
/// createVector([1, 2, 3, 4], size: 4)
/// ```
///
/// - Parameter elements: Elements to be written into the buffer
/// - Parameter size: Count of elements
/// - returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector<T: Scalar>(
_ elements: [T],
size: Int) -> Offset
{
let size = size
startVector(size, elementSize: MemoryLayout<T>.size)
_bb.push(elements: elements)
return endVector(len: size)
}
#if swift(>=5.0) && !os(WASI)
@inline(__always)
/// Creates a vector of bytes in the buffer.
///
/// Allows creating a vector from `Data` without copying to a `[UInt8]`
///
/// - Parameter bytes: bytes to be written into the buffer
/// - Returns: ``Offset`` of the vector
mutating public func createVector(bytes: ContiguousBytes) -> Offset {
let size = bytes.withUnsafeBytes { ptr in ptr.count }
startVector(size, elementSize: MemoryLayout<UInt8>.size)
_bb.push(bytes: bytes)
return endVector(len: size)
}
#endif
/// Creates a vector of type ``Enum`` into the ``ByteBuffer``
///
/// ``createVector(_:)-9h189`` writes a vector of type ``Enum`` into
/// ``ByteBuffer``. This is a convenient method instead of calling,
/// ``startVector(_:elementSize:)`` and then ``endVector(len:)``
/// ```swift
/// let vectorOffset = builder.
/// createVector([.swift, .cpp])
/// ```
///
/// The underlying implementation simply calls ``createVector(_:size:)-7cx6z``
///
/// - Parameter elements: elements to be written into the buffer
/// - returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector<T: Enum>(_ elements: [T]) -> Offset {
createVector(elements, size: elements.count)
}
/// Creates a vector of type ``Enum`` into the ``ByteBuffer``
///
/// ``createVector(_:)-9h189`` writes a vector of type ``Enum`` into
/// ``ByteBuffer``. This is a convenient method instead of calling,
/// ``startVector(_:elementSize:)`` and then ``endVector(len:)``
/// ```swift
/// let vectorOffset = builder.
/// createVector([.swift, .cpp])
/// ```
///
/// - Parameter elements: Elements to be written into the buffer
/// - Parameter size: Count of elements
/// - returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector<T: Enum>(
_ elements: [T],
size: Int) -> Offset
{
let size = size
startVector(size, elementSize: T.byteSize)
for index in stride(from: elements.count, to: 0, by: -1) {
_bb.push(value: elements[index &- 1].value, len: T.byteSize)
}
return endVector(len: size)
}
/// Creates a vector of already written offsets
///
/// ``createVector(ofOffsets:)`` creates a vector of ``Offset`` into
/// ``ByteBuffer``. This is a convenient method instead of calling,
/// ``startVector(_:elementSize:)`` and then ``endVector(len:)``.
///
/// The underlying implementation simply calls ``createVector(ofOffsets:len:)``
///
/// ```swift
/// let namesOffsets = builder.
/// createVector(ofOffsets: [name1, name2])
/// ```
/// - Parameter offsets: Array of offsets of type ``Offset``
/// - returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector(ofOffsets offsets: [Offset]) -> Offset {
createVector(ofOffsets: offsets, len: offsets.count)
}
/// Creates a vector of already written offsets
///
/// ``createVector(ofOffsets:)`` creates a vector of ``Offset`` into
/// ``ByteBuffer``. This is a convenient method instead of calling,
/// ``startVector(_:elementSize:)`` and then ``endVector(len:)``
///
/// ```swift
/// let namesOffsets = builder.
/// createVector(ofOffsets: [name1, name2])
/// ```
///
/// - Parameter offsets: Array of offsets of type ``Offset``
/// - Parameter size: Count of elements
/// - returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector(
ofOffsets offsets: [Offset],
len: Int) -> Offset
{
startVector(len, elementSize: MemoryLayout<Offset>.size)
for index in stride(from: offsets.count, to: 0, by: -1) {
push(element: offsets[index &- 1])
}
return endVector(len: len)
}
/// Creates a vector of strings
///
/// ``createVector(ofStrings:)`` creates a vector of `String` into
/// ``ByteBuffer``. This is a convenient method instead of manually
/// creating the string offsets, you simply pass it to this function
/// and it would write the strings into the ``ByteBuffer``.
/// After that it calls ``createVector(ofOffsets:)``
///
/// ```swift
/// let namesOffsets = builder.
/// createVector(ofStrings: ["Name", "surname"])
/// ```
///
/// - Parameter str: Array of string
/// - returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector(ofStrings str: [String]) -> Offset {
var offsets: [Offset] = []
for index in stride(from: 0, to: str.count, by: 1) {
offsets.append(create(string: str[index]))
}
return createVector(ofOffsets: offsets)
}
/// Creates a vector of type ``NativeStruct``.
///
/// Any swift struct in the generated code, should confirm to
/// ``NativeStruct``. Since the generated swift structs are padded
/// to the `FlatBuffers` standards.
///
/// ```swift
/// let offsets = builder.
/// createVector(ofStructs: [NativeStr(num: 1), NativeStr(num: 2)])
/// ```
///
/// - Parameter structs: A vector of ``NativeStruct``
/// - Returns: ``Offset`` of the vector
@inline(__always)
mutating public func createVector<T: NativeStruct>(ofStructs structs: [T])
-> Offset
{
startVector(
structs.count * MemoryLayout<T>.size,
elementSize: MemoryLayout<T>.alignment)
_bb.push(elements: structs)
return endVector(len: structs.count)
}
// MARK: - Inserting Structs
/// Writes a ``NativeStruct`` into the ``ByteBuffer``
///
/// Adds a native struct that's build and padded according
/// to `FlatBuffers` standards. with a predefined position.
///
/// ```swift
/// let offset = builder.create(
/// struct: NativeStr(num: 1),
/// position: 10)
/// ```
///
/// - Parameters:
/// - s: ``NativeStruct`` to be inserted into the ``ByteBuffer``
/// - position: The predefined position of the object
/// - Returns: ``Offset`` of written struct
@inline(__always)
@discardableResult
mutating public func create<T: NativeStruct>(
struct s: T, position: VOffset) -> Offset
{
let offset = create(struct: s)
_vtableStorage.add(
loc: (offset: _bb.size, position: VOffset(position)))
return offset
}
/// Writes a ``NativeStruct`` into the ``ByteBuffer``
///
/// Adds a native struct that's build and padded according
/// to `FlatBuffers` standards, directly into the buffer without
/// a predefined position.
///
/// ```swift
/// let offset = builder.create(
/// struct: NativeStr(num: 1))
/// ```
///
/// - Parameters:
/// - s: ``NativeStruct`` to be inserted into the ``ByteBuffer``
/// - Returns: ``Offset`` of written struct
@inline(__always)
@discardableResult
mutating public func create<T: NativeStruct>(
struct s: T) -> Offset
{
let size = MemoryLayout<T>.size
preAlign(len: size, alignment: MemoryLayout<T>.alignment)
_bb.push(struct: s, size: size)
return Offset(offset: _bb.size)
}
// MARK: - Inserting Strings
/// Insets a string into the buffer of type `UTF8`
///
/// Adds a swift string into ``ByteBuffer`` by encoding it
/// using `UTF8`
///
/// ```swift
/// let nameOffset = builder
/// .create(string: "welcome")
/// ```
///
/// - Parameter str: String to be serialized
/// - returns: ``Offset`` of inserted string
@inline(__always)
mutating public func create(string str: String?) -> Offset {
guard let str = str else { return Offset() }
let len = str.utf8.count
notNested()
preAlign(len: len &+ 1, type: UOffset.self)
_bb.fill(padding: 1)
_bb.push(string: str, len: len)
push(element: UOffset(len))
return Offset(offset: _bb.size)
}
/// Insets a shared string into the buffer of type `UTF8`
///
/// Adds a swift string into ``ByteBuffer`` by encoding it
/// using `UTF8`. The function will check if the string,
/// is already written to the ``ByteBuffer``
///
/// ```swift
/// let nameOffset = builder
/// .createShared(string: "welcome")
///
///
/// let secondOffset = builder
/// .createShared(string: "welcome")
///
/// assert(nameOffset.o == secondOffset.o)
/// ```
///
/// - Parameter str: String to be serialized
/// - returns: ``Offset`` of inserted string
@inline(__always)
mutating public func createShared(string str: String?) -> Offset {
guard let str = str else { return Offset() }
if let offset = stringOffsetMap[str] {
return offset
}
let offset = create(string: str)
stringOffsetMap[str] = offset
return offset
}
// MARK: - Inseting offsets
/// Writes the ``Offset`` of an already written table
///
/// Writes the ``Offset`` of a table if not empty into the
/// ``ByteBuffer``
///
/// - Parameters:
/// - offset: ``Offset`` of another object to be written
/// - position: The predefined position of the object
@inline(__always)
mutating public func add(offset: Offset, at position: VOffset) {
if offset.isEmpty { return }
add(element: refer(to: offset.o), def: 0, at: position)
}
/// Pushes a value of type ``Offset`` into the ``ByteBuffer``
/// - Parameter o: ``Offset``
/// - returns: Current position of the ``Offset``
@inline(__always)
@discardableResult
mutating public func push(element o: Offset) -> UOffset {
push(element: refer(to: o.o))
}
// MARK: - Inserting Scalars to Buffer
/// Writes a ``Scalar`` value into ``ByteBuffer``
///
/// ``add(element:def:at:)`` takes in a default value, and current value
/// and the position within the `VTable`. The default value would not
/// be serialized if the value is the same as the current value or
/// `serializeDefaults` is equal to false.
///
/// If serializing defaults is important ``init(initialSize:serializeDefaults:)``,
/// passing true for `serializeDefaults` would do the job.
///
/// ```swift
/// // Adds 10 to the buffer
/// builder.add(element: Int(10), def: 1, position 12)
/// ```
///
/// *NOTE: Never call this manually*
///
/// - Parameters:
/// - element: Element to insert
/// - def: Default value for that element
/// - position: The predefined position of the element
@inline(__always)
mutating public func add<T: Scalar>(
element: T,
def: T,
at position: VOffset)
{
if element == def && !serializeDefaults { return }
track(offset: push(element: element), at: position)
}
/// Writes a optional ``Scalar`` value into ``ByteBuffer``
///
/// Takes an optional value to be written into the ``ByteBuffer``
///
/// *NOTE: Never call this manually*
///
/// - Parameters:
/// - element: Optional element of type scalar
/// - position: The predefined position of the element
@inline(__always)
mutating public func add<T: Scalar>(element: T?, at position: VOffset) {
guard let element = element else { return }
track(offset: push(element: element), at: position)
}
/// Pushes a values of type ``Scalar`` into the ``ByteBuffer``
///
/// *NOTE: Never call this manually*
///
/// - Parameter element: Element to insert
/// - returns: position of the Element
@inline(__always)
@discardableResult
mutating public func push<T: Scalar>(element: T) -> UOffset {
let size = MemoryLayout<T>.size
preAlign(
len: size,
alignment: size)
_bb.push(value: element, len: size)
return _bb.size
}
}
extension FlatBufferBuilder: CustomDebugStringConvertible {
public var debugDescription: String {
"""
buffer debug:
\(_bb)
builder debug:
{ finished: \(finished), serializeDefaults: \(
serializeDefaults), isNested: \(isNested) }
"""
}
typealias FieldLoc = (offset: UOffset, position: VOffset)
/// VTableStorage is a class to contain the VTable buffer that would be serialized into buffer
@usableFromInline
internal class VTableStorage {
/// Memory check since deallocating each time we want to clear would be expensive
/// and memory leaks would happen if we dont deallocate the first allocated memory.
/// memory is promised to be available before adding `FieldLoc`
private var memoryInUse = false
/// Size of FieldLoc in memory
let size = MemoryLayout<FieldLoc>.stride
/// Memeory buffer
var memory: UnsafeMutableRawBufferPointer!
/// Capacity of the current buffer
var capacity: Int = 0
/// Maximuim offset written to the class
var maxOffset: VOffset = 0
/// number of fields written into the buffer
var numOfFields: Int = 0
/// Last written Index
var writtenIndex: Int = 0
/// Creates the memory to store the buffer in
@usableFromInline
@inline(__always)
init() {
memory = UnsafeMutableRawBufferPointer.allocate(
byteCount: 0,
alignment: 0)
}
@inline(__always)
deinit {
memory.deallocate()
}
/// Builds a buffer with byte count of fieldloc.size * count of field numbers
/// - Parameter count: number of fields to be written
@inline(__always)
func start(count: Int) {
assert(count >= 0, "number of fields should NOT be negative")
let capacity = count &* size
ensure(space: capacity)
}
/// Adds a FieldLoc into the buffer, which would track how many have been written,
/// and max offset
/// - Parameter loc: Location of encoded element
@inline(__always)
func add(loc: FieldLoc) {
memory.baseAddress?.advanced(by: writtenIndex).storeBytes(
of: loc,
as: FieldLoc.self)
writtenIndex = writtenIndex &+ size
numOfFields = numOfFields &+ 1
maxOffset = max(loc.position, maxOffset)
}
/// Clears the data stored related to the encoded buffer
@inline(__always)
func clear() {
maxOffset = 0
numOfFields = 0
writtenIndex = 0
}
/// Ensure that the buffer has enough space instead of recreating the buffer each time.
/// - Parameter space: space required for the new vtable
@inline(__always)
func ensure(space: Int) {
guard space &+ writtenIndex > capacity else { return }
memory.deallocate()
memory = UnsafeMutableRawBufferPointer.allocate(
byteCount: space,
alignment: size)
capacity = space
}
/// Loads an object of type `FieldLoc` from buffer memory
/// - Parameter index: index of element
/// - Returns: a FieldLoc at index
@inline(__always)
func load(at index: Int) -> FieldLoc {
memory.load(fromByteOffset: index, as: FieldLoc.self)
}
}
}
@@ -0,0 +1,64 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// NativeStruct is a protocol that indicates if the struct is a native `swift` struct
/// since now we will be serializing native structs into the buffer.
public protocol NativeStruct {}
/// FlatbuffersInitializable is a protocol that allows any object to be
/// Initialized from a ByteBuffer
public protocol FlatbuffersInitializable {
/// Any flatbuffers object that confirms to this protocol is going to be
/// initializable through this initializer
init(_ bb: ByteBuffer, o: Int32)
}
/// FlatbufferObject structures all the Flatbuffers objects
public protocol FlatBufferObject: FlatbuffersInitializable {
var __buffer: ByteBuffer! { get }
}
/// ``ObjectAPIPacker`` is a protocol that allows object to pack and unpack from a
/// ``NativeObject`` to a flatbuffers Object and vice versa.
public protocol ObjectAPIPacker {
/// associatedtype to the object that should be unpacked.
associatedtype T
/// ``pack(_:obj:)-3ptws`` tries to pacs the variables of a native Object into the `ByteBuffer` by using
/// a FlatBufferBuilder
/// - Parameters:
/// - builder: FlatBufferBuilder that will host incoming data
/// - obj: Object of associatedtype to the current implementer
///
/// ``pack(_:obj:)-3ptws`` can be called by passing through an already initialized ``FlatBufferBuilder``
/// or it can be called by using the public API that will create a new ``FlatBufferBuilder``
static func pack(_ builder: inout FlatBufferBuilder, obj: inout T?) -> Offset
/// ``pack(_:obj:)-20ipk`` packs the variables of a native Object into the `ByteBuffer` by using
/// the FlatBufferBuilder
/// - Parameters:
/// - builder: FlatBufferBuilder that will host incoming data
/// - obj: Object of associatedtype to the current implementer
///
/// ``pack(_:obj:)-20ipk`` can be called by passing through an already initialized ``FlatBufferBuilder``
/// or it can be called by using the public API that will create a new ``FlatBufferBuilder``
static func pack(_ builder: inout FlatBufferBuilder, obj: inout T) -> Offset
/// ``unpack()`` unpacks a ``FlatBuffers`` object into a Native swift object.
mutating func unpack() -> T
}
@@ -0,0 +1,37 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// FlatBuffersUtils hosts some utility functions that might be useful
public enum FlatBuffersUtils {
/// Gets the size of the prefix
/// - Parameter bb: Flatbuffer object
public static func getSizePrefix(bb: ByteBuffer) -> Int32 {
bb.read(def: Int32.self, position: bb.reader)
}
/// Removes the prefix by duplicating the Flatbuffer this call is expensive since its
/// creates a new buffer use `readPrefixedSizeCheckedRoot` instead
/// unless a completely new buffer is required
/// - Parameter bb: Flatbuffer object
///
///
public static func removeSizePrefix(bb: ByteBuffer) -> ByteBuffer {
bb.duplicate(removing: MemoryLayout<Int32>.size)
}
}
@@ -0,0 +1,75 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// Collection of thrown from the Flatbuffer verifier
public enum FlatbuffersErrors: Error, Equatable {
/// Thrown when trying to verify a buffer that doesnt have the length of an ID
case bufferDoesntContainID
/// Thrown when verifying a file id that doesnt match buffer id
case bufferIdDidntMatchPassedId
/// Prefixed size doesnt match the current (readable) buffer size
case prefixedSizeNotEqualToBufferSize
/// Thrown when buffer is bigger than the allowed 2GiB
case exceedsMaxSizeAllowed
/// Thrown when there is an missaligned pointer at position
/// of type
case missAlignedPointer(position: Int, type: String)
/// Thrown when trying to read a value that goes out of the
/// current buffer bounds
case outOfBounds(position: UInt, end: Int)
/// Thrown when the signed offset is out of the bounds of the
/// current buffer
case signedOffsetOutOfBounds(offset: Int, position: Int)
/// Thrown when a required field doesnt exist within the buffer
case requiredFieldDoesntExist(position: VOffset, name: String)
/// Thrown when a string is missing its NULL Terminator `\0`,
/// this can be disabled in the `VerifierOptions`
case missingNullTerminator(position: Int, str: String?)
/// Thrown when the verifier has reached the maximum tables allowed,
/// this can be disabled in the `VerifierOptions`
case maximumTables
/// Thrown when the verifier has reached the maximum depth allowed,
/// this can be disabled in the `VerifierOptions`
case maximumDepth
/// Thrown when the verifier is presented with an unknown union case
case unknownUnionCase
/// thrown when a value for a union is not found within the buffer
case valueNotFound(key: Int?, keyName: String, field: Int?, fieldName: String)
/// thrown when the size of the keys vector doesnt match fields vector
case unionVectorSize(
keyVectorSize: Int,
fieldVectorSize: Int,
unionKeyName: String,
fieldName: String)
case apparentSizeTooLarge
}
#if !os(WASI)
extension FlatbuffersErrors {
public static func == (
lhs: FlatbuffersErrors,
rhs: FlatbuffersErrors) -> Bool
{
lhs.localizedDescription == rhs.localizedDescription
}
}
#endif
@@ -0,0 +1,47 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
extension Int {
/// Moves the current int into the nearest power of two
///
/// This is used since the UnsafeMutableRawPointer will face issues when writing/reading
/// if the buffer alignment exceeds that actual size of the buffer
var convertToPowerofTwo: Int {
guard self > 0 else { return 1 }
var n = UOffset(self)
#if arch(arm) || arch(i386)
let max = UInt32(Int.max)
#else
let max = UInt32.max
#endif
n -= 1
n |= n >> 1
n |= n >> 2
n |= n >> 4
n |= n >> 8
n |= n >> 16
if n != max {
n += 1
}
return Int(n)
}
}
@@ -0,0 +1,65 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// FlatBufferGRPCMessage protocol that should allow us to invoke
/// initializers directly from the GRPC generated code
public protocol FlatBufferGRPCMessage {
/// Raw pointer which would be pointing to the beginning of the readable bytes
var rawPointer: UnsafeMutableRawPointer { get }
/// Size of readable bytes in the buffer
var size: Int { get }
init(byteBuffer: ByteBuffer)
}
/// Message is a wrapper around Buffers to to able to send Flatbuffers `Buffers` through the
/// GRPC library
public struct Message<T: FlatBufferObject>: FlatBufferGRPCMessage {
internal var buffer: ByteBuffer
/// Returns the an object of type T that would be read from the buffer
public var object: T {
T.init(
buffer,
o: Int32(buffer.read(def: UOffset.self, position: buffer.reader)) +
Int32(buffer.reader))
}
public var rawPointer: UnsafeMutableRawPointer {
buffer.memory.advanced(by: buffer.reader) }
public var size: Int { Int(buffer.size) }
/// Initializes the message with the type Flatbuffer.Bytebuffer that is transmitted over
/// GRPC
/// - Parameter byteBuffer: Flatbuffer ByteBuffer object
public init(byteBuffer: ByteBuffer) {
buffer = byteBuffer
}
/// Initializes the message by copying the buffer to the message to be sent.
/// from the builder
/// - Parameter builder: FlatbufferBuilder that has the bytes created in
/// - Note: Use `builder.finish(offset)` before passing the builder without prefixing anything to it
public init(builder: inout FlatBufferBuilder) {
buffer = builder.sizedBuffer
builder.clear()
}
}
@@ -0,0 +1,84 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// Mutable is a protocol that allows us to mutate Scalar values within a ``ByteBuffer``
public protocol Mutable {
/// makes Flatbuffer accessed within the Protocol
var bb: ByteBuffer { get }
/// makes position of the ``Table``/``Struct`` accessed within the Protocol
var position: Int32 { get }
}
extension Mutable {
/// Mutates the memory in the buffer, this is only called from the access function of ``Table`` and ``struct``
/// - Parameters:
/// - value: New value to be inserted to the buffer
/// - index: index of the Element
func mutate<T: Scalar>(value: T, o: Int32) -> Bool {
guard o != 0 else { return false }
bb.write(value: value, index: Int(o), direct: true)
return true
}
}
extension Mutable where Self == Table {
/// Mutates a value by calling mutate with respect to the position in a ``Table``
/// - Parameters:
/// - value: New value to be inserted to the buffer
/// - index: index of the Element
public func mutate<T: Scalar>(_ value: T, index: Int32) -> Bool {
guard index != 0 else { return false }
return mutate(value: value, o: index + position)
}
/// Directly mutates the element by calling mutate
///
/// Mutates the Element at index ignoring the current position by calling mutate
/// - Parameters:
/// - value: New value to be inserted to the buffer
/// - index: index of the Element
public func directMutate<T: Scalar>(_ value: T, index: Int32) -> Bool {
mutate(value: value, o: index)
}
}
extension Mutable where Self == Struct {
/// Mutates a value by calling mutate with respect to the position in the struct
/// - Parameters:
/// - value: New value to be inserted to the buffer
/// - index: index of the Element
public func mutate<T: Scalar>(_ value: T, index: Int32) -> Bool {
mutate(value: value, o: index + position)
}
/// Directly mutates the element by calling mutate
///
/// Mutates the Element at index ignoring the current position by calling mutate
/// - Parameters:
/// - value: New value to be inserted to the buffer
/// - index: index of the Element
public func directMutate<T: Scalar>(_ value: T, index: Int32) -> Bool {
mutate(value: value, o: index)
}
}
extension Struct: Mutable {}
extension Table: Mutable {}
@@ -0,0 +1,53 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// NativeObject is a protocol that all of the `Object-API` generated code should be
/// conforming to since it allows developers the ease of use to pack and unpack their
/// Flatbuffers objects
public protocol NativeObject {}
extension NativeObject {
/// Serialize is a helper function that serializes the data from the Object API to a bytebuffer directly th
/// - Parameter type: Type of the Flatbuffer object
/// - Returns: returns the encoded sized ByteBuffer
public func serialize<T: ObjectAPIPacker>(type: T.Type) -> ByteBuffer
where T.T == Self
{
var builder = FlatBufferBuilder(initialSize: 1024)
return serialize(builder: &builder, type: type.self)
}
/// Serialize is a helper function that serializes the data from the Object API to a bytebuffer directly.
///
/// - Parameters:
/// - builder: A FlatBufferBuilder
/// - type: Type of the Flatbuffer object
/// - Returns: returns the encoded sized ByteBuffer
/// - Note: The `serialize(builder:type)` can be considered as a function that allows you to create smaller builder instead of the default `1024`.
/// It can be considered less expensive in terms of memory allocation
public func serialize<T: ObjectAPIPacker>(
builder: inout FlatBufferBuilder,
type: T.Type) -> ByteBuffer where T.T == Self
{
var s = self
let root = type.pack(&builder, obj: &s)
builder.finish(offset: root)
return builder.sizedBuffer
}
}
@@ -0,0 +1,28 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// Offset object for all the Objects that are written into the buffer
public struct Offset {
/// Offset of the object in the buffer
public var o: UOffset
/// Returns false if the offset is equal to zero
public var isEmpty: Bool { o == 0 }
public init(offset: UOffset) { o = offset }
public init() { o = 0 }
}
@@ -0,0 +1,116 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// Takes in a prefixed sized buffer, where the prefixed size would be skipped.
/// And would verify that the buffer passed is a valid `Flatbuffers` Object.
/// - Parameters:
/// - byteBuffer: Buffer that needs to be checked and read
/// - options: Verifier options
/// - Throws: FlatbuffersErrors
/// - Returns: Returns a valid, checked Flatbuffers object
///
/// ``getPrefixedSizeCheckedRoot(byteBuffer:options:)`` would skip the first Bytes in
/// the ``ByteBuffer`` and verifies the buffer by calling ``getCheckedRoot(byteBuffer:options:)``
public func getPrefixedSizeCheckedRoot<T: FlatBufferObject & Verifiable>(
byteBuffer: inout ByteBuffer,
fileId: String? = nil,
options: VerifierOptions = .init()) throws -> T
{
byteBuffer.skipPrefix()
return try getCheckedRoot(
byteBuffer: &byteBuffer,
fileId: fileId,
options: options)
}
/// Takes in a prefixed sized buffer, where we check if the sized buffer is equal to prefix size.
/// And would verify that the buffer passed is a valid `Flatbuffers` Object.
/// - Parameters:
/// - byteBuffer: Buffer that needs to be checked and read
/// - options: Verifier options
/// - Throws: FlatbuffersErrors
/// - Returns: Returns a valid, checked Flatbuffers object
///
/// ``getPrefixedSizeCheckedRoot(byteBuffer:options:)`` would skip the first Bytes in
/// the ``ByteBuffer`` and verifies the buffer by calling ``getCheckedRoot(byteBuffer:options:)``
public func getCheckedPrefixedSizeRoot<T: FlatBufferObject & Verifiable>(
byteBuffer: inout ByteBuffer,
fileId: String? = nil,
options: VerifierOptions = .init()) throws -> T
{
let prefix = byteBuffer.skipPrefix()
if prefix != byteBuffer.size {
throw FlatbuffersErrors.prefixedSizeNotEqualToBufferSize
}
return try getCheckedRoot(
byteBuffer: &byteBuffer,
fileId: fileId,
options: options)
}
/// Takes in a prefixed sized buffer, where the prefixed size would be skipped.
/// Returns a `NON-Checked` flatbuffers object
/// - Parameter byteBuffer: Buffer that contains data
/// - Returns: Returns a Flatbuffers object
///
/// ``getPrefixedSizeCheckedRoot(byteBuffer:options:)`` would skip the first Bytes in
/// the ``ByteBuffer`` and then calls ``getRoot(byteBuffer:)``
public func getPrefixedSizeRoot<T: FlatBufferObject>(
byteBuffer: inout ByteBuffer)
-> T
{
byteBuffer.skipPrefix()
return getRoot(byteBuffer: &byteBuffer)
}
/// Verifies that the buffer passed is a valid `Flatbuffers` Object.
/// - Parameters:
/// - byteBuffer: Buffer that needs to be checked and read
/// - options: Verifier options
/// - Throws: FlatbuffersErrors
/// - Returns: Returns a valid, checked Flatbuffers object
///
/// ``getCheckedRoot(byteBuffer:options:)`` Takes in a ``ByteBuffer`` and verifies
/// that by creating a ``Verifier`` and checkes if all the `Bytes` and correctly aligned
/// and within the ``ByteBuffer`` range.
public func getCheckedRoot<T: FlatBufferObject & Verifiable>(
byteBuffer: inout ByteBuffer,
fileId: String? = nil,
options: VerifierOptions = .init()) throws -> T
{
var verifier = try Verifier(buffer: &byteBuffer, options: options)
if let fileId = fileId {
try verifier.verify(id: fileId)
}
try ForwardOffset<T>.verify(&verifier, at: 0, of: T.self)
return T.init(
byteBuffer,
o: Int32(byteBuffer.read(def: UOffset.self, position: byteBuffer.reader)) +
Int32(byteBuffer.reader))
}
/// Returns a `NON-Checked` flatbuffers object
/// - Parameter byteBuffer: Buffer that contains data
/// - Returns: Returns a Flatbuffers object
public func getRoot<T: FlatBufferObject>(byteBuffer: inout ByteBuffer) -> T {
T.init(
byteBuffer,
o: Int32(byteBuffer.read(def: UOffset.self, position: byteBuffer.reader)) +
Int32(byteBuffer.reader))
}
@@ -0,0 +1,109 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
extension String: Verifiable {
/// Verifies that the current value is which the bounds of the buffer, and if
/// the current `Value` is aligned properly
/// - Parameters:
/// - verifier: Verifier that hosts the buffer
/// - position: Current position within the buffer
/// - type: The type of the object to be verified
/// - Throws: Errors coming from `inBuffer`, `missingNullTerminator` and `outOfBounds`
public static func verify<T>(
_ verifier: inout Verifier,
at position: Int,
of type: T.Type) throws where T: Verifiable
{
let range = try String.verifyRange(&verifier, at: position, of: UInt8.self)
/// Safe &+ since we already check for overflow in verify range
let stringLen = range.start &+ range.count
if stringLen >= verifier.capacity {
throw FlatbuffersErrors.outOfBounds(
position: UInt(clamping: stringLen.magnitude),
end: verifier.capacity)
}
let isNullTerminated = verifier._buffer.read(
def: UInt8.self,
position: stringLen) == 0
if !verifier._options._ignoreMissingNullTerminators && !isNullTerminated {
let str = verifier._buffer.readString(at: range.start, count: range.count)
throw FlatbuffersErrors.missingNullTerminator(
position: position,
str: str)
}
}
}
extension String: FlatbuffersInitializable {
/// Initailizes a string from a Flatbuffers ByteBuffer
/// - Parameters:
/// - bb: ByteBuffer containing the readable string
/// - o: Current position
public init(_ bb: ByteBuffer, o: Int32) {
let v = Int(o)
let count = bb.read(def: Int32.self, position: v)
self = bb.readString(
at: MemoryLayout<Int32>.size + v,
count: Int(count)) ?? ""
}
}
extension String: ObjectAPIPacker {
public static func pack(
_ builder: inout FlatBufferBuilder,
obj: inout String?) -> Offset
{
guard var obj = obj else { return Offset() }
return pack(&builder, obj: &obj)
}
public static func pack(
_ builder: inout FlatBufferBuilder,
obj: inout String) -> Offset
{
builder.create(string: obj)
}
public mutating func unpack() -> String {
self
}
}
extension String: NativeObject {
public func serialize<T: ObjectAPIPacker>(type: T.Type) -> ByteBuffer
where T.T == Self
{
fatalError("serialize should never be called from string directly")
}
public func serialize<T: ObjectAPIPacker>(
builder: inout FlatBufferBuilder,
type: T.Type) -> ByteBuffer where T.T == Self
{
fatalError("serialize should never be called from string directly")
}
}
@@ -0,0 +1,47 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// Struct is a representation of a mutable `Flatbuffers` struct
/// since native structs are value types and cant be mutated
@frozen
public struct Struct {
/// Hosting Bytebuffer
public private(set) var bb: ByteBuffer
/// Current position of the struct
public private(set) var position: Int32
/// Initializer for a mutable flatbuffers struct
/// - Parameters:
/// - bb: Current hosting Bytebuffer
/// - position: Current position for the struct in the ByteBuffer
public init(bb: ByteBuffer, position: Int32 = 0) {
self.bb = bb
self.position = position
}
/// Reads data from the buffer directly at offset O
/// - Parameters:
/// - type: Type of data to be read
/// - o: Current offset of the data
/// - Returns: Data of Type T that conforms to type Scalar
public func readBuffer<T: Scalar>(of type: T.Type, at o: Int32) -> T {
let r = bb.read(def: T.self, position: Int(o + position))
return r
}
}
@@ -0,0 +1,236 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// `Table` is a Flatbuffers object that can read,
/// mutate scalar fields within a valid flatbuffers buffer
@frozen
public struct Table {
/// Hosting Bytebuffer
public private(set) var bb: ByteBuffer
/// Current position of the table within the buffer
public private(set) var position: Int32
/// Initializer for the table interface to allow generated code to read
/// data from memory
/// - Parameters:
/// - bb: ByteBuffer that stores data
/// - position: Current table position
/// - Note: This will `CRASH` if read on a big endian machine
public init(bb: ByteBuffer, position: Int32 = 0) {
guard isLitteEndian else {
fatalError(
"Reading/Writing a buffer in big endian machine is not supported on swift")
}
self.bb = bb
self.position = position
}
/// Gets the offset of the current field within the buffer by reading
/// the vtable
/// - Parameter o: current offset
/// - Returns: offset of field within buffer
public func offset(_ o: Int32) -> Int32 {
let vtable = position - bb.read(def: Int32.self, position: Int(position))
return o < bb
.read(def: VOffset.self, position: Int(vtable)) ? Int32(bb.read(
def: Int16.self,
position: Int(vtable + o))) : 0
}
/// Gets the indirect offset of the current stored object
/// (applicable only for object arrays)
/// - Parameter o: current offset
/// - Returns: offset of field within buffer
public func indirect(_ o: Int32) -> Int32 {
o + bb.read(def: Int32.self, position: Int(o))
}
/// String reads from the buffer with respect to position of the current table.
/// - Parameter offset: Offset of the string
public func string(at offset: Int32) -> String? {
directString(at: offset + position)
}
/// Direct string reads from the buffer disregarding the position of the table.
/// It would be preferable to use string unless the current position of the table
/// is not needed
/// - Parameter offset: Offset of the string
public func directString(at offset: Int32) -> String? {
var offset = offset
offset += bb.read(def: Int32.self, position: Int(offset))
let count = bb.read(def: Int32.self, position: Int(offset))
let position = Int(offset) + MemoryLayout<Int32>.size
return bb.readString(at: position, count: Int(count))
}
/// Reads from the buffer with respect to the position in the table.
/// - Parameters:
/// - type: Type of Element that needs to be read from the buffer
/// - o: Offset of the Element
public func readBuffer<T>(of type: T.Type, at o: Int32) -> T {
directRead(of: T.self, offset: o + position)
}
/// Reads from the buffer disregarding the position of the table.
/// It would be used when reading from an
/// ```
/// let offset = __t.offset(10)
/// //Only used when the we already know what is the
/// // position in the table since __t.vector(at:)
/// // returns the index with respect to the position
/// __t.directRead(of: Byte.self,
/// offset: __t.vector(at: offset) + index * 1)
/// ```
/// - Parameters:
/// - type: Type of Element that needs to be read from the buffer
/// - o: Offset of the Element
public func directRead<T>(of type: T.Type, offset o: Int32) -> T {
let r = bb.read(def: T.self, position: Int(o))
return r
}
/// Returns that current `Union` object at a specific offset
/// by adding offset to the current position of table
/// - Parameter o: offset
/// - Returns: A flatbuffers object
public func union<T: FlatbuffersInitializable>(_ o: Int32) -> T {
let o = o + position
return directUnion(o)
}
/// Returns a direct `Union` object at a specific offset
/// - Parameter o: offset
/// - Returns: A flatbuffers object
public func directUnion<T: FlatbuffersInitializable>(_ o: Int32) -> T {
T.init(bb, o: o + bb.read(def: Int32.self, position: Int(o)))
}
/// Returns a vector of type T at a specific offset
/// This should only be used by `Scalars`
/// - Parameter off: Readable offset
/// - Returns: Returns a vector of type [T]
public func getVector<T>(at off: Int32) -> [T]? {
let o = offset(off)
guard o != 0 else { return nil }
return bb.readSlice(index: Int(vector(at: o)), count: Int(vector(count: o)))
}
/// Vector count gets the count of Elements within the array
/// - Parameter o: start offset of the vector
/// - returns: Count of elements
public func vector(count o: Int32) -> Int32 {
var o = o
o += position
o += bb.read(def: Int32.self, position: Int(o))
return bb.read(def: Int32.self, position: Int(o))
}
/// Vector start index in the buffer
/// - Parameter o:start offset of the vector
/// - returns: the start index of the vector
public func vector(at o: Int32) -> Int32 {
var o = o
o += position
return o + bb.read(def: Int32.self, position: Int(o)) + 4
}
/// Reading an indirect offset of a table.
/// - Parameters:
/// - o: position within the buffer
/// - fbb: ByteBuffer
/// - Returns: table offset
static public func indirect(_ o: Int32, _ fbb: ByteBuffer) -> Int32 {
o + fbb.read(def: Int32.self, position: Int(o))
}
/// Gets a vtable value according to an table Offset and a field offset
/// - Parameters:
/// - o: offset relative to entire buffer
/// - vOffset: Field offset within a vtable
/// - fbb: ByteBuffer
/// - Returns: an position of a field
static public func offset(
_ o: Int32,
vOffset: Int32,
fbb: ByteBuffer) -> Int32
{
let vTable = Int32(fbb.capacity) - o
return vTable + Int32(fbb.read(
def: Int16.self,
position: Int(vTable + vOffset - fbb.read(
def: Int32.self,
position: Int(vTable)))))
}
/// Compares two objects at offset A and offset B within a ByteBuffer
/// - Parameters:
/// - off1: first offset to compare
/// - off2: second offset to compare
/// - fbb: Bytebuffer
/// - Returns: returns the difference between
static public func compare(
_ off1: Int32,
_ off2: Int32,
fbb: ByteBuffer) -> Int32
{
let memorySize = Int32(MemoryLayout<Int32>.size)
let _off1 = off1 + fbb.read(def: Int32.self, position: Int(off1))
let _off2 = off2 + fbb.read(def: Int32.self, position: Int(off2))
let len1 = fbb.read(def: Int32.self, position: Int(_off1))
let len2 = fbb.read(def: Int32.self, position: Int(_off2))
let startPos1 = _off1 + memorySize
let startPos2 = _off2 + memorySize
let minValue = min(len1, len2)
for i in 0...minValue {
let b1 = fbb.read(def: Int8.self, position: Int(i + startPos1))
let b2 = fbb.read(def: Int8.self, position: Int(i + startPos2))
if b1 != b2 {
return Int32(b2 - b1)
}
}
return len1 - len2
}
/// Compares two objects at offset A and array of `Bytes` within a ByteBuffer
/// - Parameters:
/// - off1: Offset to compare to
/// - key: bytes array to compare to
/// - fbb: Bytebuffer
/// - Returns: returns the difference between
static public func compare(
_ off1: Int32,
_ key: [Byte],
fbb: ByteBuffer) -> Int32
{
let memorySize = Int32(MemoryLayout<Int32>.size)
let _off1 = off1 + fbb.read(def: Int32.self, position: Int(off1))
let len1 = fbb.read(def: Int32.self, position: Int(_off1))
let len2 = Int32(key.count)
let startPos1 = _off1 + memorySize
let minValue = min(len1, len2)
for i in 0..<minValue {
let b = fbb.read(def: Int8.self, position: Int(i + startPos1))
let byte = key[Int(i)]
if b != byte {
return Int32(b - Int8(byte))
}
}
return len1 - len2
}
}
@@ -0,0 +1,203 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// `TableVerifier` verifies a table object is within a provided memory.
/// It checks if all the objects for a specific generated table, are within
/// the bounds of the buffer, aligned.
public struct TableVerifier {
/// position of current table in `ByteBuffer`
fileprivate var _position: Int
/// Current VTable position
fileprivate var _vtable: Int
/// Length of current VTable
fileprivate var _vtableLength: Int
/// `Verifier` object created in the base verifable call.
fileprivate var _verifier: Verifier
/// Creates a `TableVerifier` verifier that allows the Flatbuffer object
/// to verify the buffer before accessing any of the data.
///
/// - Parameters:
/// - position: Current table Position
/// - vtable: Current `VTable` position
/// - vtableLength: Current `VTable` length
/// - verifier: `Verifier` Object that caches the data of the verifiable object
internal init(
position: Int,
vtable: Int,
vtableLength: Int,
verifier: inout Verifier)
{
_position = position
_vtable = vtable
_vtableLength = vtableLength
_verifier = verifier
}
/// Dereference the current object position from the `VTable`
/// - Parameter field: Current VTable refrence to position.
/// - Throws: A `FlatbuffersErrors` incase the voffset is not aligned/outOfBounds/apparentSizeTooLarge
/// - Returns: An optional position for current field
internal mutating func dereference(_ field: VOffset) throws -> Int? {
if field >= _vtableLength {
return nil
}
/// Reading the offset for the field needs to be read.
let offset: VOffset = try _verifier.getValue(
at: Int(clamping: _vtable &+ Int(field)))
if offset > 0 {
return Int(clamping: _position &+ Int(offset))
}
return nil
}
/// Visits all the fields within the table to validate the integrity
/// of the data
/// - Parameters:
/// - field: voffset of the current field to be read
/// - fieldName: fieldname to report data Errors.
/// - required: If the field has to be available in the buffer
/// - type: Type of field to be read
/// - Throws: A `FlatbuffersErrors` where the field is corrupt
public mutating func visit<T>(
field: VOffset,
fieldName: String,
required: Bool,
type: T.Type) throws where T: Verifiable
{
let derefValue = try dereference(field)
if let value = derefValue {
try T.verify(&_verifier, at: value, of: T.self)
return
}
if required {
throw FlatbuffersErrors.requiredFieldDoesntExist(
position: field,
name: fieldName)
}
}
/// Visits all the fields for a union object within the table to
/// validate the integrity of the data
/// - Parameters:
/// - key: Current Key Voffset
/// - field: Current field Voffset
/// - unionKeyName: Union key name
/// - fieldName: Field key name
/// - required: indicates if an object is required to be present
/// - completion: Completion is a handler that WILL be called in the generated
/// - Throws: A `FlatbuffersErrors` where the field is corrupt
public mutating func visit<T>(
unionKey key: VOffset,
unionField field: VOffset,
unionKeyName: String,
fieldName: String,
required: Bool,
completion: @escaping (inout Verifier, T, Int) throws -> Void) throws
where T: UnionEnum
{
let keyPos = try dereference(key)
let valPos = try dereference(field)
if keyPos == nil && valPos == nil {
if required {
throw FlatbuffersErrors.requiredFieldDoesntExist(
position: key,
name: unionKeyName)
}
return
}
if let _key = keyPos,
let _val = valPos
{
/// verifiying that the key is within the buffer
try T.T.verify(&_verifier, at: _key, of: T.T.self)
guard let _enum = try T.init(value: _verifier._buffer.read(
def: T.T.self,
position: _key)) else
{
throw FlatbuffersErrors.unknownUnionCase
}
/// we are assuming that Unions will always be of type Uint8
try completion(
&_verifier,
_enum,
_val)
return
}
throw FlatbuffersErrors.valueNotFound(
key: keyPos,
keyName: unionKeyName,
field: valPos,
fieldName: fieldName)
}
/// Visits and validates all the objects within a union vector
/// - Parameters:
/// - key: Current Key Voffset
/// - field: Current field Voffset
/// - unionKeyName: Union key name
/// - fieldName: Field key name
/// - required: indicates if an object is required to be present
/// - completion: Completion is a handler that WILL be called in the generated
/// - Throws: A `FlatbuffersErrors` where the field is corrupt
public mutating func visitUnionVector<T>(
unionKey key: VOffset,
unionField field: VOffset,
unionKeyName: String,
fieldName: String,
required: Bool,
completion: @escaping (inout Verifier, T, Int) throws -> Void) throws
where T: UnionEnum
{
let keyVectorPosition = try dereference(key)
let offsetVectorPosition = try dereference(field)
if let keyPos = keyVectorPosition,
let valPos = offsetVectorPosition
{
try UnionVector<T>.verify(
&_verifier,
keyPosition: keyPos,
fieldPosition: valPos,
unionKeyName: unionKeyName,
fieldName: fieldName,
completion: completion)
return
}
if required {
throw FlatbuffersErrors.requiredFieldDoesntExist(
position: field,
name: fieldName)
}
}
/// Finishs the current Table verifier, and subtracts the current
/// table from the incremented depth.
public mutating func finish() {
_verifier.finish()
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// `VerifierOptions` is a set of options to verify a flatbuffer
public struct VerifierOptions {
/// Maximum `Apparent` size if the buffer can be expanded into a DAG tree
internal var _maxApparentSize: UOffset
/// Maximum table count allowed in a buffer
internal var _maxTableCount: UOffset
/// Maximum depth allowed in a buffer
internal var _maxDepth: UOffset
/// Ignoring missing null terminals in strings
internal var _ignoreMissingNullTerminators: Bool
/// initializes the set of options for the verifier
/// - Parameters:
/// - maxDepth: Maximum depth allowed in a buffer
/// - maxTableCount: Maximum table count allowed in a buffer
/// - maxApparentSize: Maximum `Apparent` size if the buffer can be expanded into a DAG tree
/// - ignoreMissingNullTerminators: Ignoring missing null terminals in strings *Currently not supported in swift*
public init(
maxDepth: UOffset = 64,
maxTableCount: UOffset = 1000000,
maxApparentSize: UOffset = 1 << 31,
ignoreMissingNullTerminators: Bool = false)
{
_maxDepth = maxDepth
_maxTableCount = maxTableCount
_maxApparentSize = maxApparentSize
_ignoreMissingNullTerminators = ignoreMissingNullTerminators
}
}
@@ -0,0 +1,213 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// Verifiable is a protocol all swift flatbuffers object should conform to,
/// since swift is similar to `cpp` and `rust` where the data is read directly
/// from `unsafeMemory` thus the need to verify if the buffer received is a valid one
public protocol Verifiable {
/// Verifies that the current value is which the bounds of the buffer, and if
/// the current `Value` is aligned properly
/// - Parameters:
/// - verifier: Verifier that hosts the buffer
/// - position: Current position within the buffer
/// - type: The type of the object to be verified
/// - Throws: Errors coming from `inBuffer` function
static func verify<T>(
_ verifier: inout Verifier,
at position: Int,
of type: T.Type) throws where T: Verifiable
}
extension Verifiable {
/// Verifies if the current range to be read is within the bounds of the buffer,
/// and if the range is properly aligned
/// - Parameters:
/// - verifier: Verifier that hosts the buffer
/// - position: Current position within the buffer
/// - type: The type of the object to be verified
/// - Throws: Erros thrown from `isAligned` & `rangeInBuffer`
/// - Returns: a tuple of the start position and the count of objects within the range
@discardableResult
public static func verifyRange<T>(
_ verifier: inout Verifier,
at position: Int, of type: T.Type) throws -> (start: Int, count: Int)
{
let len: UOffset = try verifier.getValue(at: position)
let intLen = Int(len)
let start = Int(clamping: (position &+ MemoryLayout<Int32>.size).magnitude)
try verifier.isAligned(position: start, type: type.self)
try verifier.rangeInBuffer(position: start, size: intLen)
return (start, intLen)
}
}
extension Verifiable where Self: Scalar {
/// Verifies that the current value is which the bounds of the buffer, and if
/// the current `Value` is aligned properly
/// - Parameters:
/// - verifier: Verifier that hosts the buffer
/// - position: Current position within the buffer
/// - type: The type of the object to be verified
/// - Throws: Errors coming from `inBuffer` function
public static func verify<T>(
_ verifier: inout Verifier,
at position: Int,
of type: T.Type) throws where T: Verifiable
{
try verifier.inBuffer(position: position, of: type.self)
}
}
// MARK: - ForwardOffset
/// ForwardOffset is a container to wrap around the Generic type to be verified
/// from the flatbuffers object.
public enum ForwardOffset<U>: Verifiable where U: Verifiable {
/// Verifies that the current value is which the bounds of the buffer, and if
/// the current `Value` is aligned properly
/// - Parameters:
/// - verifier: Verifier that hosts the buffer
/// - position: Current position within the buffer
/// - type: The type of the object to be verified
/// - Throws: Errors coming from `inBuffer` function
public static func verify<T>(
_ verifier: inout Verifier,
at position: Int,
of type: T.Type) throws where T: Verifiable
{
let offset: UOffset = try verifier.getValue(at: position)
let nextOffset = Int(clamping: (Int(offset) &+ position).magnitude)
try U.verify(&verifier, at: nextOffset, of: U.self)
}
}
// MARK: - Vector
/// Vector is a container to wrap around the Generic type to be verified
/// from the flatbuffers object.
public enum Vector<U, S>: Verifiable where U: Verifiable, S: Verifiable {
/// Verifies that the current value is which the bounds of the buffer, and if
/// the current `Value` is aligned properly
/// - Parameters:
/// - verifier: Verifier that hosts the buffer
/// - position: Current position within the buffer
/// - type: The type of the object to be verified
/// - Throws: Errors coming from `inBuffer` function
public static func verify<T>(
_ verifier: inout Verifier,
at position: Int,
of type: T.Type) throws where T: Verifiable
{
/// checks if the next verification type S is equal to U of type forwardOffset
/// This had to be done since I couldnt find a solution for duplicate call functions
/// A fix will be appreciated
if U.self is ForwardOffset<S>.Type {
let range = try verifyRange(&verifier, at: position, of: UOffset.self)
for index in stride(
from: range.start,
to: Int(
clamping: range
.start &+ (range.count &* MemoryLayout<Int32>.size)),
by: MemoryLayout<UOffset>.size)
{
try U.verify(&verifier, at: index, of: U.self)
}
} else {
try S.verifyRange(&verifier, at: position, of: S.self)
}
}
}
// MARK: - UnionVector
/// UnionVector is a container to wrap around the Generic type to be verified
/// from the flatbuffers object.
public enum UnionVector<S> where S: UnionEnum {
/// Completion handler for the function Verify, that passes the verifier
/// enum type and position of union field
public typealias Completion = (inout Verifier, S, Int) throws -> Void
/// Verifies if the current range to be read is within the bounds of the buffer,
/// and if the range is properly aligned. It also verifies if the union type is a
/// *valid/supported* union type.
/// - Parameters:
/// - verifier: Verifier that hosts the buffer
/// - keyPosition: Current union key position within the buffer
/// - fieldPosition: Current union field position within the buffer
/// - unionKeyName: Name of key to written if error is presented
/// - fieldName: Name of field to written if error is presented
/// - completion: Completion is a handler that WILL be called in the generated
/// code to verify the actual objects
/// - Throws: FlatbuffersErrors
public static func verify(
_ verifier: inout Verifier,
keyPosition: Int,
fieldPosition: Int,
unionKeyName: String,
fieldName: String,
completion: @escaping Completion) throws
{
/// Get offset for union key vectors and offset vectors
let keyOffset: UOffset = try verifier.getValue(at: keyPosition)
let fieldOffset: UOffset = try verifier.getValue(at: fieldPosition)
/// Check if values are within the buffer, returns the start position of vectors, and vector counts
/// Using &+ is safe since we already verified that the value is within the buffer, where the max is
/// going to be 2Gib and swift supports Int64 by default
let keysRange = try S.T.verifyRange(
&verifier,
at: Int(keyOffset) &+ keyPosition,
of: S.T.self)
let offsetsRange = try UOffset.verifyRange(
&verifier,
at: Int(fieldOffset) &+ fieldPosition,
of: UOffset.self)
guard keysRange.count == offsetsRange.count else {
throw FlatbuffersErrors.unionVectorSize(
keyVectorSize: keysRange.count,
fieldVectorSize: offsetsRange.count,
unionKeyName: unionKeyName,
fieldName: fieldName)
}
var count = 0
/// Iterate over the vector of keys and offsets.
while count < keysRange.count {
/// index of readable enum value in array
let keysIndex = MemoryLayout<S.T>.size * count
guard let _enum = try S.init(value: verifier._buffer.read(
def: S.T.self,
position: keysRange.start + keysIndex)) else
{
throw FlatbuffersErrors.unknownUnionCase
}
/// index of readable offset value in array
let fieldIndex = MemoryLayout<UOffset>.size * count
try completion(&verifier, _enum, offsetsRange.start + fieldIndex)
count += 1
}
}
}
@@ -0,0 +1,238 @@
/*
* Copyright 2024 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// Verifier that check if the buffer passed into it is a valid,
/// safe, aligned Flatbuffers object since swift read from `unsafeMemory`
public struct Verifier {
/// Flag to check for alignment if true
fileprivate let _checkAlignment: Bool
/// Storage for all changing values within the verifier
private let storage: Storage
/// Current verifiable ByteBuffer
internal var _buffer: ByteBuffer
/// Options for verification
internal let _options: VerifierOptions
/// Current stored capacity within the verifier
var capacity: Int {
storage.capacity
}
/// Current depth of verifier
var depth: Int {
storage.depth
}
/// Current table count
var tableCount: Int {
storage.tableCount
}
/// Initializer for the verifier
/// - Parameters:
/// - buffer: Bytebuffer that is required to be verified
/// - options: `VerifierOptions` that set the rule for some of the verification done
/// - checkAlignment: If alignment check is required to be preformed
/// - Throws: `exceedsMaxSizeAllowed` if capacity of the buffer is more than 2GiB
public init(
buffer: inout ByteBuffer,
options: VerifierOptions = .init(),
checkAlignment: Bool = true) throws
{
guard buffer.capacity < FlatBufferMaxSize else {
throw FlatbuffersErrors.exceedsMaxSizeAllowed
}
_buffer = buffer
_checkAlignment = checkAlignment
_options = options
storage = Storage(capacity: buffer.capacity)
}
/// Resets the verifier to initial state
public func reset() {
storage.depth = 0
storage.tableCount = 0
}
/// Checks if the value of type `T` is aligned properly in the buffer
/// - Parameters:
/// - position: Current position
/// - type: Type of value to check
/// - Throws: `missAlignedPointer` if the pointer is not aligned properly
public func isAligned<T>(position: Int, type: T.Type) throws {
/// If check alignment is false this mutating function doesnt continue
if !_checkAlignment { return }
/// advance pointer to position X
let ptr = _buffer._storage.memory.advanced(by: position)
/// Check if the pointer is aligned
if Int(bitPattern: ptr) & (MemoryLayout<T>.alignment &- 1) == 0 {
return
}
throw FlatbuffersErrors.missAlignedPointer(
position: position,
type: String(describing: T.self))
}
/// Checks if the value of Size "X" is within the range of the buffer
/// - Parameters:
/// - position: Current position to be read
/// - size: `Byte` Size of readable object within the buffer
/// - Throws: `outOfBounds` if the value is out of the bounds of the buffer
/// and `apparentSizeTooLarge` if the apparent size is bigger than the one specified
/// in `VerifierOptions`
public func rangeInBuffer(position: Int, size: Int) throws {
let end = UInt(clamping: (position &+ size).magnitude)
if end > _buffer.capacity {
throw FlatbuffersErrors.outOfBounds(position: end, end: storage.capacity)
}
storage.apparentSize = storage.apparentSize &+ UInt32(size)
if storage.apparentSize > _options._maxApparentSize {
throw FlatbuffersErrors.apparentSizeTooLarge
}
}
/// Validates if a value of type `T` is aligned and within the bounds of
/// the buffer
/// - Parameters:
/// - position: Current readable position
/// - type: Type of value to check
/// - Throws: FlatbuffersErrors
public func inBuffer<T>(position: Int, of type: T.Type) throws {
try isAligned(position: position, type: type)
try rangeInBuffer(position: position, size: MemoryLayout<T>.size)
}
/// Visits a table at the current position and validates if the table meets
/// the rules specified in the `VerifierOptions`
/// - Parameter position: Current position to be read
/// - Throws: FlatbuffersErrors
/// - Returns: A `TableVerifier` at the current readable table
public mutating func visitTable(at position: Int) throws -> TableVerifier {
let vtablePosition = try derefOffset(position: position)
let vtableLength: VOffset = try getValue(at: vtablePosition)
let length = Int(vtableLength)
try isAligned(
position: Int(clamping: (vtablePosition + length).magnitude),
type: VOffset.self)
try rangeInBuffer(position: vtablePosition, size: length)
storage.tableCount += 1
if storage.tableCount > _options._maxTableCount {
throw FlatbuffersErrors.maximumTables
}
storage.depth += 1
if storage.depth > _options._maxDepth {
throw FlatbuffersErrors.maximumDepth
}
return TableVerifier(
position: position,
vtable: vtablePosition,
vtableLength: length,
verifier: &self)
}
/// Validates if a value of type `T` is within the buffer and returns it
/// - Parameter position: Current position to be read
/// - Throws: `inBuffer` errors
/// - Returns: a value of type `T` usually a `VTable` or a table offset
internal func getValue<T>(at position: Int) throws -> T {
try inBuffer(position: position, of: T.self)
return _buffer.read(def: T.self, position: position)
}
/// derefrences an offset within a vtable to get the position of the field
/// in the bytebuffer
/// - Parameter position: Current readable position
/// - Throws: `inBuffer` errors & `signedOffsetOutOfBounds`
/// - Returns: Current readable position for a field
@inline(__always)
internal func derefOffset(position: Int) throws -> Int {
try inBuffer(position: position, of: Int32.self)
let offset = _buffer.read(def: Int32.self, position: position)
// switching to int32 since swift's default Int is int64
// this should be safe since we already checked if its within
// the buffer
let _int32Position = UInt32(position)
let reportedOverflow: (partialValue: UInt32, overflow: Bool)
if offset > 0 {
reportedOverflow = _int32Position
.subtractingReportingOverflow(offset.magnitude)
} else {
reportedOverflow = _int32Position
.addingReportingOverflow(offset.magnitude)
}
/// since `subtractingReportingOverflow` & `addingReportingOverflow` returns true,
/// if there is overflow we return failure
if reportedOverflow.overflow || reportedOverflow.partialValue > _buffer
.capacity
{
throw FlatbuffersErrors.signedOffsetOutOfBounds(
offset: Int(offset),
position: position)
}
return Int(reportedOverflow.partialValue)
}
/// finishes the current iteration of verification on an object
internal func finish() {
storage.depth -= 1
}
@inline(__always)
func verify(id: String) throws {
let size = MemoryLayout<Int32>.size
guard storage.capacity >= (size * 2) else {
throw FlatbuffersErrors.bufferDoesntContainID
}
let str = _buffer.readString(at: size, count: size)
if id == str {
return
}
throw FlatbuffersErrors.bufferIdDidntMatchPassedId
}
final private class Storage {
/// Current ApparentSize
fileprivate var apparentSize: UOffset = 0
/// Amount of tables present within a buffer
fileprivate var tableCount = 0
/// Capacity of the current buffer
fileprivate let capacity: Int
/// Current reached depth within the buffer
fileprivate var depth = 0
init(capacity: Int) {
self.capacity = capacity
}
}
}
@@ -0,0 +1 @@
Sources/
@@ -0,0 +1,56 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
models = glob([
"Models/*.fbs",
])
model_names = [
f[7:-4] for f in models
]
generated_models = [ "{}_generated.swift".format(name) for name in model_names ]
flatc_input = " ".join([ "$(location Models/{}.fbs)".format(name) for name in model_names ])
genrule(
name = "GenerateModels",
srcs = models,
tools = [
"//third-party/flatc:flatc_bin"
],
cmd_bash =
"""
set -ex
FLATC="$$(pwd)/$(location //third-party/flatc:flatc_bin)"
BUILD_DIR="$(RULEDIR)/build"
rm -rf "$$BUILD_DIR"
mkdir -p "$$BUILD_DIR"
"$$FLATC" --require-explicit-ids --swift -o "$$BUILD_DIR" {flatc_input}
""".format(
flatc_input=flatc_input
) + "\n" + "\n".join([
"""
cp "$$BUILD_DIR/{name}_generated.swift" "$(location {name}_generated.swift)"
""".format(name=name) for name in model_names
]),
outs = generated_models,
visibility = [
"//visibility:public",
]
)
swift_library(
name = "FlatSerialization",
module_name = "FlatSerialization",
srcs = generated_models,
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/TelegramCore/FlatBuffers",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,16 @@
include "InstantPageBlock.fbs";
include "MediaId.fbs";
include "Media.fbs";
namespace TelegramCore;
table InstantPage {
blocks:[InstantPageBlock] (id: 0, required);
media:[Media] (id: 1, required);
isComplete:bool (id: 2);
rtl:bool (id: 3);
url:string (id: 4, required);
views:int32 (id: 5);
}
root_type InstantPage;
@@ -0,0 +1,235 @@
include "RichText.fbs";
include "MediaId.fbs";
include "TelegramChannel.fbs";
include "PixelDimensions.fbs";
include "RichText.fbs";
namespace TelegramCore;
union InstantPageBlock_Value {
InstantPageBlock_Unsupported,
InstantPageBlock_Title,
InstantPageBlock_Subtitle,
InstantPageBlock_AuthorDate,
InstantPageBlock_Header,
InstantPageBlock_Subheader,
InstantPageBlock_Paragraph,
InstantPageBlock_Preformatted,
InstantPageBlock_Footer,
InstantPageBlock_Divider,
InstantPageBlock_Anchor,
InstantPageBlock_List,
InstantPageBlock_BlockQuote,
InstantPageBlock_PullQuote,
InstantPageBlock_Image,
InstantPageBlock_Video,
InstantPageBlock_Audio,
InstantPageBlock_Cover,
InstantPageBlock_WebEmbed,
InstantPageBlock_PostEmbed,
InstantPageBlock_Collage,
InstantPageBlock_Slideshow,
InstantPageBlock_ChannelBanner,
InstantPageBlock_Kicker,
InstantPageBlock_Table,
InstantPageBlock_Details,
InstantPageBlock_RelatedArticles,
InstantPageBlock_Map
}
table InstantPageBlock {
value:InstantPageBlock_Value (id: 1, required);
}
table InstantPageBlock_Unsupported {}
table InstantPageBlock_Title {
text:RichText (id: 0, required);
}
table InstantPageBlock_Subtitle {
text:RichText (id: 0, required);
}
table InstantPageBlock_AuthorDate {
author:RichText (id: 0, required);
date:int32 (id: 1);
}
table InstantPageBlock_Header {
text:RichText (id: 0, required);
}
table InstantPageBlock_Subheader {
text:RichText (id: 0, required);
}
table InstantPageBlock_Paragraph {
text:RichText (id: 0, required);
}
table InstantPageBlock_Preformatted {
text:RichText (id: 0, required);
}
table InstantPageBlock_Footer {
text:RichText (id: 0, required);
}
table InstantPageBlock_Divider {}
table InstantPageBlock_Anchor {
name:string (id: 0, required);
}
table InstantPageBlock_List {
items:[InstantPageListItem] (id: 0, required);
ordered:bool (id: 1);
}
table InstantPageBlock_BlockQuote {
text:RichText (id: 0, required);
caption:RichText (id: 1, required);
}
table InstantPageBlock_PullQuote {
text:RichText (id: 0, required);
caption:RichText (id: 1, required);
}
table InstantPageBlock_Image {
id:MediaId (id: 0, required);
caption:InstantPageCaption (id: 1, required);
url:string (id: 2);
webpageId:MediaId (id: 3);
}
table InstantPageBlock_Video {
id:MediaId (id: 0, required);
caption:InstantPageCaption (id: 1, required);
autoplay:bool (id: 2);
loop:bool (id: 3);
}
table InstantPageBlock_Audio {
id:MediaId (id: 0, required);
caption:InstantPageCaption (id: 1, required);
}
table InstantPageBlock_Cover {
block:InstantPageBlock (id: 0, required);
}
table InstantPageBlock_WebEmbed {
url:string (id: 0);
html:string (id: 1);
dimensions:PixelDimensions (id: 2);
caption:InstantPageCaption (id: 3, required);
stretchToWidth:bool (id: 4);
allowScrolling:bool (id: 5);
coverId:MediaId (id: 6);
}
table InstantPageBlock_PostEmbed {
url:string (id: 0, required);
webpageId:MediaId (id: 1);
avatarId:MediaId (id: 2);
author:string (id: 3, required);
date:int32 (id: 4);
blocks:[InstantPageBlock] (id: 5, required);
caption:InstantPageCaption (id: 6, required);
}
table InstantPageBlock_Collage {
items:[InstantPageBlock] (id: 0, required);
caption:InstantPageCaption (id: 1, required);
}
table InstantPageBlock_Slideshow {
items:[InstantPageBlock] (id: 0, required);
caption:InstantPageCaption (id: 1, required);
}
table InstantPageBlock_ChannelBanner {
channel:TelegramChannel (id: 0);
}
table InstantPageBlock_Kicker {
text:RichText (id: 0, required);
}
table InstantPageBlock_Table {
title:RichText (id: 0, required);
rows:[InstantPageTableRow] (id: 1, required);
bordered:bool (id: 2);
striped:bool (id: 3);
}
table InstantPageBlock_Details {
title:RichText (id: 0, required);
blocks:[InstantPageBlock] (id: 1, required);
expanded:bool (id: 2);
}
table InstantPageBlock_RelatedArticles {
title:RichText (id: 0, required);
articles:[InstantPageRelatedArticle] (id: 1, required);
}
table InstantPageBlock_Map {
latitude:float64 (id: 0);
longitude:float64 (id: 1);
zoom:int32 (id: 2);
dimensions:PixelDimensions (id: 3, required);
caption:InstantPageCaption (id: 4, required);
}
table InstantPageCaption {
text:RichText (id: 0, required);
credit:RichText (id: 1, required);
}
union InstantPageListItem_Value {
InstantPageListItem_Text,
InstantPageListItem_Blocks,
InstantPageListItem_Unknown
}
table InstantPageListItem {
value:InstantPageListItem_Value (id: 1, required);
}
table InstantPageListItem_Text {
text:RichText (id: 0, required);
number:string (id: 1);
}
table InstantPageListItem_Blocks {
blocks:[InstantPageBlock] (id: 0, required);
number:string (id: 1);
}
table InstantPageListItem_Unknown {}
table InstantPageTableCell {
text:RichText (id: 0);
header:bool (id: 1);
alignment:int32 (id: 2);
verticalAlignment:int32 (id: 3);
colspan:int32 (id: 4);
rowspan:int32 (id: 5);
}
table InstantPageTableRow {
cells:[InstantPageTableCell] (id: 0, required);
}
table InstantPageRelatedArticle {
url:string (id: 0, required);
webpageId:MediaId (id: 1, required);
title:string (id: 2);
description:string (id: 3);
photoId:MediaId (id: 4);
author:string (id: 5);
date:int32 (id: 6);
}
@@ -0,0 +1,6 @@
namespace TelegramCore;
struct ItemCollectionId {
namespace: int;
id: int64;
}
@@ -0,0 +1,23 @@
include "MediaId.fbs";
include "TelegramMediaFile.fbs";
include "TelegramMediaImage.fbs";
namespace TelegramCore;
union Media_Value {
Media_TelegramMediaFile,
Media_TelegramMediaImage
}
table Media {
value:Media_Value (id: 1, required);
}
table Media_TelegramMediaFile {
file:TelegramMediaFile (id: 0, required);
}
table Media_TelegramMediaImage {
image:TelegramMediaImage (id: 0, required);
}
root_type Media;
@@ -0,0 +1,6 @@
namespace TelegramCore;
struct MediaId {
namespace: int;
id: int64;
}
@@ -0,0 +1,9 @@
include "PeerId.fbs";
namespace TelegramCore;
struct MessageId {
peerId: PeerId (id: 0);
namespace: int32 (id: 1);
id: int32 (id: 2);
}
@@ -0,0 +1,5 @@
namespace TelegramCore;
struct OptionalBool {
value:bool (id: 0);
}
@@ -0,0 +1,62 @@
include "MessageId.fbs";
include "PeerId.fbs";
include "PeerReference.fbs";
include "StickerPackReference.fbs";
namespace TelegramCore;
table MessageReference {
peer:PeerReference (id: 0, required);
author:PeerReference (id: 1);
messageId:MessageId (id: 2, required);
timestamp:int32 (id: 3);
incoming:bool (id: 4);
secret:bool (id: 5);
threadId:int64 (id: 6);
}
table WebpageReference {
webpageId:int64 (id: 0);
url:string (id: 1, required);
}
table PartialMediaReference_Message {
message:MessageReference (id: 0);
}
table PartialMediaReference_WebPage {
webPage:WebpageReference (id: 0);
}
table PartialMediaReference_StickerPack {
stickerPack:StickerPackReference (id: 0, required);
}
table PartialMediaReference_SavedGif {
}
table PartialMediaReference_SavedSticker {
}
table PartialMediaReference_RecentSticker {
}
table PartialMediaReference_SavedMusic {
peer:PeerReference (id: 0);
}
union PartialMediaReference_Value {
PartialMediaReference_Message,
PartialMediaReference_WebPage,
PartialMediaReference_StickerPack,
PartialMediaReference_SavedGif,
PartialMediaReference_SavedSticker,
PartialMediaReference_RecentSticker,
PartialMediaReference_SavedMusic
}
table PartialMediaReference {
value:PartialMediaReference_Value (id: 1, required);
}
root_type PartialMediaReference;
@@ -0,0 +1,11 @@
namespace TelegramCore;
table RestrictionRule {
platform:string (id: 0, required);
reason:string (id: 1, required);
text:string (id: 2, required);
}
table PeerAccessRestrictionInfo {
rules:[RestrictionRule] (id: 0, required);
}
@@ -0,0 +1,13 @@
namespace TelegramCore;
table PeerCollectibleColor {
collectibleId:long (id: 0);
giftEmojiFileId:long (id: 1);
backgroundEmojiId:long (id: 2);
accentColor:uint (id: 3);
colors:[uint] (id: 4);
darkAccentColor:uint (id: 5);
darkColors:[uint] (id: 6);
}
root_type PeerCollectibleColor;
@@ -0,0 +1,31 @@
namespace TelegramCore;
table PeerEmojiStatusContentEmoji {
fileId:int64 (id: 0);
}
table PeerEmojiStatusContentStarGift {
id:int64 (id: 0);
fileId:int64 (id: 1);
title:string (id: 2, required);
slug:string (id: 3, required);
patternFileId:int64 (id: 4);
innerColor:int32 (id: 5);
outerColor:int32 (id: 6);
patternColor:int32 (id: 7);
textColor:int32 (id: 8);
}
union PeerEmojiStatusContent_Value {
PeerEmojiStatusContentEmoji,
PeerEmojiStatusContentStarGift
}
table PeerEmojiStatusContent {
value:PeerEmojiStatusContent_Value (id: 1, required);
}
table PeerEmojiStatus {
content:PeerEmojiStatusContent (id: 0, required);
expirationDate:int32 (id: 1);
}
@@ -0,0 +1,6 @@
namespace TelegramCore;
struct PeerId {
namespace: int32 (id: 0);
id: int64 (id: 1);
}
@@ -0,0 +1,7 @@
namespace TelegramCore;
table PeerNameColor {
value:int32 (id: 0);
}
root_type PeerNameColor;
@@ -0,0 +1,27 @@
namespace TelegramCore;
table PeerReference_User {
id:int64 (id: 0);
accessHash:int64 (id: 1);
}
table PeerReference_Group {
id:int64 (id: 0);
}
table PeerReference_Channel {
id:int64 (id: 0);
accessHash:int64 (id: 1);
}
union PeerReference_Value {
PeerReference_User,
PeerReference_Group,
PeerReference_Channel
}
table PeerReference {
value:PeerReference_Value (id: 1, required);
}
root_type PeerReference;
@@ -0,0 +1,6 @@
namespace TelegramCore;
struct PixelDimensions {
width: int;
height: int;
}
@@ -0,0 +1,95 @@
include "MediaId.fbs";
include "PixelDimensions.fbs";
namespace TelegramCore;
union RichText_Value {
RichText_Empty,
RichText_Plain,
RichText_Bold,
RichText_Italic,
RichText_Underline,
RichText_Strikethrough,
RichText_Fixed,
RichText_Url,
RichText_Email,
RichText_Concat,
RichText_Subscript,
RichText_Superscript,
RichText_Marked,
RichText_Phone,
RichText_Image,
RichText_Anchor
}
table RichText {
value:RichText_Value (id: 1, required);
}
table RichText_Empty {}
table RichText_Plain {
text:string (id: 0, required);
}
table RichText_Bold {
text:RichText (id: 0, required);
}
table RichText_Italic {
text:RichText (id: 0, required);
}
table RichText_Underline {
text:RichText (id: 0, required);
}
table RichText_Strikethrough {
text:RichText (id: 0, required);
}
table RichText_Fixed {
text:RichText (id: 0, required);
}
table RichText_Url {
text:RichText (id: 0, required);
url:string (id: 1, required);
webpageId:MediaId (id: 2);
}
table RichText_Email {
text:RichText (id: 0, required);
email:string (id: 1, required);
}
table RichText_Concat {
texts:[RichText] (id: 0, required);
}
table RichText_Subscript {
text:RichText (id: 0, required);
}
table RichText_Superscript {
text:RichText (id: 0, required);
}
table RichText_Marked {
text:RichText (id: 0, required);
}
table RichText_Phone {
text:RichText (id: 0, required);
phone:string (id: 1, required);
}
table RichText_Image {
id:MediaId (id: 0, required);
dimensions:PixelDimensions (id: 1, required);
}
table RichText_Anchor {
text:RichText (id: 0, required);
name:string (id: 1, required);
}
@@ -0,0 +1,6 @@
namespace TelegramCore;
table StarsAmount {
value:int64 (id: 0);
nanos:int32 (id: 1);
}
@@ -0,0 +1,19 @@
include "ItemCollectionId.fbs";
include "TelegramMediaImageRepresentation.fbs";
namespace TelegramCore;
table StickerPackCollectionInfo {
id:ItemCollectionId (id: 0, required);
flags:int32 (id: 1);
accessHash:int64 (id: 2);
title:string (id: 3, required);
shortName:string (id: 4, required);
thumbnail:TelegramMediaImageRepresentation (id: 5);
thumbnailFileId:int64 (id: 6);
immediateThumbnailData:[ubyte] (id: 7);
hash:int32 (id: 8);
count:int32 (id: 9);
}
root_type StickerPackCollectionInfo;
@@ -0,0 +1,58 @@
namespace TelegramCore;
table StickerPackReference_Id {
id:int64 (id: 0);
accessHash:int64 (id: 1);
}
table StickerPackReference_Name {
name:string (id: 0, required);
}
table StickerPackReference_AnimatedEmoji {
}
table StickerPackReference_Dice {
emoji:string (id: 0, required);
}
table StickerPackReference_AnimatedEmojiAnimations {
}
table StickerPackReference_PremiumGifts {
}
table StickerPackReference_EmojiGenericAnimations {
}
table StickerPackReference_IconStatusEmoji {
}
table StickerPackReference_IconTopicEmoji {
}
table StickerPackReference_IconChannelStatusEmoji {
}
table StickerPackReference_TonGifts {
}
union StickerPackReference_Value {
StickerPackReference_Id,
StickerPackReference_Name,
StickerPackReference_AnimatedEmoji,
StickerPackReference_Dice,
StickerPackReference_AnimatedEmojiAnimations,
StickerPackReference_PremiumGifts,
StickerPackReference_EmojiGenericAnimations,
StickerPackReference_IconStatusEmoji,
StickerPackReference_IconTopicEmoji,
StickerPackReference_IconChannelStatusEmoji,
StickerPackReference_TonGifts
}
table StickerPackReference {
value:StickerPackReference_Value (id: 1, required);
}
root_type StickerPackReference;
@@ -0,0 +1,14 @@
namespace TelegramCore;
table DeviceContactPhoneNumberData {
label:string (id: 0, required);
value:string (id: 1, required);
}
table StoredDeviceContactData {
firstName:string (id: 0, required);
lastName:string (id: 1, required);
phoneNumbers:[DeviceContactPhoneNumberData] (id: 2);
}
root_type StoredDeviceContactData;
@@ -0,0 +1,62 @@
include "PeerId.fbs";
include "TelegramPeerAccessHash.fbs";
include "TelegramMediaImageRepresentation.fbs";
include "PeerAccessRestrictionInfo.fbs";
include "TelegramChatAdminRights.fbs";
include "TelegramChatBannedRights.fbs";
include "TelegramPeerUsername.fbs";
include "Optional.fbs";
include "PeerNameColor.fbs";
include "PeerEmojiStatus.fbs";
include "StarsAmount.fbs";
namespace TelegramCore;
table TelegramChannelInfo_Broadcast {
flags:int32 (id: 0);
}
table TelegramChannelInfo_Group {
flags:int32 (id: 0);
}
union TelegramChannelInfo_Value {
TelegramChannelInfo_Broadcast,
TelegramChannelInfo_Group
}
table TelegramChannelInfo {
value:TelegramChannelInfo_Value (id: 1, required);
}
table TelegramChannel {
id:PeerId (id: 0, required);
accessHash:TelegramPeerAccessHash (id: 1);
title:string (id: 2, required);
username:string (id: 3);
photo:[TelegramMediaImageRepresentation] (id: 4);
creationDate:int32 (id: 5);
version:int32 (id: 6);
participationStatus:int32 (id: 7);
info:TelegramChannelInfo (id: 8, required);
flags:int32 (id: 9);
restrictionInfo:PeerAccessRestrictionInfo (id: 10);
adminRights:TelegramChatAdminRights (id: 11);
bannedRights:TelegramChatBannedRights (id: 12);
defaultBannedRights:TelegramChatBannedRights (id: 13);
usernames:[TelegramPeerUsername] (id: 14);
storiesHidden:OptionalBool (id: 15);
nameColor:PeerNameColor (id: 16);
backgroundEmojiId:int64 (id: 17);
profileColor:PeerNameColor (id: 18);
profileBackgroundEmojiId:int64 (id: 19);
emojiStatus:PeerEmojiStatus (id: 20);
approximateBoostLevel:int32 (id: 21);
subscriptionUntilDate:int32 (id: 22);
verificationIconFileId:int64 (id: 23);
sendPaidMessageStars:StarsAmount (id: 24);
linkedMonoforumId:PeerId (id: 25);
linkedBotId:PeerId (id: 26, deprecated);
}
root_type TelegramChannel;
@@ -0,0 +1,7 @@
namespace TelegramCore;
table TelegramChatAdminRights {
rights:int32 (id: 0);
}
root_type TelegramChatAdminRights;
@@ -0,0 +1,7 @@
namespace TelegramCore;
table TelegramChatBannedRights {
flags:int32 (id: 0);
untilDate:int32 (id: 1);
}
@@ -0,0 +1,48 @@
include "PeerId.fbs";
include "TelegramMediaImageRepresentation.fbs";
include "TelegramChatAdminRights.fbs";
include "TelegramChatBannedRights.fbs";
namespace TelegramCore;
table TelegramGroupRole_Creator {
rank:string (id: 0);
}
table TelegramGroupRole_Admin {
rights:TelegramChatAdminRights (id: 0, required);
rank:string (id: 1);
}
table TelegramGroupRole_Member {}
union TelegramGroupRole_Value {
TelegramGroupRole_Creator,
TelegramGroupRole_Admin,
TelegramGroupRole_Member
}
table TelegramGroupRole {
value:TelegramGroupRole_Value (id: 1, required);
}
table TelegramGroupToChannelMigrationReference {
peerId:int64 (id: 0);
accessHash:int64 (id: 1);
}
table TelegramGroup {
id:PeerId (id: 0, required);
title:string (id: 1, required);
photo:[TelegramMediaImageRepresentation] (id: 2, required);
participantCount:int32 (id: 3);
role:TelegramGroupRole (id: 4, required);
membership:int32 (id: 5);
flags:int32 (id: 6);
defaultBannedRights:TelegramChatBannedRights (id: 7);
migrationReference:TelegramGroupToChannelMigrationReference (id: 8);
creationDate:int32 (id: 9);
version:int32 (id: 10);
}
root_type TelegramGroup;
@@ -0,0 +1,25 @@
include "MediaId.fbs";
include "TelegramMediaResource.fbs";
include "TelegramMediaFileAttribute.fbs";
include "TelegramMediaImageRepresentation.fbs";
include "VideoThumbnail.fbs";
include "TelegramMediaImage.fbs";
include "PartialMediaReference.fbs";
namespace TelegramCore;
table TelegramMediaFile {
fileId:MediaId (id: 0, required);
partialReference:PartialMediaReference (id: 1);
resource:TelegramMediaResource (id: 2, required);
previewRepresentations:[TelegramMediaImageRepresentation] (id: 3);
videoThumbnails:[VideoThumbnail] (id: 4);
videoCover:TelegramMediaImage (id: 5);
immediateThumbnailData:[ubyte] (id: 6);
mimeType:string (id: 7, required);
size:int64 (id: 8);
attributes:[TelegramMediaFileAttribute] (id: 9);
alternativeRepresentations:[TelegramMediaFile] (id: 10);
}
root_type TelegramMediaFile;
@@ -0,0 +1,85 @@
include "StickerPackReference.fbs";
namespace TelegramCore;
table TelegramMediaFileAttribute_FileName {
fileName:string (id: 0, required);
}
table StickerMaskCoords {
n:int32 (id: 0);
x:float32 (id: 1);
y:float32 (id: 2);
zoom:float32 (id: 3);
}
table TelegramMediaFileAttribute_Sticker {
displayText:string (id: 0, required);
packReference:StickerPackReference (id: 1);
maskData:StickerMaskCoords (id: 2);
}
table TelegramMediaFileAttribute_ImageSize {
width:int32 (id: 0);
height:int32 (id: 1);
}
table TelegramMediaFileAttribute_Animated {
}
table TelegramMediaFileAttribute_Video {
duration:float32 (id: 0);
width:int32 (id: 1);
height:int32 (id: 2);
flags:int32 (id: 3);
preloadSize:int32 (id: 4);
coverTime:float32 (id: 5);
videoCodec:string (id: 6);
}
table TelegramMediaFileAttribute_Audio {
isVoice:bool (id: 0);
duration:int32 (id: 1);
title:string (id: 2);
performer:string (id: 3);
waveform:[ubyte] (id: 4);
}
table TelegramMediaFileAttribute_HasLinkedStickers {
}
table TelegramMediaFileAttribute_HintFileIsLarge {
}
table TelegramMediaFileAttribute_HintIsValidated {
}
table TelegramMediaFileAttribute_NoPremium {
}
table TelegramMediaFileAttribute_CustomEmoji {
isPremium:bool (id: 0);
isSingleColor:bool (id: 1);
alt:string (id: 2, required);
packReference:StickerPackReference (id: 3);
}
union TelegramMediaFileAttribute_Value {
TelegramMediaFileAttribute_FileName,
TelegramMediaFileAttribute_Sticker,
TelegramMediaFileAttribute_ImageSize,
TelegramMediaFileAttribute_Animated,
TelegramMediaFileAttribute_Video,
TelegramMediaFileAttribute_Audio,
TelegramMediaFileAttribute_HasLinkedStickers,
TelegramMediaFileAttribute_HintFileIsLarge,
TelegramMediaFileAttribute_HintIsValidated,
TelegramMediaFileAttribute_NoPremium,
TelegramMediaFileAttribute_CustomEmoji
}
table TelegramMediaFileAttribute {
value:TelegramMediaFileAttribute_Value (id: 1, required);
}
root_type TelegramMediaFileAttribute;
@@ -0,0 +1,51 @@
include "MediaId.fbs";
include "TelegramMediaImageRepresentation.fbs";
include "StickerPackReference.fbs";
include "PartialMediaReference.fbs";
namespace TelegramCore;
table TelegramMediaImageReference {
imageId:int64 (id: 0);
accessHash:int64 (id: 1);
fileReference:[ubyte] (id: 2);
}
table VideoRepresentation {
width:int32 (id: 0);
height:int32 (id: 1);
resource:TelegramMediaResource (id: 2, required);
startTimestamp:float32 (id: 3);
}
table EmojiMarkup_Content_Emoji {
fileId:int64 (id: 0);
}
table EmojiMarkup_Content_Sticker {
packReference:StickerPackReference (id: 0, required);
fileId:int64 (id: 1);
}
union EmojiMarkup_Content {
EmojiMarkup_Content_Emoji,
EmojiMarkup_Content_Sticker
}
table EmojiMarkup {
content:EmojiMarkup_Content (id: 1, required);
backgroundColors:[int32] (id: 2);
}
table TelegramMediaImage {
imageId:MediaId (id: 0, required);
representations:[TelegramMediaImageRepresentation] (id: 1);
videoRepresentations:[VideoRepresentation] (id: 2);
immediateThumbnailData:[ubyte] (id: 3);
emojiMarkup:EmojiMarkup (id: 4);
reference:TelegramMediaImageReference (id: 5);
partialReference:PartialMediaReference (id: 6);
flags:int32 (id: 7);
}
root_type TelegramMediaImage;
@@ -0,0 +1,22 @@
include "TelegramMediaResource.fbs";
namespace TelegramCore;
enum TelegramMediaImageRepresentation_TypeHint:int32 {
generic = 0,
animated = 1,
video = 2
}
table TelegramMediaImageRepresentation {
width:int32 (id: 0);
height:int32 (id: 1);
resource:TelegramMediaResource (id: 2, required);
progressiveSizes:[int32] (id: 3);
immediateThumbnailData:[ubyte] (id: 4);
hasVideo:bool (id: 5);
isPersonal:bool (id: 6);
typeHint:TelegramMediaImageRepresentation_TypeHint (id: 7);
}
root_type TelegramMediaImageRepresentation;
@@ -0,0 +1,78 @@
namespace TelegramCore;
table TelegramMediaResource_CloudFileMediaResource {
datacenterId:int32 (id: 0);
volumeId:int64 (id: 1);
localId:int32 (id: 2);
secret:int64 (id: 3);
size:int64 (id: 4);
fileReference:[ubyte] (id: 5);
}
table TelegramMediaResource_CloudDocumentSizeMediaResource {
datacenterId:int32 (id: 0);
documentId:int64 (id: 1);
accessHash:int64 (id: 2);
sizeSpec:string (id: 3, required);
fileReference:[ubyte] (id: 4);
}
table TelegramMediaResource_CloudPhotoSizeMediaResource {
datacenterId:int32 (id: 0);
photoId:int64 (id: 1);
accessHash:int64 (id: 2);
sizeSpec:string (id: 3, required);
size:int64 (id: 4);
fileReference:[ubyte] (id: 5);
}
enum CloudPeerPhotoSizeSpec:int32 {
small = 0,
fullSize = 1
}
table TelegramMediaResource_CloudPeerPhotoSizeMediaResource {
datacenterId:int32 (id: 0);
photoId:int64 (id: 1);
sizeSpec:CloudPeerPhotoSizeSpec (id: 2);
volumeId:int64 (id: 3);
localId:int32 (id: 4);
}
table TelegramMediaResource_CloudStickerPackThumbnailMediaResource {
datacenterId:int32 (id: 0);
thumbVersion:int32 (id: 1);
volumeId:int64 (id: 2);
localId:int32 (id: 3);
}
table TelegramMediaResource_CloudDocumentMediaResource {
datacenterId:int32 (id: 0);
fileId:int64 (id: 1);
accessHash:int64 (id: 2);
size:int64 (id: 3);
fileReference:[ubyte] (id: 4);
fileName:string (id: 5);
}
table TelegramMediaResource_LocalFileMediaResource {
fileId:int64 (id: 0);
size:int64 (id: 1);
isSecretRelated:bool (id: 2);
}
union TelegramMediaResource_Value {
TelegramMediaResource_CloudFileMediaResource,
TelegramMediaResource_CloudDocumentSizeMediaResource,
TelegramMediaResource_CloudPhotoSizeMediaResource,
TelegramMediaResource_CloudPeerPhotoSizeMediaResource,
TelegramMediaResource_CloudStickerPackThumbnailMediaResource,
TelegramMediaResource_CloudDocumentMediaResource,
TelegramMediaResource_LocalFileMediaResource
}
table TelegramMediaResource {
value:TelegramMediaResource_Value (id: 1, required);
}
root_type TelegramMediaResource;
@@ -0,0 +1,18 @@
namespace TelegramCore;
table TelegramPeerAccessHash_Personal {
accessHash:int64 (id: 0);
}
table TelegramPeerAccessHash_GenericPublic {
accessHash:int64 (id: 0);
}
union TelegramPeerAccessHash_Value {
TelegramPeerAccessHash_Personal,
TelegramPeerAccessHash_GenericPublic
}
table TelegramPeerAccessHash {
value:TelegramPeerAccessHash_Value (id: 1, required);
}
@@ -0,0 +1,8 @@
namespace TelegramCore;
table TelegramPeerUsername {
flags:int32 (id: 0);
username:string (id: 1, required);
}
root_type TelegramPeerUsername;
@@ -0,0 +1,42 @@
include "PeerId.fbs";
include "TelegramPeerAccessHash.fbs";
include "TelegramMediaImageRepresentation.fbs";
include "PeerAccessRestrictionInfo.fbs";
include "PeerEmojiStatus.fbs";
include "TelegramPeerUsername.fbs";
include "PeerNameColor.fbs";
include "PeerCollectibleColor.fbs";
include "Optional.fbs";
include "StarsAmount.fbs";
namespace TelegramCore;
table BotUserInfo {
flags:int32 (id: 0);
inlinePlaceholder:string (id: 1);
}
table TelegramUser {
id:PeerId (id: 0, required);
accessHash:TelegramPeerAccessHash (id: 1);
firstName:string (id: 2);
lastName:string (id: 3);
username:string (id: 4);
phone:string (id: 5);
photo:[TelegramMediaImageRepresentation] (id: 6, required);
botInfo:BotUserInfo (id: 7);
restrictionInfo:PeerAccessRestrictionInfo (id: 8);
flags:int32 (id: 9);
emojiStatus:PeerEmojiStatus (id: 10);
usernames:[TelegramPeerUsername] (id: 11);
storiesHidden:OptionalBool (id: 12);
nameColor:PeerNameColor (id: 13);
backgroundEmojiId:int64 (id: 14);
profileColor:PeerNameColor (id: 15);
profileBackgroundEmojiId:int64 (id: 16);
subscriberCount:int32 (id: 17);
verificationIconFileId:int64 (id: 18);
collectibleColor:PeerCollectibleColor (id: 19);
}
root_type TelegramUser;
@@ -0,0 +1,11 @@
include "TelegramMediaResource.fbs";
namespace TelegramCore;
table VideoThumbnail {
width:int32 (id: 0);
height:int32 (id: 1);
resource:TelegramMediaResource (id: 2, required);
}
root_type VideoThumbnail;
@@ -0,0 +1,30 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "FlatSerialization",
platforms: [.macOS(.v10_13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "FlatSerialization",
targets: ["FlatSerialization"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(name: "FlatBuffers", path: "../FlatBuffers")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "FlatSerialization",
dependencies: [
.product(name: "FlatBuffers", package: "FlatBuffers", condition: nil),
],
path: "Sources"),
]
)
@@ -0,0 +1,72 @@
#!/bin/sh
# Default directories
OUTPUT_DIR=""
INPUT_DIR=""
BINARY_PATH=""
# Parse command line arguments
while [ "$#" -gt 0 ]; do
case "$1" in
--binary)
BINARY_PATH="$2"
shift 2
;;
--output)
OUTPUT_DIR="$2"
shift 2
;;
--input)
INPUT_DIR="$2"
shift 2
;;
*)
echo "Unknown parameter: $1"
exit 1
;;
esac
done
# Validate output directory
if [ -z "$OUTPUT_DIR" ]; then
echo "Error: --output argument is required"
exit 1
fi
# Validate output directory
if [ -z "$BINARY_PATH" ]; then
echo "Error: --binary argument is required"
exit 1
fi
if [ ! -d "$OUTPUT_DIR" ]; then
echo "Error: Output directory does not exist: $OUTPUT_DIR"
exit 1
fi
# Validate input directory
if [ -z "$INPUT_DIR" ]; then
echo "Error: --input argument is required"
exit 1
fi
if [ ! -d "$INPUT_DIR" ]; then
echo "Error: Input directory does not exist: $INPUT_DIR"
exit 1
fi
# Remove existing Swift files from output directory
rm -f "$OUTPUT_DIR"/*.swift
# Get all .fbs files in Models directory
models=$(ls "$INPUT_DIR"/*.fbs)
# Initialize empty flatc_input
flatc_input=""
# Build space-separated list of model paths
for model in $models; do
flatc_input="$flatc_input $model"
done
$BINARY_PATH --require-explicit-ids --swift -o "$OUTPUT_DIR" ${flatc_input}
+51
View File
@@ -0,0 +1,51 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "TelegramCore",
platforms: [.macOS(.v10_13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "TelegramCore",
targets: ["TelegramCore"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(name: "FlatBuffers", path: "./FlatBuffers"),
.package(name: "FlatSerialization", path: "./FlatSerialization"),
.package(name: "Postbox", path: "../Postbox"),
.package(name: "SSignalKit", path: "../SSignalKit"),
.package(name: "MtProtoKit", path: "../MtProtoKit"),
.package(name: "TelegramApi", path: "../TelegramApi"),
.package(name: "CryptoUtils", path: "../CryptoUtils"),
.package(name: "NetworkLogging", path: "../NetworkLogging"),
.package(name: "Reachability", path: "../Reachability"),
.package(name: "DarwinDirStat", path: "../Utils/DarwinDirStat"),
.package(name: "EncryptionProvider", path: "../EncryptionProvider"),
.package(name: "Emoji", path: "../Emoji"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "TelegramCore",
dependencies: [.product(name: "Postbox", package: "Postbox", condition: nil),
.product(name: "SwiftSignalKit", package: "SSignalKit", condition: nil),
.product(name: "MtProtoKit", package: "MtProtoKit", condition: nil),
.product(name: "TelegramApi", package: "TelegramApi", condition: nil),
.product(name: "CryptoUtils", package: "CryptoUtils", condition: nil),
.product(name: "NetworkLogging", package: "NetworkLogging", condition: nil),
.product(name: "DarwinDirStat", package: "DarwinDirStat", condition: nil),
.product(name: "Reachability", package: "Reachability", condition: nil),
.product(name: "Emoji", package: "Emoji", condition: nil),
.product(name: "FlatBuffers", package: "FlatBuffers", condition: nil),
.product(name: "FlatSerialization", package: "FlatSerialization", condition: nil),
.product(name: "EncryptionProvider", package: "EncryptionProvider", condition: nil)],
path: "Sources",
swiftSettings: [.unsafeFlags(["-warnings-as-errors"])]),
]
)
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
}
}
}
}

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