Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2022-09-17 20:28:20 +03:00
commit 38ca457a26
65 changed files with 1509 additions and 592 deletions

2
.gitignore vendored
View File

@ -62,5 +62,5 @@ bazel-testlogs/*
*/*.swp */*.swp
*.swp *.swp
build-input/* build-input/*
*/*.pyc **/*.pyc
*.pyc *.pyc

View File

@ -18,8 +18,8 @@ internal:
except: except:
- tags - tags
script: script:
- bash buildbox/build-telegram.sh hockeyapp - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="http://host.docker.internal:8650" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="/telegram-private-data/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64
- bash buildbox/deploy-telegram.sh hockeyapp - python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-internal.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip"
environment: environment:
name: internal name: internal
artifacts: artifacts:
@ -37,8 +37,8 @@ appstore_development:
except: except:
- tags - tags
script: script:
- bash buildbox/build-telegram.sh appstore-development - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="http://host.docker.internal:8650" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64
- bash buildbox/deploy-telegram.sh appstore-development - python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appstore-development.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip"
environment: environment:
name: appstore-development name: appstore-development
artifacts: artifacts:
@ -48,15 +48,15 @@ appstore_development:
experimental_i: experimental_i:
tags: tags:
- ios_internal - ios_experimental
stage: build stage: build
only: only:
- experimental-3 - experimental-3
except: except:
- tags - tags
script: script:
- bash buildbox/build-telegram.sh appcenter-experimental - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="http://host.docker.internal:8650" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64
- bash buildbox/deploy-telegram.sh appcenter-experimental - python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-experimental.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip"
environment: environment:
name: experimental name: experimental
artifacts: artifacts:
@ -73,8 +73,8 @@ experimental:
except: except:
- tags - tags
script: script:
- bash buildbox/build-telegram.sh appcenter-experimental-2 - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="http://host.docker.internal:8650" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="$TELEGRAM_PRIVATE_DATA_PATH/build-configurations/enterprise-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=enterprise --configuration=release_arm64
- bash buildbox/deploy-telegram.sh appcenter-experimental-2 - python3 -u build-system/Make/DeployToAppCenter.py --configuration="$TELEGRAM_PRIVATE_DATA_PATH/appcenter-configurations/appcenter-experimental2.json" --ipa="build/artifacts/Telegram.ipa" --dsyms="build/artifacts/Telegram.DSYMs.zip"
environment: environment:
name: experimental-2 name: experimental-2
artifacts: artifacts:
@ -92,7 +92,7 @@ beta_testflight:
except: except:
- tags - tags
script: script:
- bash buildbox/build-telegram.sh appstore - PYTHONPATH="$PYTHONPATH:/darwin-containers" python3 -u build-system/Make/Make.py remote-build --darwinContainersHost="http://host.docker.internal:8650" --cacheHost="$TELEGRAM_BAZEL_CACHE_HOST" --configurationPath="build-system/appstore-configuration.json" --gitCodesigningRepository="$TELEGRAM_GIT_CODESIGNING_REPOSITORY" --gitCodesigningType=appstore --configuration=release_universal
environment: environment:
name: testflight_llc name: testflight_llc
artifacts: artifacts:

View File

@ -464,7 +464,7 @@ private struct NotificationContent: CustomStringConvertible {
return string return string
} }
mutating func addSenderInfo(mediaBox: MediaBox, accountPeerId: PeerId, peer: Peer) { mutating func addSenderInfo(mediaBox: MediaBox, accountPeerId: PeerId, peer: Peer, contactIdentifier: String?) {
if #available(iOS 15.0, *) { if #available(iOS 15.0, *) {
let image = peerAvatar(mediaBox: mediaBox, accountPeerId: accountPeerId, peer: peer) let image = peerAvatar(mediaBox: mediaBox, accountPeerId: accountPeerId, peer: peer)
@ -483,7 +483,7 @@ private struct NotificationContent: CustomStringConvertible {
nameComponents: personNameComponents, nameComponents: personNameComponents,
displayName: displayName, displayName: displayName,
image: image, image: image,
contactIdentifier: nil, contactIdentifier: contactIdentifier,
customIdentifier: "\(peer.id.toInt64())", customIdentifier: "\(peer.id.toInt64())",
isMe: false, isMe: false,
suggestionType: .none suggestionType: .none
@ -818,6 +818,7 @@ private final class NotificationServiceHandler {
var updates: String var updates: String
var accountId: Int64 var accountId: Int64
var peer: EnginePeer? var peer: EnginePeer?
var localContactId: String?
} }
var callData: CallData? var callData: CallData?
@ -1033,35 +1034,51 @@ private final class NotificationServiceHandler {
if let action = action { if let action = action {
switch action { switch action {
case let .call(callData): case let .call(callData):
let voipPayload: [AnyHashable: Any] = [ if let stateManager = strongSelf.stateManager {
"call_id": "\(callData.id)",
"call_ah": "\(callData.accessHash)",
"from_id": "\(callData.fromId.id._internalGetInt64Value())",
"updates": callData.updates,
"accountId": "\(callData.accountId)"
]
if #available(iOS 14.5, *), voiceCallSettings.enableSystemIntegration {
Logger.shared.log("NotificationService \(episode)", "Will report voip notification")
let content = NotificationContent(isLockedMessage: nil) let content = NotificationContent(isLockedMessage: nil)
updateCurrentContent(content) updateCurrentContent(content)
CXProvider.reportNewIncomingVoIPPushPayload(voipPayload, completion: { error in let _ = (stateManager.postbox.transaction { transaction -> String? in
Logger.shared.log("NotificationService \(episode)", "Did report voip notification, error: \(String(describing: error))") if let peer = transaction.getPeer(callData.fromId) as? TelegramUser {
return peer.phone
} else {
return nil
}
}).start(next: { phoneNumber in
var voipPayload: [AnyHashable: Any] = [
"call_id": "\(callData.id)",
"call_ah": "\(callData.accessHash)",
"from_id": "\(callData.fromId.id._internalGetInt64Value())",
"updates": callData.updates,
"accountId": "\(callData.accountId)"
]
if let phoneNumber = phoneNumber {
voipPayload["phoneNumber"] = phoneNumber
}
completed() if #available(iOS 14.5, *), voiceCallSettings.enableSystemIntegration {
Logger.shared.log("NotificationService \(episode)", "Will report voip notification")
let content = NotificationContent(isLockedMessage: nil)
updateCurrentContent(content)
CXProvider.reportNewIncomingVoIPPushPayload(voipPayload, completion: { error in
Logger.shared.log("NotificationService \(episode)", "Did report voip notification, error: \(String(describing: error))")
completed()
})
} else {
var content = NotificationContent(isLockedMessage: nil)
if let peer = callData.peer {
content.title = peer.debugDisplayTitle
content.body = incomingCallMessage
} else {
content.body = "Incoming Call"
}
updateCurrentContent(content)
completed()
}
}) })
} else {
var content = NotificationContent(isLockedMessage: nil)
if let peer = callData.peer {
content.title = peer.debugDisplayTitle
content.body = incomingCallMessage
} else {
content.body = "Incoming Call"
}
updateCurrentContent(content)
completed()
} }
case .logout: case .logout:
Logger.shared.log("NotificationService \(episode)", "Will logout") Logger.shared.log("NotificationService \(episode)", "Will logout")
@ -1334,7 +1351,23 @@ private final class NotificationServiceHandler {
if let interactionAuthorId = interactionAuthorId { if let interactionAuthorId = interactionAuthorId {
if inAppNotificationSettings.displayNameOnLockscreen, let peer = transaction.getPeer(interactionAuthorId) { if inAppNotificationSettings.displayNameOnLockscreen, let peer = transaction.getPeer(interactionAuthorId) {
content.addSenderInfo(mediaBox: stateManager.postbox.mediaBox, accountPeerId: stateManager.accountPeerId, peer: peer) var foundLocalId: String?
transaction.enumerateDeviceContactImportInfoItems({ _, value in
if let value = value as? TelegramDeviceContactImportedData {
switch value {
case let .imported(data, _, peerId):
if peerId == interactionAuthorId {
foundLocalId = data.localIdentifiers.first
return false
}
default:
break
}
}
return true
})
content.addSenderInfo(mediaBox: stateManager.postbox.mediaBox, accountPeerId: stateManager.accountPeerId, peer: peer, contactIdentifier: foundLocalId)
} }
} }

View File

@ -0,0 +1,311 @@
import json
import os
import sys
import shutil
import tempfile
import plistlib
from BuildEnvironment import run_executable_with_output, check_run_system
class BuildConfiguration:
def __init__(self,
bundle_id,
api_id,
api_hash,
team_id,
app_center_id,
is_internal_build,
is_appstore_build,
appstore_id,
app_specific_url_scheme,
premium_iap_product_id,
enable_siri,
enable_icloud
):
self.bundle_id = bundle_id
self.api_id = api_id
self.api_hash = api_hash
self.team_id = team_id
self.app_center_id = app_center_id
self.is_internal_build = is_internal_build
self.is_appstore_build = is_appstore_build
self.appstore_id = appstore_id
self.app_specific_url_scheme = app_specific_url_scheme
self.premium_iap_product_id = premium_iap_product_id
self.enable_siri = enable_siri
self.enable_icloud = enable_icloud
def write_to_variables_file(self, aps_environment, path):
string = ''
string += 'telegram_bundle_id = "{}"\n'.format(self.bundle_id)
string += 'telegram_api_id = "{}"\n'.format(self.api_id)
string += 'telegram_api_hash = "{}"\n'.format(self.api_hash)
string += 'telegram_team_id = "{}"\n'.format(self.team_id)
string += 'telegram_app_center_id = "{}"\n'.format(self.app_center_id)
string += 'telegram_is_internal_build = "{}"\n'.format(self.is_internal_build)
string += 'telegram_is_appstore_build = "{}"\n'.format(self.is_appstore_build)
string += 'telegram_appstore_id = "{}"\n'.format(self.appstore_id)
string += 'telegram_app_specific_url_scheme = "{}"\n'.format(self.app_specific_url_scheme)
string += 'telegram_premium_iap_product_id = "{}"\n'.format(self.premium_iap_product_id)
string += 'telegram_aps_environment = "{}"\n'.format(aps_environment)
string += 'telegram_enable_siri = {}\n'.format(self.enable_siri)
string += 'telegram_enable_icloud = {}\n'.format(self.enable_icloud)
string += 'telegram_enable_watch = True\n'
if os.path.exists(path):
os.remove(path)
with open(path, 'w+') as file:
file.write(string)
def build_configuration_from_json(path):
if not os.path.exists(path):
print('Could not load build configuration from {}'.format(path))
sys.exit(1)
with open(path) as file:
configuration_dict = json.load(file)
required_keys = [
'bundle_id',
'api_id',
'api_hash',
'team_id',
'app_center_id',
'is_internal_build',
'is_appstore_build',
'appstore_id',
'app_specific_url_scheme',
'premium_iap_product_id',
'enable_siri',
'enable_icloud'
]
for key in required_keys:
if key not in configuration_dict:
print('Configuration at {} does not contain {}'.format(path, key))
return BuildConfiguration(
bundle_id=configuration_dict['bundle_id'],
api_id=configuration_dict['api_id'],
api_hash=configuration_dict['api_hash'],
team_id=configuration_dict['team_id'],
app_center_id=configuration_dict['app_center_id'],
is_internal_build=configuration_dict['is_internal_build'],
is_appstore_build=configuration_dict['is_appstore_build'],
appstore_id=configuration_dict['appstore_id'],
app_specific_url_scheme=configuration_dict['app_specific_url_scheme'],
premium_iap_product_id=configuration_dict['premium_iap_product_id'],
enable_siri=configuration_dict['enable_siri'],
enable_icloud=configuration_dict['enable_icloud']
)
def decrypt_codesigning_directory_recursively(source_base_path, destination_base_path, password):
for file_name in os.listdir(source_base_path):
source_path = source_base_path + '/' + file_name
destination_path = destination_base_path + '/' + file_name
if os.path.isfile(source_path):
os.system('openssl aes-256-cbc -md md5 -k "{password}" -in "{source_path}" -out "{destination_path}" -a -d 2>/dev/null'.format(
password=password,
source_path=source_path,
destination_path=destination_path
))
elif os.path.isdir(source_path):
os.makedirs(destination_path, exist_ok=True)
decrypt_codesigning_directory_recursively(source_path, destination_path, password)
def load_codesigning_data_from_git(working_dir, repo_url, temp_key_path, branch, password, always_fetch):
if not os.path.exists(working_dir):
os.makedirs(working_dir, exist_ok=True)
ssh_command = 'ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
if temp_key_path is not None:
ssh_command += ' -i {}'.format(temp_key_path)
encrypted_working_dir = working_dir + '/encrypted'
if os.path.exists(encrypted_working_dir):
if always_fetch:
original_working_dir = os.getcwd()
os.chdir(encrypted_working_dir)
check_run_system('GIT_SSH_COMMAND="{ssh_command}" git fetch'.format(ssh_command=ssh_command))
check_run_system('git checkout "{branch}"'.format(branch=branch))
check_run_system('GIT_SSH_COMMAND="{ssh_command}" git pull'.format(ssh_command=ssh_command))
os.chdir(original_working_dir)
else:
os.makedirs(encrypted_working_dir, exist_ok=True)
original_working_dir = os.getcwd()
os.chdir(working_dir)
check_run_system('GIT_SSH_COMMAND="{ssh_command}" git clone --depth=1 {repo_url} -b "{branch}" "{target_path}"'.format(
ssh_command=ssh_command,
repo_url=repo_url,
branch=branch,
target_path=encrypted_working_dir
))
os.chdir(original_working_dir)
decrypted_working_dir = working_dir + '/decrypted'
if os.path.exists(decrypted_working_dir):
shutil.rmtree(decrypted_working_dir)
os.makedirs(decrypted_working_dir, exist_ok=True)
decrypt_codesigning_directory_recursively(encrypted_working_dir + '/profiles', decrypted_working_dir + '/profiles', password)
decrypt_codesigning_directory_recursively(encrypted_working_dir + '/certs', decrypted_working_dir + '/certs', password)
def copy_profiles_from_directory(source_path, destination_path, team_id, bundle_id):
profile_name_mapping = {
'.SiriIntents': 'Intents',
'.NotificationContent': 'NotificationContent',
'.NotificationService': 'NotificationService',
'.Share': 'Share',
'': 'Telegram',
'.watchkitapp': 'WatchApp',
'.watchkitapp.watchkitextension': 'WatchExtension',
'.Widget': 'Widget',
'.BroadcastUpload': 'BroadcastUpload'
}
for file_name in os.listdir(source_path):
file_path = source_path + '/' + file_name
if os.path.isfile(file_path):
if not file_path.endswith('.mobileprovision'):
continue
profile_data = run_executable_with_output('openssl', arguments=[
'smime',
'-inform',
'der',
'-verify',
'-noverify',
'-in',
file_path
], decode=False, stderr_to_stdout=False, check_result=True)
profile_dict = plistlib.loads(profile_data)
profile_name = profile_dict['Entitlements']['application-identifier']
if profile_name.startswith(team_id + '.' + bundle_id):
profile_base_name = profile_name[len(team_id + '.' + bundle_id):]
if profile_base_name in profile_name_mapping:
shutil.copyfile(file_path, destination_path + '/' + profile_name_mapping[profile_base_name] + '.mobileprovision')
else:
print('Warning: skipping provisioning profile at {} with bundle_id {} (base_name {})'.format(file_path, profile_name, profile_base_name))
def resolve_aps_environment_from_directory(source_path, team_id, bundle_id):
for file_name in os.listdir(source_path):
file_path = source_path + '/' + file_name
if os.path.isfile(file_path):
if not file_path.endswith('.mobileprovision'):
continue
profile_data = run_executable_with_output('openssl', arguments=[
'smime',
'-inform',
'der',
'-verify',
'-noverify',
'-in',
file_path
], decode=False, stderr_to_stdout=False, check_result=True)
profile_dict = plistlib.loads(profile_data)
profile_name = profile_dict['Entitlements']['application-identifier']
if profile_name.startswith(team_id + '.' + bundle_id):
profile_base_name = profile_name[len(team_id + '.' + bundle_id):]
if profile_base_name == '':
if 'aps-environment' not in profile_dict['Entitlements']:
print('Provisioning profile at {} does not include an aps-environment entitlement'.format(file_path))
sys.exit(1)
return profile_dict['Entitlements']['aps-environment']
return None
def copy_certificates_from_directory(source_path, destination_path):
for file_name in os.listdir(source_path):
file_path = source_path + '/' + file_name
if os.path.isfile(file_path):
if file_path.endswith('.p12') or file_path.endswith('.cer'):
shutil.copyfile(file_path, destination_path + '/' + file_name)
class CodesigningSource:
def __init__(self):
pass
def load_data(self, working_dir):
raise Exception('Not implemented')
def copy_profiles_to_destination(self, destination_path):
raise Exception('Not implemented')
def resolve_aps_environment(self):
raise Exception('Not implemented')
def copy_certificates_to_destination(self, destination_path):
raise Exception('Not implemented')
class GitCodesigningSource(CodesigningSource):
def __init__(self, repo_url, private_key, team_id, bundle_id, codesigning_type, password, always_fetch):
self.repo_url = repo_url
self.private_key = private_key
self.team_id = team_id
self.bundle_id = bundle_id
self.codesigning_type = codesigning_type
self.password = password
self.always_fetch = always_fetch
def load_data(self, working_dir):
self.working_dir = working_dir
temp_key_path = None
if self.private_key is not None:
temp_key_path = tempfile.mktemp()
with open(temp_key_path, 'w+') as file:
file.write(self.private_key)
if not self.private_key.endswith('\n'):
file.write('\n')
os.chmod(temp_key_path, 0o600)
load_codesigning_data_from_git(working_dir=self.working_dir, repo_url=self.repo_url, temp_key_path=temp_key_path, branch=self.team_id, password=self.password, always_fetch=self.always_fetch)
if temp_key_path is not None:
os.remove(temp_key_path)
def copy_profiles_to_destination(self, destination_path):
source_path = self.working_dir + '/decrypted/profiles/{}'.format(self.codesigning_type)
copy_profiles_from_directory(source_path=source_path, destination_path=destination_path, team_id=self.team_id, bundle_id=self.bundle_id)
def resolve_aps_environment(self):
source_path = self.working_dir + '/decrypted/profiles/{}'.format(self.codesigning_type)
return resolve_aps_environment_from_directory(source_path=source_path, team_id=self.team_id, bundle_id=self.bundle_id)
def copy_certificates_to_destination(self, destination_path):
source_path = None
if self.codesigning_type in ['adhoc', 'appstore']:
source_path = self.working_dir + '/decrypted/certs/distribution'
elif self.codesigning_type == 'enterprise':
source_path = self.working_dir + '/decrypted/certs/enterprise'
elif self.codesigning_type == 'development':
source_path = self.working_dir + '/decrypted/certs/development'
else:
raise Exception('Unknown codesigning type {}'.format(self.codesigning_type))
copy_certificates_from_directory(source_path=source_path, destination_path=destination_path)
class DirectoryCodesigningSource(CodesigningSource):
def __init__(self, directory_path, team_id, bundle_id):
self.directory_path = directory_path
self.team_id = team_id
self.bundle_id = bundle_id
def load_data(self, working_dir):
pass
def copy_profiles_to_destination(self, destination_path):
copy_profiles_from_directory(source_path=self.directory_path + '/profiles', destination_path=destination_path, team_id=self.team_id, bundle_id=self.bundle_id)
def resolve_aps_environment(self):
return resolve_aps_environment_from_directory(source_path=self.directory_path + '/profiles', team_id=self.team_id, bundle_id=self.bundle_id)
def copy_certificates_to_destination(self, destination_path):
copy_certificates_from_directory(source_path=self.directory_path + '/certs', destination_path=destination_path)

View File

@ -2,7 +2,7 @@ import json
import os import os
import platform import platform
import subprocess import subprocess
import sys
def is_apple_silicon(): def is_apple_silicon():
if platform.processor() == 'arm': if platform.processor() == 'arm':
@ -28,20 +28,41 @@ def resolve_executable(program):
return None return None
def run_executable_with_output(path, arguments): def run_executable_with_output(path, arguments, decode=True, input=None, stderr_to_stdout=True, print_command=False, check_result=False):
executable_path = resolve_executable(path) executable_path = resolve_executable(path)
if executable_path is None: if executable_path is None:
raise Exception('Could not resolve {} to a valid executable file'.format(path)) raise Exception('Could not resolve {} to a valid executable file'.format(path))
stderr_assignment = subprocess.DEVNULL
if stderr_to_stdout:
stderr_assignment = subprocess.STDOUT
if print_command:
print('Running {} {}'.format(executable_path, arguments))
process = subprocess.Popen( process = subprocess.Popen(
[executable_path] + arguments, [executable_path] + arguments,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stderr=stderr_assignment,
stdin=subprocess.PIPE,
env=get_clean_env() env=get_clean_env()
) )
output_data, _ = process.communicate() if input is not None:
output_data, _ = process.communicate(input=input)
else:
output_data, _ = process.communicate()
output_string = output_data.decode('utf-8') output_string = output_data.decode('utf-8')
return output_string
if check_result:
if process.returncode != 0:
print('Command {} {} finished with non-zero return code and output:\n{}'.format(executable_path, arguments, output_string))
sys.exit(1)
if decode:
return output_string
else:
return output_data
def call_executable(arguments, use_clean_environment=True, check_result=True): def call_executable(arguments, use_clean_environment=True, check_result=True):
@ -62,6 +83,12 @@ def call_executable(arguments, use_clean_environment=True, check_result=True):
subprocess.call(resolved_arguments, env=resolved_env) subprocess.call(resolved_arguments, env=resolved_env)
def check_run_system(command):
if os.system(command) != 0:
print('Command failed: {}'.format(command))
sys.exit(1)
def get_bazel_version(bazel_path): def get_bazel_version(bazel_path):
command_result = run_executable_with_output(bazel_path, ['--version']).strip('\n') command_result = run_executable_with_output(bazel_path, ['--version']).strip('\n')
if not command_result.startswith('bazel '): if not command_result.startswith('bazel '):

View File

@ -0,0 +1,70 @@
import os
import sys
import argparse
import json
from BuildEnvironment import check_run_system
def deploy_to_appcenter(args):
if not os.path.exists(args.configuration):
print('{} does not exist'.format(args.configuration))
sys.exit(1)
if not os.path.exists(args.ipa):
print('{} does not exist'.format(args.ipa))
sys.exit(1)
if args.dsyms is not None and not os.path.exists(args.dsyms):
print('{} does not exist'.format(args.dsyms))
sys.exit(1)
with open(args.configuration) as file:
configuration_dict = json.load(file)
required_keys = [
'username',
'app_name',
'api_token',
]
for key in required_keys:
if key not in configuration_dict:
print('Configuration at {} does not contain {}'.format(args.configuration, key))
check_run_system('appcenter login --token {token}'.format(token=configuration_dict['api_token']))
check_run_system('appcenter distribute release --app "{username}/{app_name}" -f "{ipa_path}" -g Internal'.format(
username=configuration_dict['username'],
app_name=configuration_dict['app_name'],
ipa_path=args.ipa,
))
if args.dsyms is not None:
check_run_system('appcenter crashes upload-symbols --app "{username}/{app_name}" --symbol "{dsym_path}"'.format(
username=configuration_dict['username'],
app_name=configuration_dict['app_name'],
dsym_path=args.dsyms
))
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='deploy-appcenter')
parser.add_argument(
'--configuration',
required=True,
help='Path to configuration json.'
)
parser.add_argument(
'--ipa',
required=True,
help='Path to IPA.'
)
parser.add_argument(
'--dsyms',
required=False,
help='Path to DSYMs.zip.'
)
if len(sys.argv) < 2:
parser.print_help()
sys.exit(1)
args = parser.parse_args()
deploy_to_appcenter(args)

View File

@ -0,0 +1,62 @@
import os
import sys
import argparse
import json
from BuildEnvironment import check_run_system
def deploy_to_appstore_connect(args):
if not os.path.exists(args.configuration):
print('{} does not exist'.format(args.configuration))
sys.exit(1)
if not os.path.exists(args.ipa):
print('{} does not exist'.format(args.ipa))
sys.exit(1)
with open(args.configuration) as file:
configuration_dict = json.load(file)
required_keys = [
'username',
'app_name',
'api_token',
]
for key in required_keys:
if key not in configuration_dict:
print('Configuration at {} does not contain {}'.format(args.configuration, key))
check_run_system('appcenter login --token {token}'.format(token=configuration_dict['api_token']))
check_run_system('appcenter distribute release --app "{username}/{app_name}" -f "{ipa_path}" -g Internal'.format(
username=configuration_dict['username'],
app_name=configuration_dict['app_name'],
ipa_path=args.ipa,
))
if args.dsyms is not None:
check_run_system('appcenter crashes upload-symbols --app "{username}/{app_name}" --symbol "{dsym_path}"'.format(
username=configuration_dict['username'],
app_name=configuration_dict['app_name'],
dsym_path=args.dsyms
))
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='deploy-appstore-connect')
parser.add_argument(
'--configuration',
required=True,
help='Path to configuration json.'
)
parser.add_argument(
'--ipa',
required=True,
help='Path to IPA.'
)
if len(sys.argv) < 2:
parser.print_help()
sys.exit(1)
args = parser.parse_args()
deploy_to_appstore_connect(args)

View File

@ -0,0 +1,95 @@
import os
import sys
import argparse
from BuildEnvironment import run_executable_with_output
def import_certificates(certificatesPath):
if not os.path.exists(certificatesPath):
print('{} does not exist'.format(certificatesPath))
sys.exit(1)
keychain_name = 'temp.keychain'
keychain_password = 'secret'
existing_keychains = run_executable_with_output('security', arguments=['list-keychains'], check_result=True)
if keychain_name in existing_keychains:
run_executable_with_output('security', arguments=['delete-keychain'], check_result=True)
run_executable_with_output('security', arguments=[
'create-keychain',
'-p',
keychain_password,
keychain_name
], check_result=True)
existing_keychains = run_executable_with_output('security', arguments=['list-keychains', '-d', 'user'])
existing_keychains.replace('"', '')
run_executable_with_output('security', arguments=[
'list-keychains',
'-d',
'user',
'-s',
keychain_name,
existing_keychains
], check_result=True)
run_executable_with_output('security', arguments=['set-keychain-settings', keychain_name])
run_executable_with_output('security', arguments=['unlock-keychain', '-p', keychain_password, keychain_name])
for file_name in os.listdir(certificatesPath):
file_path = certificatesPath + '/' + file_name
if file_path.endswith('.p12') or file_path.endswith('.cer'):
run_executable_with_output('security', arguments=[
'import',
file_path,
'-k',
keychain_name,
'-P',
'',
'-T',
'/usr/bin/codesign',
'-T',
'/usr/bin/security'
], check_result=True)
run_executable_with_output('security', arguments=[
'import',
'build-system/AppleWWDRCAG3.cer',
'-k',
keychain_name,
'-P',
'',
'-T',
'/usr/bin/codesign',
'-T',
'/usr/bin/security'
], check_result=True)
run_executable_with_output('security', arguments=[
'set-key-partition-list',
'-S',
'apple-tool:,apple:',
'-k',
keychain_password,
keychain_name
], check_result=True)
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='build')
parser.add_argument(
'--path',
required=True,
help='Path to certificates.'
)
if len(sys.argv) < 2:
parser.print_help()
sys.exit(1)
args = parser.parse_args()
import_certificates(args.path)

View File

@ -6,10 +6,20 @@ import shlex
import sys import sys
import tempfile import tempfile
import subprocess import subprocess
import shutil
import glob
from BuildEnvironment import resolve_executable, call_executable, BuildEnvironment from BuildEnvironment import resolve_executable, call_executable, run_executable_with_output, BuildEnvironment
from ProjectGeneration import generate from ProjectGeneration import generate
from BazelLocation import locate_bazel from BazelLocation import locate_bazel
from BuildConfiguration import CodesigningSource, GitCodesigningSource, DirectoryCodesigningSource, BuildConfiguration, build_configuration_from_json
import RemoteBuild
class ResolvedCodesigningData:
def __init__(self, aps_environment):
self.aps_environment = aps_environment
class BazelCommandLine: class BazelCommandLine:
def __init__(self, bazel, override_bazel_version, override_xcode_version, bazel_user_root): def __init__(self, bazel, override_bazel_version, override_xcode_version, bazel_user_root):
@ -380,34 +390,95 @@ def clean(bazel, arguments):
bazel_command_line.invoke_clean() bazel_command_line.invoke_clean()
def resolve_configuration(bazel_command_line: BazelCommandLine, arguments): def resolve_codesigning(arguments, base_path, build_configuration, provisioning_profiles_path, additional_codesigning_output_path) -> ResolvedCodesigningData:
if arguments.configurationGenerator is not None: profile_source = None
configuration_generator_arguments = shlex.split(arguments.configurationGenerator) if arguments.gitCodesigningRepository is not None:
password = os.getenv('TELEGRAM_CODESIGNING_GIT_PASSWORD')
if password is None:
print('TELEGRAM_CODESIGNING_GIT_PASSWORD environment variable is not set')
sys.exit(1)
configuration_generator_executable = resolve_executable(configuration_generator_arguments[0]) if arguments.gitCodesigningType is None:
print('--gitCodesigningType is required if --gitCodesigningRepository is set')
sys.exit(1)
if configuration_generator_executable is None: private_key = os.getenv('TELEGRAM_CODESIGNING_PRIVATE_KEY')
print('{} is not a valid executable'.format(configuration_generator_arguments[0]))
exit(1)
temp_configuration_path = tempfile.mkdtemp() profile_source = GitCodesigningSource(
repo_url=arguments.gitCodesigningRepository,
resolved_configuration_generator_arguments = [configuration_generator_executable] private_key=private_key,
resolved_configuration_generator_arguments += configuration_generator_arguments[1:] team_id=build_configuration.team_id,
resolved_configuration_generator_arguments += [temp_configuration_path] bundle_id=build_configuration.bundle_id,
codesigning_type=arguments.gitCodesigningType,
call_executable(resolved_configuration_generator_arguments, use_clean_environment=False) password=password,
always_fetch=arguments.gitCodesigningAlwaysFetch
print('TelegramBuild: using generated configuration in {}'.format(temp_configuration_path)) )
bazel_command_line.set_configuration_path(temp_configuration_path) elif arguments.codesigningInformationPath is not None:
elif arguments.configurationPath is not None: profile_source = DirectoryCodesigningSource(
absolute_configuration_path = os.path.abspath(arguments.configurationPath) directory_path=arguments.codesigningInformationPath,
if not os.path.isdir(absolute_configuration_path): team_id=build_configuration.team_id,
print('Error: {} does not exist'.format(absolute_configuration_path)) bundle_id=build_configuration.bundle_id
exit(1) )
bazel_command_line.set_configuration_path(absolute_configuration_path)
else: else:
raise Exception('Neither configurationPath nor configurationGenerator are set') raise Exception('Neither gitCodesigningRepository nor codesigningInformationPath are set')
workdir_path = '{}/build-input/configuration-repository-workdir'.format(base_path)
os.makedirs(workdir_path, exist_ok=True)
profile_source.load_data(working_dir=workdir_path)
if provisioning_profiles_path is not None:
profile_source.copy_profiles_to_destination(destination_path=provisioning_profiles_path)
if additional_codesigning_output_path is not None:
profile_source.copy_profiles_to_destination(destination_path=additional_codesigning_output_path + '/profiles')
profile_source.copy_certificates_to_destination(destination_path=additional_codesigning_output_path + '/certs')
return ResolvedCodesigningData(aps_environment=profile_source.resolve_aps_environment())
def resolve_configuration(base_path, bazel_command_line: BazelCommandLine, arguments, additional_codesigning_output_path):
configuration_repository_path = '{}/build-input/configuration-repository'.format(base_path)
os.makedirs(configuration_repository_path, exist_ok=True)
build_configuration = build_configuration_from_json(path=arguments.configurationPath)
with open(configuration_repository_path + '/WORKSPACE', 'w+') as file:
pass
with open(configuration_repository_path + '/BUILD', 'w+') as file:
pass
provisioning_path = '{}/provisioning'.format(configuration_repository_path)
if os.path.exists(provisioning_path):
shutil.rmtree(provisioning_path)
os.makedirs(provisioning_path, exist_ok=True)
codesigning_data = resolve_codesigning(
arguments=arguments,
base_path=base_path,
build_configuration=build_configuration,
provisioning_profiles_path=provisioning_path,
additional_codesigning_output_path=additional_codesigning_output_path
)
if codesigning_data.aps_environment is None:
print('Could not find a valid aps-environment entitlement in the provided provisioning profiles')
sys.exit(1)
build_configuration.write_to_variables_file(aps_environment=codesigning_data.aps_environment, path=configuration_repository_path + '/variables.bzl')
provisioning_profile_files = []
for file_name in os.listdir(provisioning_path):
if file_name.endswith('.mobileprovision'):
provisioning_profile_files.append(file_name)
with open(provisioning_path + '/BUILD', 'w+') as file:
file.write('exports_files([\n')
for file_name in provisioning_profile_files:
file.write(' "{}",\n'.format(file_name))
file.write('])\n')
if bazel_command_line is not None:
bazel_command_line.set_configuration_path(configuration_repository_path)
def generate_project(bazel, arguments): def generate_project(bazel, arguments):
@ -425,7 +496,12 @@ def generate_project(bazel, arguments):
bazel_command_line.set_continue_on_error(arguments.continueOnError) bazel_command_line.set_continue_on_error(arguments.continueOnError)
resolve_configuration(bazel_command_line, arguments) resolve_configuration(
base_path=os.getcwd(),
bazel_command_line=bazel_command_line,
arguments=arguments,
additional_codesigning_output_path=None
)
bazel_command_line.set_build_number(arguments.buildNumber) bazel_command_line.set_build_number(arguments.buildNumber)
@ -469,7 +545,12 @@ def build(bazel, arguments):
elif arguments.cacheHost is not None: elif arguments.cacheHost is not None:
bazel_command_line.add_remote_cache(arguments.cacheHost) bazel_command_line.add_remote_cache(arguments.cacheHost)
resolve_configuration(bazel_command_line, arguments) resolve_configuration(
base_path=os.getcwd(),
bazel_command_line=bazel_command_line,
arguments=arguments,
additional_codesigning_output_path=None
)
bazel_command_line.set_configuration(arguments.configuration) bazel_command_line.set_configuration(arguments.configuration)
bazel_command_line.set_build_number(arguments.buildNumber) bazel_command_line.set_build_number(arguments.buildNumber)
@ -481,6 +562,38 @@ def build(bazel, arguments):
bazel_command_line.invoke_build() bazel_command_line.invoke_build()
if arguments.outputBuildArtifactsPath is not None:
artifacts_path = os.path.abspath(arguments.outputBuildArtifactsPath)
if os.path.exists(artifacts_path + '/Telegram.ipa'):
os.remove(path)
if os.path.exists(artifacts_path + '/DSYMs'):
shutil.rmtree(artifacts_path + '/DSYMs')
os.makedirs(artifacts_path, exist_ok=True)
os.makedirs(artifacts_path + '/DSYMs', exist_ok=True)
ipa_paths = glob.glob('bazel-out/applebin_ios-ios_arm*-opt-ST-*/bin/Telegram/Telegram.ipa')
if len(ipa_paths) == 0:
print('Could not find the IPA at bazel-out/applebin_ios-ios_arm*-opt-ST-*/bin/Telegram/Telegram.ipa')
sys.exit(1)
elif len(ipa_paths) > 1:
print('Multiple matching IPA files found: {}'.format(ipa_paths))
sys.exit(1)
shutil.copyfile(ipa_paths[0], artifacts_path + '/Telegram.ipa')
dsym_paths = glob.glob('bazel-out/applebin_ios-ios_arm*-opt-ST-*/bin/Telegram/*.dSYM')
for dsym_path in dsym_paths:
file_name = os.path.basename(dsym_path)
shutil.copytree(dsym_path, artifacts_path + '/DSYMs/{}'.format(file_name))
previous_directory = os.getcwd()
os.chdir(artifacts_path)
run_executable_with_output('zip', arguments=[
'-r',
'Telegram.DSYMs.zip',
'./DSYMs'
], check_result=True)
os.chdir(previous_directory)
shutil.rmtree(artifacts_path + '/DSYMs')
def test(bazel, arguments): def test(bazel, arguments):
bazel_command_line = BazelCommandLine( bazel_command_line = BazelCommandLine(
@ -495,7 +608,12 @@ def test(bazel, arguments):
elif arguments.cacheHost is not None: elif arguments.cacheHost is not None:
bazel_command_line.add_remote_cache(arguments.cacheHost) bazel_command_line.add_remote_cache(arguments.cacheHost)
resolve_configuration(bazel_command_line, arguments) resolve_configuration(
base_path=os.getcwd(),
bazel_command_line=bazel_command_line,
arguments=arguments,
additional_codesigning_output_path=None
)
bazel_command_line.set_configuration('debug_sim_arm64') bazel_command_line.set_configuration('debug_sim_arm64')
bazel_command_line.set_build_number('10000') bazel_command_line.set_build_number('10000')
@ -503,29 +621,64 @@ def test(bazel, arguments):
bazel_command_line.invoke_test() bazel_command_line.invoke_test()
def add_project_and_build_common_arguments(current_parser: argparse.ArgumentParser): def add_codesigning_common_arguments(current_parser: argparse.ArgumentParser):
group = current_parser.add_mutually_exclusive_group(required=True) configuration_group = current_parser.add_mutually_exclusive_group(required=True)
group.add_argument( configuration_group.add_argument(
'--configurationPath', '--configurationPath',
help=''' help='''
Path to a folder containing build configuration and provisioning profiles. Path to a json containing build configuration.
See build-system/example-configuration for an example. See build-system/appstore-configuration.json for an example.
''', ''',
metavar='path' metavar='path'
) )
group.add_argument(
'--configurationGenerator', codesigning_group = current_parser.add_mutually_exclusive_group(required=True)
codesigning_group.add_argument(
'--gitCodesigningRepository',
help=''' help='''
A command line invocation that will dynamically generate the configuration data If specified, certificates and provisioning profiles will be loaded from git.
(project constants and provisioning profiles). TELEGRAM_CODESIGNING_GIT_PASSWORD environment variable must be set.
The expression will be parsed according to the shell parsing rules into program and arguments parts. ''',
The program will be then invoked with the given arguments plus the path to the output directory. metavar='path'
See build-system/generate-configuration.sh for an example. )
Example: --configurationGenerator="sh ~/my_script.sh argument1" codesigning_group.add_argument(
'--codesigningInformationPath',
help='''
Use signing certificates and provisioning profiles from a local directory.
''', ''',
metavar='command' metavar='command'
) )
current_parser.add_argument(
'--gitCodesigningType',
choices=[
'development',
'adhoc',
'appstore',
'enterprise'
],
required=False,
help='''
The name of the folder to use inside "profiles" folder in the git repository.
Required if gitCodesigningRepository is specified.
''',
metavar='type'
)
current_parser.add_argument(
'--gitCodesigningAlwaysFetch',
action='store_true',
required=False,
default=True,
help='''
Always refresh codesigning repository.
'''
)
def add_project_and_build_common_arguments(current_parser: argparse.ArgumentParser):
add_codesigning_common_arguments(current_parser=current_parser)
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='Make') parser = argparse.ArgumentParser(prog='Make')
@ -575,7 +728,7 @@ if __name__ == '__main__':
'--cacheHost', '--cacheHost',
required=False, required=False,
help='Use remote build artifact cache to speed up rebuilds (See https://github.com/buchgr/bazel-remote).', help='Use remote build artifact cache to speed up rebuilds (See https://github.com/buchgr/bazel-remote).',
metavar='http://host:9092' metavar='grpc://host:9092'
) )
cacheTypeGroup.add_argument( cacheTypeGroup.add_argument(
'--cacheDir', '--cacheDir',
@ -700,6 +853,40 @@ if __name__ == '__main__':
default=False, default=False,
help='Enable sandbox.', help='Enable sandbox.',
) )
buildParser.add_argument(
'--outputBuildArtifactsPath',
required=False,
help='Store IPA and DSYM at the specified path after a successful build.',
metavar='arguments'
)
remote_build_parser = subparsers.add_parser('remote-build', help='Build the app using a remote environment.')
add_codesigning_common_arguments(remote_build_parser)
remote_build_parser.add_argument(
'--darwinContainersHost',
required=True,
type=str,
help='DarwinContainers host address.'
)
remote_build_parser.add_argument(
'--configuration',
choices=[
'debug_universal',
'debug_arm64',
'debug_armv7',
'release_arm64',
'release_armv7',
'release_universal'
],
required=True,
help='Build configuration'
)
remote_build_parser.add_argument(
'--cacheHost',
required=False,
type=str,
help='Bazel remote cache host address.'
)
if len(sys.argv) < 2: if len(sys.argv) < 2:
parser.print_help() parser.print_help()
@ -726,6 +913,30 @@ if __name__ == '__main__':
generate_project(bazel=bazel_path, arguments=args) generate_project(bazel=bazel_path, arguments=args)
elif args.commandName == 'build': elif args.commandName == 'build':
build(bazel=bazel_path, arguments=args) build(bazel=bazel_path, arguments=args)
elif args.commandName == 'remote-build':
base_path = os.getcwd()
remote_input_path = '{}/build-input/remote-input'.format(base_path)
if os.path.exists(remote_input_path):
shutil.rmtree(remote_input_path)
os.makedirs(remote_input_path)
os.makedirs(remote_input_path + '/certs')
os.makedirs(remote_input_path + '/profiles')
resolve_configuration(
base_path=os.getcwd(),
bazel_command_line=None,
arguments=args,
additional_codesigning_output_path=remote_input_path
)
shutil.copyfile(args.configurationPath, remote_input_path + '/configuration.json')
RemoteBuild.remote_build(
darwin_containers_host=args.darwinContainersHost,
bazel_cache_host=args.cacheHost,
configuration=args.configuration,
build_input_data_path=remote_input_path
)
elif args.commandName == 'test': elif args.commandName == 'test':
test(bazel=bazel_path, arguments=args) test(bazel=bazel_path, arguments=args)
else: else:

View File

@ -0,0 +1,141 @@
import os
import sys
import json
import shutil
import shlex
import tempfile
from BuildEnvironment import run_executable_with_output
def session_scp_upload(session, source_path, destination_path):
scp_command = 'scp -i {privateKeyPath} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr {source_path} containerhost@"{ipAddress}":{destination_path}'.format(
privateKeyPath=session.privateKeyPath,
ipAddress=session.ipAddress,
source_path=shlex.quote(source_path),
destination_path=shlex.quote(destination_path)
)
if os.system(scp_command) != 0:
print('Command {} finished with a non-zero status'.format(scp_command))
def session_scp_download(session, source_path, destination_path):
scp_command = 'scp -i {privateKeyPath} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr containerhost@"{ipAddress}":{source_path} {destination_path}'.format(
privateKeyPath=session.privateKeyPath,
ipAddress=session.ipAddress,
source_path=shlex.quote(source_path),
destination_path=shlex.quote(destination_path)
)
if os.system(scp_command) != 0:
print('Command {} finished with a non-zero status'.format(scp_command))
def session_ssh(session, command):
ssh_command = 'ssh -i {privateKeyPath} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null containerhost@"{ipAddress}" -o ServerAliveInterval=60 -t "{command}"'.format(
privateKeyPath=session.privateKeyPath,
ipAddress=session.ipAddress,
command=command
)
return os.system(ssh_command)
def remote_build(darwin_containers_host, bazel_cache_host, configuration, build_input_data_path):
macos_version = '12.5'
from darwin_containers import DarwinContainers
base_dir = os.getcwd()
configuration_path = 'versions.json'
xcode_version = ''
with open(configuration_path) as file:
configuration_dict = json.load(file)
if configuration_dict['xcode'] is None:
raise Exception('Missing xcode version in {}'.format(configuration_path))
xcode_version = configuration_dict['xcode']
print('Xcode version: {}'.format(xcode_version))
commit_count = run_executable_with_output('git', [
'rev-list',
'--count',
'HEAD'
])
build_number_offset = 0
with open('build_number_offset') as file:
build_number_offset = int(file.read())
build_number = build_number_offset + int(commit_count)
print('Build number: {}'.format(build_number))
image_name = 'macos-{macos_version}-xcode-{xcode_version}'.format(macos_version=macos_version, xcode_version=xcode_version)
print('Image name: {}'.format(image_name))
source_dir = os.path.basename(base_dir)
buildbox_dir = 'buildbox'
transient_data_dir = '{}/transient-data'.format(buildbox_dir)
os.makedirs(transient_data_dir, exist_ok=True)
source_archive_path = '{buildbox_dir}/transient-data/source.tar'.format(buildbox_dir=buildbox_dir)
if os.path.exists(source_archive_path):
os.remove(source_archive_path)
print('Compressing source code...')
os.system('find . -type f -a -not -regex "\\." -a -not -regex ".*\\./git" -a -not -regex ".*\\./git/.*" -a -not -regex "\\./bazel-bin" -a -not -regex "\\./bazel-bin/.*" -a -not -regex "\\./bazel-out" -a -not -regex "\\./bazel-out/.*" -a -not -regex "\\./bazel-testlogs" -a -not -regex "\\./bazel-testlogs/.*" -a -not -regex "\\./bazel-telegram-ios" -a -not -regex "\\./bazel-telegram-ios/.*" -a -not -regex "\\./buildbox" -a -not -regex "\\./buildbox/.*" -a -not -regex "\\./buck-out" -a -not -regex "\\./buck-out/.*" -a -not -regex "\\./\\.buckd" -a -not -regex "\\./\\.buckd/.*" -a -not -regex "\\./build" -a -not -regex "\\./build/.*" -print0 | tar cf "{buildbox_dir}/transient-data/source.tar" --null -T -'.format(buildbox_dir=buildbox_dir))
darwinContainers = DarwinContainers(serverAddress=darwin_containers_host, verbose=False)
print('Opening container session...')
with darwinContainers.workingImageSession(name=image_name) as session:
print('Uploading data to container...')
session_scp_upload(session=session, source_path=build_input_data_path, destination_path='telegram-build-input')
session_scp_upload(session=session, source_path='{base_dir}/{buildbox_dir}/transient-data/source.tar'.format(base_dir=base_dir, buildbox_dir=buildbox_dir), destination_path='')
guest_build_sh = '''
set -x
set -e
mkdir /Users/Shared/telegram-ios
cd /Users/Shared/telegram-ios
tar -xf $HOME/source.tar
python3 build-system/Make/ImportCertificates.py --path $HOME/telegram-build-input/certs
'''
guest_build_sh += 'python3 build-system/Make/Make.py \\'
if bazel_cache_host is not None:
guest_build_sh += '--cacheHost="{}" \\'.format(bazel_cache_host)
guest_build_sh += 'build \\'
guest_build_sh += ''
guest_build_sh += '--buildNumber={} \\'.format(build_number)
guest_build_sh += '--configuration={} \\'.format(configuration)
guest_build_sh += '--configurationPath=$HOME/telegram-build-input/configuration.json \\'
guest_build_sh += '--codesigningInformationPath=$HOME/telegram-build-input \\'
guest_build_sh += '--outputBuildArtifactsPath=/Users/Shared/telegram-ios/build/artifacts \\'
guest_build_file_path = tempfile.mktemp()
with open(guest_build_file_path, 'w+') as file:
file.write(guest_build_sh)
session_scp_upload(session=session, source_path=guest_build_file_path, destination_path='guest-build-telegram.sh')
os.unlink(guest_build_file_path)
print('Executing remote build...')
session_ssh(session=session, command='bash -l guest-build-telegram.sh')
print('Retrieving build artifacts...')
artifacts_path='{base_dir}/build/artifacts'.format(base_dir=base_dir)
if os.path.exists(artifacts_path):
shutil.rmtree(artifacts_path)
os.makedirs(artifacts_path, exist_ok=True)
session_scp_download(session=session, source_path='/Users/Shared/telegram-ios/build/artifacts/*', destination_path='{artifacts_path}/'.format(artifacts_path=artifacts_path))
if os.path.exists(artifacts_path + '/Telegram.ipa'):
print('Artifacts have been stored at {}'.format(artifacts_path))
else:
print('Telegram.ipa not found')
sys.exit(1)

View File

@ -0,0 +1,14 @@
{
"bundle_id": "ph.telegra.Telegraph",
"api_id": "8",
"api_hash": "7245de8e747a0d6fbe11f7cc14fcc0bb",
"team_id": "C67CF9S4VU",
"app_center_id": "0",
"is_internal_build": "false",
"is_appstore_build": "true",
"appstore_id": "686449807",
"app_specific_url_scheme": "tg",
"premium_iap_product_id": "org.telegram.telegramPremium.monthly",
"enable_siri": true,
"enable_icloud": true
}

View File

@ -1,358 +0,0 @@
#!/usr/bin/python3
import argparse
import json
import os
import sys
import shlex
import shutil
import subprocess
import time
from darwin_containers import DarwinContainers
def get_clean_env():
clean_env = os.environ.copy()
return clean_env
def resolve_executable(program):
def is_executable(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
for path in get_clean_env()["PATH"].split(os.pathsep):
executable_file = os.path.join(path, program)
if is_executable(executable_file):
return executable_file
return None
def run_executable_with_output(path, arguments):
executable_path = resolve_executable(path)
if executable_path is None:
raise Exception('Could not resolve {} to a valid executable file'.format(path))
process = subprocess.Popen(
[executable_path] + arguments,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=get_clean_env()
)
output_data, _ = process.communicate()
output_string = output_data.decode('utf-8')
return output_string
def session_scp_upload(session, source_path, destination_path):
scp_command = 'scp -i {privateKeyPath} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr {source_path} containerhost@"{ipAddress}":{destination_path}'.format(
privateKeyPath=session.privateKeyPath,
ipAddress=session.ipAddress,
source_path=shlex.quote(source_path),
destination_path=shlex.quote(destination_path)
)
if os.system(scp_command) != 0:
print('Command {} finished with a non-zero status'.format(scp_command))
def session_ssh(session, command):
ssh_command = 'ssh -i {privateKeyPath} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null containerhost@"{ipAddress}" -o ServerAliveInterval=60 -t "{command}"'.format(
privateKeyPath=session.privateKeyPath,
ipAddress=session.ipAddress,
command=command
)
return os.system(ssh_command)
def remote_build(darwin_containers_host, configuration):
base_dir = os.getcwd()
configuration_path = 'versions.json'
xcode_version = ''
with open(configuration_path) as file:
configuration_dict = json.load(file)
if configuration_dict['xcode'] is None:
raise Exception('Missing xcode version in {}'.format(configuration_path))
xcode_version = configuration_dict['xcode']
print('Xcode version: {}'.format(xcode_version))
commit_count = run_executable_with_output('git', [
'rev-list',
'--count',
'HEAD'
])
build_number_offset = 0
with open('build_number_offset') as file:
build_number_offset = int(file.read())
build_number = build_number_offset + int(commit_count)
print('Build number: {}'.format(build_number))
macos_version = '12.5'
image_name = 'macos-{macos_version}-xcode-{xcode_version}'.format(macos_version=macos_version, xcode_version=xcode_version)
print('Image name: {}'.format(image_name))
buildbox_dir = 'buildbox'
os.makedirs('{buildbox_dir}/transient-data'.format(buildbox_dir=buildbox_dir), exist_ok=True)
codesigning_subpath = ''
remote_configuration = ''
if configuration == 'appcenter':
remote_configuration = 'hockeyapp'
elif configuration == 'appstore':
remote_configuration = 'appstore'
elif configuration == 'reproducible':
codesigning_subpath = 'build-system/fake-codesigning'
remote_configuration = 'verify'
destination_codesigning_path = '{buildbox_dir}/transient-data/telegram-codesigning'.format(buildbox_dir=buildbox_dir)
destination_build_configuration_path = '{buildbox_dir}/transient-data/build-configuration'.format(buildbox_dir=buildbox_dir)
if os.path.exists(destination_codesigning_path):
shutil.rmtree(destination_codesigning_path)
if os.path.exists(destination_build_configuration_path):
shutil.rmtree(destination_build_configuration_path)
shutil.copytree('build-system/fake-codesigning', '{buildbox_dir}/transient-data/telegram-codesigning'.format(buildbox_dir=buildbox_dir))
shutil.copytree('build-system/example-configuration', '{buildbox_dir}/transient-data/build-configuration'.format(buildbox_dir=buildbox_dir))
else:
print('Unknown configuration {}'.format(configuration))
sys.exit(1)
source_dir = os.path.basename(base_dir)
source_archive_path = '{buildbox_dir}/transient-data/source.tar'.format(buildbox_dir=buildbox_dir)
if os.path.exists(source_archive_path):
os.remove(source_archive_path)
print('Compressing source code...')
os.system('find . -type f -a -not -regex "\\." -a -not -regex ".*\\./git" -a -not -regex ".*\\./git/.*" -a -not -regex "\\./bazel-bin" -a -not -regex "\\./bazel-bin/.*" -a -not -regex "\\./bazel-out" -a -not -regex "\\./bazel-out/.*" -a -not -regex "\\./bazel-testlogs" -a -not -regex "\\./bazel-testlogs/.*" -a -not -regex "\\./bazel-telegram-ios" -a -not -regex "\\./bazel-telegram-ios/.*" -a -not -regex "\\./buildbox" -a -not -regex "\\./buildbox/.*" -a -not -regex "\\./buck-out" -a -not -regex "\\./buck-out/.*" -a -not -regex "\\./\\.buckd" -a -not -regex "\\./\\.buckd/.*" -a -not -regex "\\./build" -a -not -regex "\\./build/.*" -print0 | tar cf "{buildbox_dir}/transient-data/source.tar" --null -T -'.format(buildbox_dir=buildbox_dir))
darwinContainers = DarwinContainers(serverAddress=darwin_containers_host, verbose=False)
print('Opening container session...')
with darwinContainers.workingImageSession(name=image_name) as session:
print('Uploading data to container...')
session_scp_upload(session=session, source_path=codesigning_subpath, destination_path='codesigning_data')
session_scp_upload(session=session, source_path='{base_dir}/{buildbox_dir}/transient-data/build-configuration'.format(base_dir=base_dir, buildbox_dir=buildbox_dir), destination_path='telegram-configuration')
session_scp_upload(session=session, source_path='{base_dir}/{buildbox_dir}/guest-build-telegram.sh'.format(base_dir=base_dir, buildbox_dir=buildbox_dir), destination_path='')
session_scp_upload(session=session, source_path='{base_dir}/{buildbox_dir}/transient-data/source.tar'.format(base_dir=base_dir, buildbox_dir=buildbox_dir), destination_path='')
print('Executing remote build...')
bazel_cache_host=''
session_ssh(session=session, command='BUILD_NUMBER="{build_number}" BAZEL_HTTP_CACHE_URL="{bazel_cache_host}" bash -l guest-build-telegram.sh {remote_configuration}'.format(
build_number=build_number,
bazel_cache_host=bazel_cache_host,
remote_configuration=remote_configuration
))
print('Retrieving build artifacts...')
artifacts_path='{base_dir}/build/artifacts'.format(base_dir=base_dir)
if os.path.exists(artifacts_path):
shutil.rmtree(artifacts_path)
os.makedirs(artifacts_path, exist_ok=True)
session_scp_download(session=session, source_path='telegram-ios/build/artifacts/*', destination_path='{artifacts_path}/'.format(artifacts_path=artifacts_path))
print('Artifacts have been stored at {}'.format(artifacts_path))
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='build')
parser.add_argument(
'--verbose',
action='store_true',
default=False,
help='Print debug info'
)
subparsers = parser.add_subparsers(dest='commandName', help='Commands')
remote_build_parser = subparsers.add_parser('remote-build', help='Build the app using a remote environment.')
remote_build_parser.add_argument(
'--darwinContainersHost',
required=True,
type=str,
help='DarwinContainers host address.'
)
remote_build_parser.add_argument(
'--configuration',
choices=[
'appcenter',
'appstore',
'reproducible'
],
required=True,
help='Build configuration'
)
if len(sys.argv) < 2:
parser.print_help()
sys.exit(1)
args = parser.parse_args()
if args.commandName is None:
exit(0)
if args.commandName == 'remote-build':
remote_build(darwin_containers_host=args.darwinContainersHost, configuration=args.configuration)
'''set -e
rm -f "tools/bazel"
cp "$BAZEL" "tools/bazel"
BUILD_CONFIGURATION="$1"
if [ "$BUILD_CONFIGURATION" == "hockeyapp" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental-2" ]; then
CODESIGNING_SUBPATH="$BUILDBOX_DIR/transient-data/telegram-codesigning/codesigning"
elif [ "$BUILD_CONFIGURATION" == "appstore" ] || [ "$BUILD_CONFIGURATION" == "appstore-development" ]; then
CODESIGNING_SUBPATH="$BUILDBOX_DIR/transient-data/telegram-codesigning/codesigning"
elif [ "$BUILD_CONFIGURATION" == "verify" ]; then
CODESIGNING_SUBPATH="build-system/fake-codesigning"
else
echo "Unknown configuration $1"
exit 1
fi
COMMIT_COMMENT="$(git log -1 --pretty=%B)"
case "$COMMIT_COMMENT" in
*"[nocache]"*)
export BAZEL_HTTP_CACHE_URL=""
;;
esac
COMMIT_ID="$(git rev-parse HEAD)"
COMMIT_AUTHOR=$(git log -1 --pretty=format:'%an')
if [ -z "$2" ]; then
COMMIT_COUNT=$(git rev-list --count HEAD)
BUILD_NUMBER_OFFSET="$(cat build_number_offset)"
COMMIT_COUNT="$(($COMMIT_COUNT+$BUILD_NUMBER_OFFSET))"
BUILD_NUMBER="$COMMIT_COUNT"
else
BUILD_NUMBER="$2"
fi
BASE_DIR=$(pwd)
if [ "$BUILD_CONFIGURATION" == "hockeyapp" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental-2" ] || [ "$BUILD_CONFIGURATION" == "appstore" ] || [ "$BUILD_CONFIGURATION" == "appstore-development" ]; then
if [ ! `which generate-configuration.sh` ]; then
echo "generate-configuration.sh not found in PATH $PATH"
exit 1
fi
mkdir -p "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning"
mkdir -p "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
case "$BUILD_CONFIGURATION" in
"hockeyapp"|"appcenter-experimental"|"appcenter-experimental-2")
generate-configuration.sh internal release "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning" "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
;;
"appstore")
generate-configuration.sh appstore release "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning" "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
;;
"appstore-development")
generate-configuration.sh appstore development "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning" "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
;;
*)
echo "Unknown build configuration $BUILD_CONFIGURATION"
exit 1
;;
esac
elif [ "$BUILD_CONFIGURATION" == "verify" ]; then
mkdir -p "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning"
mkdir -p "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
cp -R build-system/fake-codesigning/* "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning/"
cp -R build-system/example-configuration/* "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration/"
fi
if [ ! -d "$CODESIGNING_SUBPATH" ]; then
echo "$CODESIGNING_SUBPATH does not exist"
exit 1
fi
SOURCE_DIR=$(basename "$BASE_DIR")
rm -f "$BUILDBOX_DIR/transient-data/source.tar"
set -x
find . -type f -a -not -regex "\\." -a -not -regex ".*\\./git" -a -not -regex ".*\\./git/.*" -a -not -regex "\\./bazel-bin" -a -not -regex "\\./bazel-bin/.*" -a -not -regex "\\./bazel-out" -a -not -regex "\\./bazel-out/.*" -a -not -regex "\\./bazel-testlogs" -a -not -regex "\\./bazel-testlogs/.*" -a -not -regex "\\./bazel-telegram-ios" -a -not -regex "\\./bazel-telegram-ios/.*" -a -not -regex "\\./buildbox" -a -not -regex "\\./buildbox/.*" -a -not -regex "\\./buck-out" -a -not -regex "\\./buck-out/.*" -a -not -regex "\\./\\.buckd" -a -not -regex "\\./\\.buckd/.*" -a -not -regex "\\./build" -a -not -regex "\\./build/.*" -print0 | tar cf "$BUILDBOX_DIR/transient-data/source.tar" --null -T -
PROCESS_ID="$$"
if [ -z "$RUNNING_VM" ]; then
VM_NAME="$VM_BASE_NAME-$(openssl rand -hex 10)-build-telegram-$PROCESS_ID"
else
VM_NAME="$RUNNING_VM"
fi
if [ "$BUILD_MACHINE" == "linux" ]; then
virt-clone --original "$VM_BASE_NAME" --name "$VM_NAME" --auto-clone
virsh start "$VM_NAME"
echo "Getting VM IP"
while [ 1 ]; do
TEST_IP=$(virsh domifaddr "$VM_NAME" 2>/dev/null | egrep -o 'ipv4.*' | sed -e 's/ipv4\s*//g' | sed -e 's|/.*||g')
if [ ! -z "$TEST_IP" ]; then
RESPONSE=$(ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null telegram@"$TEST_IP" -o ServerAliveInterval=60 -t "echo -n 1")
if [ "$RESPONSE" == "1" ]; then
VM_IP="$TEST_IP"
break
fi
fi
sleep 1
done
elif [ "$BUILD_MACHINE" == "macOS" ]; then
if [ -z "$RUNNING_VM" ]; then
prlctl clone "$VM_BASE_NAME" --linked --name "$VM_NAME"
prlctl start "$VM_NAME"
echo "Getting VM IP"
while [ 1 ]; do
TEST_IP=$(prlctl exec "$VM_NAME" "ifconfig | grep inet | grep broadcast | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1 | tr '\n' '\0'" 2>/dev/null || echo "")
if [ ! -z "$TEST_IP" ]; then
RESPONSE=$(ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null telegram@"$TEST_IP" -o ServerAliveInterval=60 -t "echo -n 1")
if [ "$RESPONSE" == "1" ]; then
VM_IP="$TEST_IP"
break
fi
fi
sleep 1
done
fi
echo "VM_IP=$VM_IP"
fi
scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr "$CODESIGNING_SUBPATH" telegram@"$VM_IP":codesigning_data
scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration" telegram@"$VM_IP":telegram-configuration
scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr "$BUILDBOX_DIR/guest-build-telegram.sh" "$BUILDBOX_DIR/transient-data/source.tar" telegram@"$VM_IP":
ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null telegram@"$VM_IP" -o ServerAliveInterval=60 -t "export BUILD_NUMBER=\"$BUILD_NUMBER\"; export BAZEL_HTTP_CACHE_URL=\"$BAZEL_HTTP_CACHE_URL\"; $GUEST_SHELL -l guest-build-telegram.sh $BUILD_CONFIGURATION" || true
OUTPUT_PATH="build/artifacts"
rm -rf "$OUTPUT_PATH"
mkdir -p "$OUTPUT_PATH"
scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr telegram@"$VM_IP":"telegram-ios/build/artifacts/*" "$OUTPUT_PATH/"
if [ -z "$RUNNING_VM" ]; then
if [ "$BUILD_MACHINE" == "linux" ]; then
virsh destroy "$VM_NAME"
virsh undefine "$VM_NAME" --remove-all-storage --nvram
elif [ "$BUILD_MACHINE" == "macOS" ]; then
echo "Deleting VM..."
#prlctl stop "$VM_NAME" --kill
#prlctl delete "$VM_NAME"
fi
fi
if [ ! -f "$OUTPUT_PATH/Telegram.ipa" ]; then
exit 1
fi
'''

View File

@ -101,7 +101,8 @@ python3 build-system/Make/Make.py \
--configurationPath="$HOME/telegram-configuration" \ --configurationPath="$HOME/telegram-configuration" \
--buildNumber="$BUILD_NUMBER" \ --buildNumber="$BUILD_NUMBER" \
--disableParallelSwiftmoduleGeneration \ --disableParallelSwiftmoduleGeneration \
--configuration="$APP_CONFIGURATION" --configuration="$APP_CONFIGURATION" \
--apsEnvironment=production
OUTPUT_PATH="build/artifacts" OUTPUT_PATH="build/artifacts"
rm -rf "$OUTPUT_PATH" rm -rf "$OUTPUT_PATH"

View File

@ -57,7 +57,7 @@ public func messageMediaImageInteractiveFetched(fetchManager: FetchManager, mess
if let range = range { if let range = range {
ranges = RangeSet(range.lowerBound ..< range.upperBound) ranges = RangeSet(range.lowerBound ..< range.upperBound)
} else { } else {
ranges = RangeSet(0 ..< Int64.max) ranges = RangeSet(0 ..< Int64(Int32.max))
} }
return fetchManager.interactivelyFetched(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(resource), ranges: ranges, statsCategory: .image, elevatedPriority: false, userInitiated: userInitiated, priority: priority, storeToDownloadsPeerType: storeToDownloadsPeerType) return fetchManager.interactivelyFetched(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(resource), ranges: ranges, statsCategory: .image, elevatedPriority: false, userInitiated: userInitiated, priority: priority, storeToDownloadsPeerType: storeToDownloadsPeerType)
} }

View File

@ -132,6 +132,10 @@ public final class AppLockContextImpl: AppLockContext {
if !strongSelf.lastActiveValue { if !strongSelf.lastActiveValue {
strongSelf.lastActiveValue = true strongSelf.lastActiveValue = true
strongSelf.lastActiveTimestamp = timestamp strongSelf.lastActiveTimestamp = timestamp
if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: strongSelf.rootPath))), let current = try? JSONDecoder().decode(LockState.self, from: data) {
strongSelf.currentStateValue = current
}
} }
if let lastActiveTimestamp = strongSelf.lastActiveTimestamp { if let lastActiveTimestamp = strongSelf.lastActiveTimestamp {

View File

@ -482,7 +482,7 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
for node in nodes { for node in nodes {
other.addSubnode(node) other.addSubnode(node)
node.layer.animateAlpha(from: node.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak node] _ in node.layer.animateAlpha(from: node.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
node?.removeFromSupernode() node?.removeFromSupernode()
}) })
} }
@ -491,12 +491,11 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
func animateContentIn() { func animateContentIn() {
let nodes: [ASDisplayNode] = [ let nodes: [ASDisplayNode] = [
self.textNode.textNode, self.textNode.textNode,
self.iconNode, self.iconNode
self.placeholderNode
] ]
for node in nodes { for node in nodes {
node.layer.animateAlpha(from: 0.0, to: node.alpha, duration: 0.2) node.layer.animateAlpha(from: 0.0, to: node.alpha, duration: 0.25)
} }
} }

View File

@ -378,6 +378,7 @@ open class NavigationController: UINavigationController, ContainableController,
let belowKeyboardOverlayLayout = layout let belowKeyboardOverlayLayout = layout
var globalOverlayLayout = layout var globalOverlayLayout = layout
globalOverlayLayout.inputHeight = nil
if let globalOverlayBelowKeyboardContainerParent = self.globalOverlayBelowKeyboardContainerParent { if let globalOverlayBelowKeyboardContainerParent = self.globalOverlayBelowKeyboardContainerParent {
if globalOverlayBelowKeyboardContainerParent.view.superview != self.displayNode.view { if globalOverlayBelowKeyboardContainerParent.view.superview != self.displayNode.view {

View File

@ -226,7 +226,7 @@ private final class FetchManagerCategoryContext {
let storeManager = self.storeManager let storeManager = self.storeManager
let parsedRanges: [(Range<Int64>, MediaBoxFetchPriority)]? let parsedRanges: [(Range<Int64>, MediaBoxFetchPriority)]?
if ranges == RangeSet<Int64>(0 ..< Int64.max) { if ranges == RangeSet<Int64>(0 ..< Int64.max), !"".isEmpty {
parsedRanges = nil parsedRanges = nil
} else { } else {
var resultRanges: [(Range<Int64>, MediaBoxFetchPriority)] = [] var resultRanges: [(Range<Int64>, MediaBoxFetchPriority)] = []
@ -325,7 +325,7 @@ private final class FetchManagerCategoryContext {
isVideoPreload = true isVideoPreload = true
} }
if count == 1 && isCompleteRange { if count == 1 && isCompleteRange && !"".isEmpty {
parsedRanges = nil parsedRanges = nil
} else { } else {
var resultRanges: [(Range<Int64>, MediaBoxFetchPriority)] = [] var resultRanges: [(Range<Int64>, MediaBoxFetchPriority)] = []

View File

@ -53,6 +53,9 @@ private func transcribeAudio(path: String, locale: String) -> Signal<Transcripti
let _ = try? FileManager.default.copyItem(atPath: path, toPath: tempFilePath) let _ = try? FileManager.default.copyItem(atPath: path, toPath: tempFilePath)
let request = SFSpeechURLRecognitionRequest(url: URL(fileURLWithPath: tempFilePath)) let request = SFSpeechURLRecognitionRequest(url: URL(fileURLWithPath: tempFilePath))
if #available(iOS 16.0, *) {
request.addsPunctuation = true
}
request.requiresOnDeviceRecognition = speechRecognizer.supportsOnDeviceRecognition request.requiresOnDeviceRecognition = speechRecognizer.supportsOnDeviceRecognition
request.shouldReportPartialResults = true request.shouldReportPartialResults = true

View File

@ -574,6 +574,12 @@ public final class MediaBox {
let disposable = MetaDisposable() let disposable = MetaDisposable()
self.dataQueue.async { self.dataQueue.async {
let paths = self.storePathsForId(resource.id)
if let _ = fileSize(paths.complete) {
subscriber.putCompletion()
return
}
guard let (fileContext, releaseContext) = self.fileContext(for: resource.id) else { guard let (fileContext, releaseContext) = self.fileContext(for: resource.id) else {
subscriber.putCompletion() subscriber.putCompletion()
return return

View File

@ -1253,6 +1253,7 @@ final class MediaBoxFileContext {
func fetched(range: Range<Int64>, priority: MediaBoxFetchPriority, fetch: @escaping (Signal<[(Range<Int64>, MediaBoxFetchPriority)], NoError>) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>, error: @escaping (MediaResourceDataFetchError) -> Void, completed: @escaping () -> Void) -> Disposable { func fetched(range: Range<Int64>, priority: MediaBoxFetchPriority, fetch: @escaping (Signal<[(Range<Int64>, MediaBoxFetchPriority)], NoError>) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError>, error: @escaping (MediaResourceDataFetchError) -> Void, completed: @escaping () -> Void) -> Disposable {
switch self.content { switch self.content {
case .complete: case .complete:
completed()
return EmptyDisposable return EmptyDisposable
case let .partial(file): case let .partial(file):
return file.fetched(range: range, priority: priority, fetch: fetch, error: error, completed: completed) return file.fetched(range: range, priority: priority, fetch: fetch, error: error, completed: completed)

View File

@ -1334,9 +1334,11 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
for attribute in item.file.attributes { for attribute in item.file.attributes {
switch attribute { switch attribute {
case let .CustomEmoji(_, alt, _): case let .CustomEmoji(_, alt, _):
if !alt.isEmpty, let keyword = allEmoticons[alt] { if !item.file.isPremiumEmoji || hasPremium {
if !item.file.isPremiumEmoji || hasPremium { if !alt.isEmpty, let keyword = allEmoticons[alt] {
result.append((alt, item.file, keyword)) result.append((alt, item.file, keyword))
} else if alt == query {
result.append((alt, item.file, alt))
} }
} }
default: default:
@ -1345,12 +1347,6 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
} }
} }
for keyword in keywords {
for emoticon in keyword.emoticons {
result.append((emoticon, nil, keyword.keyword))
}
}
var items: [EmojiPagerContentComponent.Item] = [] var items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>() var existingIds = Set<MediaId>()

View File

@ -730,10 +730,11 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1660637285] = { return Api.StatsGroupTopPoster.parse_statsGroupTopPoster($0) } dict[-1660637285] = { return Api.StatsGroupTopPoster.parse_statsGroupTopPoster($0) }
dict[-875679776] = { return Api.StatsPercentValue.parse_statsPercentValue($0) } dict[-875679776] = { return Api.StatsPercentValue.parse_statsPercentValue($0) }
dict[1202287072] = { return Api.StatsURL.parse_statsURL($0) } dict[1202287072] = { return Api.StatsURL.parse_statsURL($0) }
dict[-50416996] = { return Api.StickerKeyword.parse_stickerKeyword($0) }
dict[313694676] = { return Api.StickerPack.parse_stickerPack($0) } dict[313694676] = { return Api.StickerPack.parse_stickerPack($0) }
dict[768691932] = { return Api.StickerSet.parse_stickerSet($0) } dict[768691932] = { return Api.StickerSet.parse_stickerSet($0) }
dict[1678812626] = { return Api.StickerSetCovered.parse_stickerSetCovered($0) } dict[1678812626] = { return Api.StickerSetCovered.parse_stickerSetCovered($0) }
dict[451763941] = { return Api.StickerSetCovered.parse_stickerSetFullCovered($0) } dict[1087454222] = { return Api.StickerSetCovered.parse_stickerSetFullCovered($0) }
dict[872932635] = { return Api.StickerSetCovered.parse_stickerSetMultiCovered($0) } dict[872932635] = { return Api.StickerSetCovered.parse_stickerSetMultiCovered($0) }
dict[-1609668650] = { return Api.Theme.parse_theme($0) } dict[-1609668650] = { return Api.Theme.parse_theme($0) }
dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($0) } dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($0) }
@ -1029,7 +1030,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1802240206] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedFile($0) } dict[-1802240206] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedFile($0) }
dict[1443858741] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedMessage($0) } dict[1443858741] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedMessage($0) }
dict[1705297877] = { return Api.messages.SponsoredMessages.parse_sponsoredMessages($0) } dict[1705297877] = { return Api.messages.SponsoredMessages.parse_sponsoredMessages($0) }
dict[-1240849242] = { return Api.messages.StickerSet.parse_stickerSet($0) } dict[1846886166] = { return Api.messages.StickerSet.parse_stickerSet($0) }
dict[-738646805] = { return Api.messages.StickerSet.parse_stickerSetNotModified($0) } dict[-738646805] = { return Api.messages.StickerSet.parse_stickerSetNotModified($0) }
dict[904138920] = { return Api.messages.StickerSetInstallResult.parse_stickerSetInstallResultArchive($0) } dict[904138920] = { return Api.messages.StickerSetInstallResult.parse_stickerSetInstallResultArchive($0) }
dict[946083368] = { return Api.messages.StickerSetInstallResult.parse_stickerSetInstallResultSuccess($0) } dict[946083368] = { return Api.messages.StickerSetInstallResult.parse_stickerSetInstallResultSuccess($0) }
@ -1584,6 +1585,8 @@ public extension Api {
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.StatsURL: case let _1 as Api.StatsURL:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.StickerKeyword:
_1.serialize(buffer, boxed)
case let _1 as Api.StickerPack: case let _1 as Api.StickerPack:
_1.serialize(buffer, boxed) _1.serialize(buffer, boxed)
case let _1 as Api.StickerSet: case let _1 as Api.StickerSet:

View File

@ -1,3 +1,49 @@
public extension Api {
enum StickerKeyword: TypeConstructorDescription {
case stickerKeyword(documentId: Int64, keyword: [String])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
case .stickerKeyword(let documentId, let keyword):
if boxed {
buffer.appendInt32(-50416996)
}
serializeInt64(documentId, buffer: buffer, boxed: false)
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(keyword.count))
for item in keyword {
serializeString(item, buffer: buffer, boxed: false)
}
break
}
}
public func descriptionFields() -> (String, [(String, Any)]) {
switch self {
case .stickerKeyword(let documentId, let keyword):
return ("stickerKeyword", [("documentId", String(describing: documentId)), ("keyword", String(describing: keyword))])
}
}
public static func parse_stickerKeyword(_ reader: BufferReader) -> StickerKeyword? {
var _1: Int64?
_1 = reader.readInt64()
var _2: [String]?
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: -1255641564, elementType: String.self)
}
let _c1 = _1 != nil
let _c2 = _2 != nil
if _c1 && _c2 {
return Api.StickerKeyword.stickerKeyword(documentId: _1!, keyword: _2!)
}
else {
return nil
}
}
}
}
public extension Api { public extension Api {
enum StickerPack: TypeConstructorDescription { enum StickerPack: TypeConstructorDescription {
case stickerPack(emoticon: String, documents: [Int64]) case stickerPack(emoticon: String, documents: [Int64])
@ -133,7 +179,7 @@ public extension Api {
public extension Api { public extension Api {
enum StickerSetCovered: TypeConstructorDescription { enum StickerSetCovered: TypeConstructorDescription {
case stickerSetCovered(set: Api.StickerSet, cover: Api.Document) case stickerSetCovered(set: Api.StickerSet, cover: Api.Document)
case stickerSetFullCovered(set: Api.StickerSet, packs: [Api.StickerPack], documents: [Api.Document]) case stickerSetFullCovered(set: Api.StickerSet, packs: [Api.StickerPack], keywords: [Api.StickerKeyword], documents: [Api.Document])
case stickerSetMultiCovered(set: Api.StickerSet, covers: [Api.Document]) case stickerSetMultiCovered(set: Api.StickerSet, covers: [Api.Document])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
@ -145,9 +191,9 @@ public extension Api {
set.serialize(buffer, true) set.serialize(buffer, true)
cover.serialize(buffer, true) cover.serialize(buffer, true)
break break
case .stickerSetFullCovered(let set, let packs, let documents): case .stickerSetFullCovered(let set, let packs, let keywords, let documents):
if boxed { if boxed {
buffer.appendInt32(451763941) buffer.appendInt32(1087454222)
} }
set.serialize(buffer, true) set.serialize(buffer, true)
buffer.appendInt32(481674261) buffer.appendInt32(481674261)
@ -156,6 +202,11 @@ public extension Api {
item.serialize(buffer, true) item.serialize(buffer, true)
} }
buffer.appendInt32(481674261) buffer.appendInt32(481674261)
buffer.appendInt32(Int32(keywords.count))
for item in keywords {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(documents.count)) buffer.appendInt32(Int32(documents.count))
for item in documents { for item in documents {
item.serialize(buffer, true) item.serialize(buffer, true)
@ -179,8 +230,8 @@ public extension Api {
switch self { switch self {
case .stickerSetCovered(let set, let cover): case .stickerSetCovered(let set, let cover):
return ("stickerSetCovered", [("set", String(describing: set)), ("cover", String(describing: cover))]) return ("stickerSetCovered", [("set", String(describing: set)), ("cover", String(describing: cover))])
case .stickerSetFullCovered(let set, let packs, let documents): case .stickerSetFullCovered(let set, let packs, let keywords, let documents):
return ("stickerSetFullCovered", [("set", String(describing: set)), ("packs", String(describing: packs)), ("documents", String(describing: documents))]) return ("stickerSetFullCovered", [("set", String(describing: set)), ("packs", String(describing: packs)), ("keywords", String(describing: keywords)), ("documents", String(describing: documents))])
case .stickerSetMultiCovered(let set, let covers): case .stickerSetMultiCovered(let set, let covers):
return ("stickerSetMultiCovered", [("set", String(describing: set)), ("covers", String(describing: covers))]) return ("stickerSetMultiCovered", [("set", String(describing: set)), ("covers", String(describing: covers))])
} }
@ -213,15 +264,20 @@ public extension Api {
if let _ = reader.readInt32() { if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self)
} }
var _3: [Api.Document]? var _3: [Api.StickerKeyword]?
if let _ = reader.readInt32() { if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerKeyword.self)
}
var _4: [Api.Document]?
if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self)
} }
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
if _c1 && _c2 && _c3 { let _c4 = _4 != nil
return Api.StickerSetCovered.stickerSetFullCovered(set: _1!, packs: _2!, documents: _3!) if _c1 && _c2 && _c3 && _c4 {
return Api.StickerSetCovered.stickerSetFullCovered(set: _1!, packs: _2!, keywords: _3!, documents: _4!)
} }
else { else {
return nil return nil

View File

@ -638,14 +638,14 @@ public extension Api.messages {
} }
public extension Api.messages { public extension Api.messages {
enum StickerSet: TypeConstructorDescription { enum StickerSet: TypeConstructorDescription {
case stickerSet(set: Api.StickerSet, packs: [Api.StickerPack], documents: [Api.Document]) case stickerSet(set: Api.StickerSet, packs: [Api.StickerPack], keywords: [Api.StickerKeyword], documents: [Api.Document])
case stickerSetNotModified case stickerSetNotModified
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
case .stickerSet(let set, let packs, let documents): case .stickerSet(let set, let packs, let keywords, let documents):
if boxed { if boxed {
buffer.appendInt32(-1240849242) buffer.appendInt32(1846886166)
} }
set.serialize(buffer, true) set.serialize(buffer, true)
buffer.appendInt32(481674261) buffer.appendInt32(481674261)
@ -654,6 +654,11 @@ public extension Api.messages {
item.serialize(buffer, true) item.serialize(buffer, true)
} }
buffer.appendInt32(481674261) buffer.appendInt32(481674261)
buffer.appendInt32(Int32(keywords.count))
for item in keywords {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(documents.count)) buffer.appendInt32(Int32(documents.count))
for item in documents { for item in documents {
item.serialize(buffer, true) item.serialize(buffer, true)
@ -670,8 +675,8 @@ public extension Api.messages {
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { switch self {
case .stickerSet(let set, let packs, let documents): case .stickerSet(let set, let packs, let keywords, let documents):
return ("stickerSet", [("set", String(describing: set)), ("packs", String(describing: packs)), ("documents", String(describing: documents))]) return ("stickerSet", [("set", String(describing: set)), ("packs", String(describing: packs)), ("keywords", String(describing: keywords)), ("documents", String(describing: documents))])
case .stickerSetNotModified: case .stickerSetNotModified:
return ("stickerSetNotModified", []) return ("stickerSetNotModified", [])
} }
@ -686,15 +691,20 @@ public extension Api.messages {
if let _ = reader.readInt32() { if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self)
} }
var _3: [Api.Document]? var _3: [Api.StickerKeyword]?
if let _ = reader.readInt32() { if let _ = reader.readInt32() {
_3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerKeyword.self)
}
var _4: [Api.Document]?
if let _ = reader.readInt32() {
_4 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self)
} }
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
if _c1 && _c2 && _c3 { let _c4 = _4 != nil
return Api.messages.StickerSet.stickerSet(set: _1!, packs: _2!, documents: _3!) if _c1 && _c2 && _c3 && _c4 {
return Api.messages.StickerSet.stickerSet(set: _1!, packs: _2!, keywords: _3!, documents: _4!)
} }
else { else {
return nil return nil

View File

@ -104,6 +104,7 @@ swift_library(
"//submodules/Components/UndoPanelComponent:UndoPanelComponent", "//submodules/Components/UndoPanelComponent:UndoPanelComponent",
"//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer", "//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer",
"//submodules/PeerInfoUI/CreateExternalMediaStreamScreen:CreateExternalMediaStreamScreen", "//submodules/PeerInfoUI/CreateExternalMediaStreamScreen:CreateExternalMediaStreamScreen",
"//submodules/PhoneNumberFormat:PhoneNumberFormat",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -41,7 +41,7 @@ public final class CallKitIntegration {
} }
func setup( func setup(
startCall: @escaping (AccountContext, UUID, String, Bool) -> Signal<Bool, NoError>, startCall: @escaping (AccountContext, UUID, EnginePeer.Id?, String, Bool) -> Signal<Bool, NoError>,
answerCall: @escaping (UUID) -> Void, answerCall: @escaping (UUID) -> Void,
endCall: @escaping (UUID) -> Signal<Bool, NoError>, endCall: @escaping (UUID) -> Signal<Bool, NoError>,
setCallMuted: @escaping (UUID, Bool) -> Void, setCallMuted: @escaping (UUID, Bool) -> Void,
@ -56,22 +56,17 @@ public final class CallKitIntegration {
if !CallKitIntegration.isAvailable { if !CallKitIntegration.isAvailable {
return nil return nil
} }
#if targetEnvironment(simulator)
return nil
#else
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
} else { } else {
return nil return nil
} }
#endif
} }
func startCall(context: AccountContext, peerId: PeerId, isVideo: Bool, displayTitle: String) { func startCall(context: AccountContext, peerId: PeerId, phoneNumber: String?, localContactId: String?, isVideo: Bool, displayTitle: String) {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
(sharedProviderDelegate as? CallKitProviderDelegate)?.startCall(context: context, peerId: peerId, isVideo: isVideo, displayTitle: displayTitle) (sharedProviderDelegate as? CallKitProviderDelegate)?.startCall(context: context, peerId: peerId, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle)
self.donateIntent(peerId: peerId, displayTitle: displayTitle) self.donateIntent(peerId: peerId, displayTitle: displayTitle, localContactId: localContactId)
} }
} }
@ -87,9 +82,9 @@ public final class CallKitIntegration {
} }
} }
public func reportIncomingCall(uuid: UUID, stableId: Int64, handle: String, isVideo: Bool, displayTitle: String, completion: ((NSError?) -> Void)?) { public func reportIncomingCall(uuid: UUID, stableId: Int64, handle: String, phoneNumber: String?, isVideo: Bool, displayTitle: String, completion: ((NSError?) -> Void)?) {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
(sharedProviderDelegate as? CallKitProviderDelegate)?.reportIncomingCall(uuid: uuid, stableId: stableId, handle: handle, isVideo: isVideo, displayTitle: displayTitle, completion: completion) (sharedProviderDelegate as? CallKitProviderDelegate)?.reportIncomingCall(uuid: uuid, stableId: stableId, handle: handle, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle, completion: completion)
} }
} }
@ -99,10 +94,10 @@ public final class CallKitIntegration {
} }
} }
private func donateIntent(peerId: PeerId, displayTitle: String) { private func donateIntent(peerId: PeerId, displayTitle: String, localContactId: String?) {
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
let handle = INPersonHandle(value: "tg\(peerId.id)", type: .unknown) let handle = INPersonHandle(value: "tg\(peerId.id._internalGetInt64Value())", type: .unknown)
let contact = INPerson(personHandle: handle, nameComponents: nil, displayName: displayTitle, image: nil, contactIdentifier: nil, customIdentifier: "tg\(peerId.id)") let contact = INPerson(personHandle: handle, nameComponents: nil, displayName: displayTitle, image: nil, contactIdentifier: localContactId, customIdentifier: "tg\(peerId.id._internalGetInt64Value())")
let intent = INStartAudioCallIntent(contacts: [contact]) let intent = INStartAudioCallIntent(contacts: [contact])
@ -122,8 +117,9 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
private var currentStartCallAccount: (UUID, AccountContext)? private var currentStartCallAccount: (UUID, AccountContext)?
private var alreadyReportedIncomingCalls = Set<UUID>() private var alreadyReportedIncomingCalls = Set<UUID>()
private var uuidToPeerIdMapping: [UUID: EnginePeer.Id] = [:]
private var startCall: ((AccountContext, UUID, String, Bool) -> Signal<Bool, NoError>)? private var startCall: ((AccountContext, UUID, EnginePeer.Id?, String, Bool) -> Signal<Bool, NoError>)?
private var answerCall: ((UUID) -> Void)? private var answerCall: ((UUID) -> Void)?
private var endCall: ((UUID) -> Signal<Bool, NoError>)? private var endCall: ((UUID) -> Signal<Bool, NoError>)?
private var setCallMuted: ((UUID, Bool) -> Void)? private var setCallMuted: ((UUID, Bool) -> Void)?
@ -141,7 +137,7 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
self.provider.setDelegate(self, queue: nil) self.provider.setDelegate(self, queue: nil)
} }
func setup(audioSessionActivePromise: ValuePromise<Bool>, startCall: @escaping (AccountContext, UUID, String, Bool) -> Signal<Bool, NoError>, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal<Bool, NoError>, setCallMuted: @escaping (UUID, Bool) -> Void, audioSessionActivationChanged: @escaping (Bool) -> Void) { func setup(audioSessionActivePromise: ValuePromise<Bool>, startCall: @escaping (AccountContext, UUID, EnginePeer.Id?, String, Bool) -> Signal<Bool, NoError>, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal<Bool, NoError>, setCallMuted: @escaping (UUID, Bool) -> Void, audioSessionActivationChanged: @escaping (Bool) -> Void) {
self.audioSessionActivePromise = audioSessionActivePromise self.audioSessionActivePromise = audioSessionActivePromise
self.startCall = startCall self.startCall = startCall
self.answerCall = answerCall self.answerCall = answerCall
@ -189,10 +185,18 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
self.requestTransaction(transaction) self.requestTransaction(transaction)
} }
func startCall(context: AccountContext, peerId: PeerId, isVideo: Bool, displayTitle: String) { func startCall(context: AccountContext, peerId: PeerId, phoneNumber: String?, isVideo: Bool, displayTitle: String) {
let uuid = UUID() let uuid = UUID()
self.currentStartCallAccount = (uuid, context) self.currentStartCallAccount = (uuid, context)
let handle = CXHandle(type: .generic, value: "\(peerId.id._internalGetInt64Value())") let handle: CXHandle
if let phoneNumber = phoneNumber {
handle = CXHandle(type: .phoneNumber, value: phoneNumber)
} else {
handle = CXHandle(type: .generic, value: "\(peerId.id._internalGetInt64Value())")
}
self.uuidToPeerIdMapping[uuid] = peerId
let startCallAction = CXStartCallAction(call: uuid, handle: handle) let startCallAction = CXStartCallAction(call: uuid, handle: handle)
startCallAction.contactIdentifier = displayTitle startCallAction.contactIdentifier = displayTitle
@ -212,7 +216,7 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
}) })
} }
func reportIncomingCall(uuid: UUID, stableId: Int64, handle: String, isVideo: Bool, displayTitle: String, completion: ((NSError?) -> Void)?) { func reportIncomingCall(uuid: UUID, stableId: Int64, handle: String, phoneNumber: String?, isVideo: Bool, displayTitle: String, completion: ((NSError?) -> Void)?) {
if self.alreadyReportedIncomingCalls.contains(uuid) { if self.alreadyReportedIncomingCalls.contains(uuid) {
completion?(nil) completion?(nil)
return return
@ -220,7 +224,13 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
self.alreadyReportedIncomingCalls.insert(uuid) self.alreadyReportedIncomingCalls.insert(uuid)
let update = CXCallUpdate() let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: handle) let nativeHandle: CXHandle
if let phoneNumber = phoneNumber {
nativeHandle = CXHandle(type: .phoneNumber, value: phoneNumber)
} else {
nativeHandle = CXHandle(type: .generic, value: handle)
}
update.remoteHandle = nativeHandle
update.localizedCallerName = displayTitle update.localizedCallerName = displayTitle
update.supportsHolding = false update.supportsHolding = false
update.supportsGrouping = false update.supportsGrouping = false
@ -252,7 +262,10 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
self.currentStartCallAccount = nil self.currentStartCallAccount = nil
let disposable = MetaDisposable() let disposable = MetaDisposable()
self.disposableSet.add(disposable) self.disposableSet.add(disposable)
disposable.set((startCall(context, action.callUUID, action.handle.value, action.isVideo)
let peerId = self.uuidToPeerIdMapping[action.callUUID]
disposable.set((startCall(context, action.callUUID, peerId, action.handle.value, action.isVideo)
|> deliverOnMainQueue |> deliverOnMainQueue
|> afterDisposed { [weak self, weak disposable] in |> afterDisposed { [weak self, weak disposable] in
if let strongSelf = self, let disposable = disposable { if let strongSelf = self, let disposable = disposable {

View File

@ -13,6 +13,7 @@ import DeviceAccess
import UniversalMediaPlayer import UniversalMediaPlayer
import AccountContext import AccountContext
import DeviceProximity import DeviceProximity
import PhoneNumberFormat
final class PresentationCallToneRenderer { final class PresentationCallToneRenderer {
let queue: Queue let queue: Queue
@ -607,10 +608,15 @@ public final class PresentationCallImpl: PresentationCall {
if previous == nil || previousControl == nil { if previous == nil || previousControl == nil {
if !self.reportedIncomingCall, let stableId = sessionState.stableId { if !self.reportedIncomingCall, let stableId = sessionState.stableId {
self.reportedIncomingCall = true self.reportedIncomingCall = true
var phoneNumber: String?
if let peer = self.peer as? TelegramUser, let phone = peer.phone {
phoneNumber = formatPhoneNumber(phone)
}
self.callKitIntegration?.reportIncomingCall( self.callKitIntegration?.reportIncomingCall(
uuid: self.internalId, uuid: self.internalId,
stableId: stableId, stableId: stableId,
handle: "\(self.peerId.id._internalGetInt64Value())", handle: "\(self.peerId.id._internalGetInt64Value())",
phoneNumber: phoneNumber,
isVideo: sessionState.type == .video, isVideo: sessionState.type == .video,
displayTitle: self.peer?.debugDisplayTitle ?? "Unknown", displayTitle: self.peer?.debugDisplayTitle ?? "Unknown",
completion: { [weak self] error in completion: { [weak self] error in

View File

@ -11,6 +11,7 @@ import TelegramVoip
import TelegramUIPreferences import TelegramUIPreferences
import AccountContext import AccountContext
import CallKit import CallKit
import PhoneNumberFormat
private func callKitIntegrationIfEnabled(_ integration: CallKitIntegration?, settings: VoiceCallSettings?) -> CallKitIntegration? { private func callKitIntegrationIfEnabled(_ integration: CallKitIntegration?, settings: VoiceCallSettings?) -> CallKitIntegration? {
let enabled = settings?.enableSystemIntegration ?? true let enabled = settings?.enableSystemIntegration ?? true
@ -128,16 +129,16 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
self.isMediaPlaying = isMediaPlaying self.isMediaPlaying = isMediaPlaying
self.resumeMediaPlayback = resumeMediaPlayback self.resumeMediaPlayback = resumeMediaPlayback
var startCallImpl: ((AccountContext, UUID, String, Bool) -> Signal<Bool, NoError>)? var startCallImpl: ((AccountContext, UUID, EnginePeer.Id?, String, Bool) -> Signal<Bool, NoError>)?
var answerCallImpl: ((UUID) -> Void)? var answerCallImpl: ((UUID) -> Void)?
var endCallImpl: ((UUID) -> Signal<Bool, NoError>)? var endCallImpl: ((UUID) -> Signal<Bool, NoError>)?
var setCallMutedImpl: ((UUID, Bool) -> Void)? var setCallMutedImpl: ((UUID, Bool) -> Void)?
var audioSessionActivationChangedImpl: ((Bool) -> Void)? var audioSessionActivationChangedImpl: ((Bool) -> Void)?
self.callKitIntegration = CallKitIntegration.shared self.callKitIntegration = CallKitIntegration.shared
self.callKitIntegration?.setup(startCall: { context, uuid, handle, isVideo in self.callKitIntegration?.setup(startCall: { context, uuid, maybePeerId, handle, isVideo in
if let startCallImpl = startCallImpl { if let startCallImpl = startCallImpl {
return startCallImpl(context, uuid, handle, isVideo) return startCallImpl(context, uuid, maybePeerId, handle, isVideo)
} else { } else {
return .single(false) return .single(false)
} }
@ -215,16 +216,26 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
self?.ringingStatesUpdated(ringingStates, enableCallKit: enableCallKit) self?.ringingStatesUpdated(ringingStates, enableCallKit: enableCallKit)
}) })
startCallImpl = { [weak self] context, uuid, handle, isVideo in startCallImpl = { [weak self] context, uuid, maybePeerId, handle, isVideo in
if let strongSelf = self, let userId = Int64(handle) { guard let strongSelf = self else {
return strongSelf.startCall(context: context, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), isVideo: isVideo, internalId: uuid)
|> take(1)
|> map { result -> Bool in
return result
}
} else {
return .single(false) return .single(false)
} }
var peerId: PeerId?
if let maybePeerId = maybePeerId {
peerId = maybePeerId
} else if let userId = Int64(handle) {
peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
}
guard let peerId = peerId else {
return .single(false)
}
return strongSelf.startCall(context: context, peerId: peerId, isVideo: isVideo, internalId: uuid)
|> take(1)
|> map { result -> Bool in
return result
}
} }
answerCallImpl = { [weak self] uuid in answerCallImpl = { [weak self] uuid in
@ -398,19 +409,39 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|> runOn(Queue.mainQueue()) |> runOn(Queue.mainQueue())
let postbox = context.account.postbox let postbox = context.account.postbox
strongSelf.startCallDisposable.set((accessEnabledSignal strongSelf.startCallDisposable.set((accessEnabledSignal
|> mapToSignal { accessEnabled -> Signal<Peer?, NoError> in |> mapToSignal { accessEnabled -> Signal<(Peer?, String?), NoError> in
if !accessEnabled { if !accessEnabled {
return .single(nil) return .single((nil, nil))
}
return postbox.transaction { transaction -> (Peer?, String?) in
var foundLocalId: String?
transaction.enumerateDeviceContactImportInfoItems({ _, value in
if let value = value as? TelegramDeviceContactImportedData {
switch value {
case let .imported(data, _, importedPeerId):
if importedPeerId == peerId {
foundLocalId = data.localIdentifiers.first
return false
}
default:
break
}
}
return true
})
return (transaction.getPeer(peerId), foundLocalId)
} }
return postbox.loadedPeerWithId(peerId)
|> take(1)
|> map(Optional.init)
} }
|> deliverOnMainQueue).start(next: { peer in |> deliverOnMainQueue).start(next: { peer, localContactId in
guard let strongSelf = self, let peer = peer else { guard let strongSelf = self, let peer = peer else {
return return
} }
strongSelf.callKitIntegration?.startCall(context: context, peerId: peerId, isVideo: isVideo, displayTitle: peer.debugDisplayTitle) var phoneNumber: String?
if let peer = peer as? TelegramUser, let phone = peer.phone {
phoneNumber = formatPhoneNumber(phone)
}
strongSelf.callKitIntegration?.startCall(context: context, peerId: peerId, phoneNumber: phoneNumber, localContactId: localContactId, isVideo: isVideo, displayTitle: peer.debugDisplayTitle)
})) }))
} }
if let currentCall = self.currentCall { if let currentCall = self.currentCall {

View File

@ -715,6 +715,7 @@ private final class MultipartFetchManager {
} }
let part = self.source.request(offset: downloadRange.lowerBound, limit: downloadRange.upperBound - downloadRange.lowerBound, tag: self.parameters?.tag, resource: self.resource, resourceReference: self.resourceReference, fileReference: self.fileReference, continueInBackground: self.continueInBackground) let part = self.source.request(offset: downloadRange.lowerBound, limit: downloadRange.upperBound - downloadRange.lowerBound, tag: self.parameters?.tag, resource: self.resource, resourceReference: self.resourceReference, fileReference: self.fileReference, continueInBackground: self.continueInBackground)
//|> delay(5.0, queue: self.queue)
|> deliverOn(self.queue) |> deliverOn(self.queue)
let partDisposable = MetaDisposable() let partDisposable = MetaDisposable()
self.fetchingParts[downloadRange.lowerBound] = (Int64(downloadRange.count), partDisposable) self.fetchingParts[downloadRange.lowerBound] = (Int64(downloadRange.count), partDisposable)

View File

@ -3604,7 +3604,7 @@ func replayFinalState(
let namespace: ItemCollectionId.Namespace let namespace: ItemCollectionId.Namespace
var items: [ItemCollectionItem] = [] var items: [ItemCollectionItem] = []
let info: StickerPackCollectionInfo let info: StickerPackCollectionInfo
if case let .stickerSet(set, packs, documents) = apiSet { if case let .stickerSet(set, packs, keywords, documents) = apiSet {
var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:] var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:]
for pack in packs { for pack in packs {
switch pack { switch pack {
@ -3621,6 +3621,20 @@ func replayFinalState(
break break
} }
} }
for keyword in keywords {
switch keyword {
case let .stickerKeyword(documentId, texts):
for text in texts {
let key = ValueBoxKey(text).toMemoryBuffer()
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: documentId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
}
}
for apiDocument in documents { for apiDocument in documents {
if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id {

View File

@ -1193,7 +1193,7 @@ public final class AccountViewTracker {
return account.postbox.transaction { transaction -> Void in return account.postbox.transaction { transaction -> Void in
for result in results { for result in results {
switch result { switch result {
case let .stickerSet(_, _, documents)?: case let .stickerSet(_, _, _, documents)?:
for document in documents { for document in documents {
if let file = telegramMediaFileFromApiDocument(document) { if let file = telegramMediaFileFromApiDocument(document) {
if transaction.getMedia(file.fileId) != nil { if transaction.getMedia(file.fileId) != nil {

View File

@ -249,7 +249,7 @@ private func pushDeviceContacts(postbox: Postbox, network: Network, importableCo
if let updatedData = importableContacts[number] { if let updatedData = importableContacts[number] {
if let value = value as? TelegramDeviceContactImportedData { if let value = value as? TelegramDeviceContactImportedData {
switch value { switch value {
case let .imported(data, _): case let .imported(data, _, _):
if data != updatedData { if data != updatedData {
updatedDataIdentifiers.insert(identifier) updatedDataIdentifiers.insert(identifier)
} }
@ -289,7 +289,10 @@ private func pushDeviceContacts(postbox: Postbox, network: Network, importableCo
outer: for i in (0 ..< orderedPushIdentifiers.count).reversed() { outer: for i in (0 ..< orderedPushIdentifiers.count).reversed() {
if let user = currentContactDetails[orderedPushIdentifiers[i]], case let .phoneNumber(number) = orderedPushIdentifiers[i], let data = importableContacts[number] { if let user = currentContactDetails[orderedPushIdentifiers[i]], case let .phoneNumber(number) = orderedPushIdentifiers[i], let data = importableContacts[number] {
if (user.firstName ?? "") == data.firstName && (user.lastName ?? "") == data.lastName { if (user.firstName ?? "") == data.firstName && (user.lastName ?? "") == data.lastName {
transaction.setDeviceContactImportInfo(orderedPushIdentifiers[i].key, value: TelegramDeviceContactImportedData.imported(data: data, importedByCount: 0)) if data.localIdentifiers.contains("5DFF1D6F-8C0A-48C9-800D-F4BEC59C0E50") {
assert(true)
}
transaction.setDeviceContactImportInfo(orderedPushIdentifiers[i].key, value: TelegramDeviceContactImportedData.imported(data: data, importedByCount: 0, peerId: user.id))
orderedPushIdentifiers.remove(at: i) orderedPushIdentifiers.remove(at: i)
continue outer continue outer
} }
@ -334,6 +337,7 @@ private func pushDeviceContactData(postbox: Postbox, network: Network, contacts:
var addedContactPeerIds = Set<PeerId>() var addedContactPeerIds = Set<PeerId>()
var retryIndices = Set<Int>() var retryIndices = Set<Int>()
var importedCounts: [Int: Int32] = [:] var importedCounts: [Int: Int32] = [:]
var peerIdByClientId: [Int64: PeerId] = [:]
switch result { switch result {
case let .importedContacts(imported, popularInvites, retryContacts, users): case let .importedContacts(imported, popularInvites, retryContacts, users):
let peers = users.map { TelegramUser(user: $0) as Peer } let peers = users.map { TelegramUser(user: $0) as Peer }
@ -342,8 +346,10 @@ private func pushDeviceContactData(postbox: Postbox, network: Network, contacts:
}) })
for item in imported { for item in imported {
switch item { switch item {
case let .importedContact(userId, _): case let .importedContact(userId, clientId):
addedContactPeerIds.insert(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))) let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
addedContactPeerIds.insert(peerId)
peerIdByClientId[clientId] = peerId
} }
} }
for item in retryContacts { for item in retryContacts {
@ -363,7 +369,10 @@ private func pushDeviceContactData(postbox: Postbox, network: Network, contacts:
importedData = .retryLater importedData = .retryLater
addedReimportAttempts[.phoneNumber(batch[i].0)] = timestamp addedReimportAttempts[.phoneNumber(batch[i].0)] = timestamp
} else { } else {
importedData = .imported(data: batch[i].1, importedByCount: importedCounts[i] ?? 0) if batch[i].1.localIdentifiers.contains("5DFF1D6F-8C0A-48C9-800D-F4BEC59C0E50") {
assert(true)
}
importedData = .imported(data: batch[i].1, importedByCount: importedCounts[i] ?? 0, peerId: peerIdByClientId[Int64(i)])
} }
transaction.setDeviceContactImportInfo(TelegramDeviceContactImportIdentifier.phoneNumber(batch[i].0).key, value: importedData) transaction.setDeviceContactImportInfo(TelegramDeviceContactImportIdentifier.phoneNumber(batch[i].0).key, value: importedData)
} }

View File

@ -147,7 +147,7 @@ private func fetchStickerPack(network: Network, info: StickerPackCollectionInfo)
switch result { switch result {
case .stickerSetNotModified: case .stickerSetNotModified:
break break
case let .stickerSet(stickerSet, packs, documents): case let .stickerSet(stickerSet, packs, keywords, documents):
updatedInfo = StickerPackCollectionInfo(apiSet: stickerSet, namespace: info.id.namespace) updatedInfo = StickerPackCollectionInfo(apiSet: stickerSet, namespace: info.id.namespace)
var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:] var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:]
for pack in packs { for pack in packs {
@ -165,6 +165,20 @@ private func fetchStickerPack(network: Network, info: StickerPackCollectionInfo)
break break
} }
} }
for keyword in keywords {
switch keyword {
case let .stickerKeyword(documentId, texts):
for text in texts {
let key = ValueBoxKey(text).toMemoryBuffer()
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: documentId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
}
}
for apiDocument in documents { for apiDocument in documents {
if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id {
@ -224,7 +238,7 @@ private func installRemoteStickerPacks(network: Network, infos: [StickerPackColl
archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
case let .stickerSetMultiCovered(set, _): case let .stickerSetMultiCovered(set, _):
archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
case let .stickerSetFullCovered(set, _, _): case let .stickerSetFullCovered(set, _, _, _):
archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
} }
} }

View File

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization { public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt { public func currentLayer() -> UInt {
return 146 return 147
} }
public func parseMessage(_ data: Data!) -> Any! { public func parseMessage(_ data: Data!) -> Any! {

View File

@ -227,7 +227,7 @@ func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollect
} }
} }
return (info, items) return (info, items)
case let .stickerSetFullCovered(set, packs, documents): case let .stickerSetFullCovered(set, packs, keywords, documents):
var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:] var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:]
for pack in packs { for pack in packs {
switch pack { switch pack {
@ -244,6 +244,20 @@ func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollect
break break
} }
} }
for keyword in keywords {
switch keyword {
case let .stickerKeyword(documentId, texts):
for text in texts {
let key = ValueBoxKey(text).toMemoryBuffer()
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: documentId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
}
}
let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace)
var items: [StickerPackItem] = [] var items: [StickerPackItem] = []

View File

@ -74,7 +74,7 @@ public func addSavedSticker(postbox: Postbox, network: Network, file: TelegramMe
switch result { switch result {
case .stickerSetNotModified: case .stickerSetNotModified:
break break
case let .stickerSet(_, packs, _): case let .stickerSet(_, packs, _, _):
var stringRepresentationsByFile: [MediaId: [String]] = [:] var stringRepresentationsByFile: [MediaId: [String]] = [:]
for pack in packs { for pack in packs {
switch pack { switch pack {

View File

@ -3,20 +3,24 @@ import Postbox
public final class ImportableDeviceContactData: Equatable, PostboxCoding { public final class ImportableDeviceContactData: Equatable, PostboxCoding {
public let firstName: String public let firstName: String
public let lastName: String public let lastName: String
public let localIdentifiers: [String]
public init(firstName: String, lastName: String) { public init(firstName: String, lastName: String, localIdentifiers: [String]) {
self.firstName = firstName self.firstName = firstName
self.lastName = lastName self.lastName = lastName
self.localIdentifiers = localIdentifiers
} }
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {
self.firstName = decoder.decodeStringForKey("f", orElse: "") self.firstName = decoder.decodeStringForKey("f", orElse: "")
self.lastName = decoder.decodeStringForKey("l", orElse: "") self.lastName = decoder.decodeStringForKey("l", orElse: "")
self.localIdentifiers = decoder.decodeStringArrayForKey("dis")
} }
public func encode(_ encoder: PostboxEncoder) { public func encode(_ encoder: PostboxEncoder) {
encoder.encodeString(self.firstName, forKey: "f") encoder.encodeString(self.firstName, forKey: "f")
encoder.encodeString(self.lastName, forKey: "l") encoder.encodeString(self.lastName, forKey: "l")
encoder.encodeStringArray(self.localIdentifiers, forKey: "dis")
} }
public static func ==(lhs: ImportableDeviceContactData, rhs: ImportableDeviceContactData) -> Bool { public static func ==(lhs: ImportableDeviceContactData, rhs: ImportableDeviceContactData) -> Bool {
@ -26,6 +30,9 @@ public final class ImportableDeviceContactData: Equatable, PostboxCoding {
if lhs.lastName != rhs.lastName { if lhs.lastName != rhs.lastName {
return false return false
} }
if lhs.localIdentifiers != rhs.localIdentifiers {
return false
}
return true return true
} }
} }

View File

@ -1,13 +1,13 @@
import Postbox import Postbox
public enum TelegramDeviceContactImportedData: PostboxCoding { public enum TelegramDeviceContactImportedData: PostboxCoding {
case imported(data: ImportableDeviceContactData, importedByCount: Int32) case imported(data: ImportableDeviceContactData, importedByCount: Int32, peerId: PeerId?)
case retryLater case retryLater
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {
switch decoder.decodeInt32ForKey("_t", orElse: 0) { switch decoder.decodeInt32ForKey("_t", orElse: 0) {
case 0: case 0:
self = .imported(data: decoder.decodeObjectForKey("d", decoder: { ImportableDeviceContactData(decoder: $0) }) as! ImportableDeviceContactData, importedByCount: decoder.decodeInt32ForKey("c", orElse: 0)) self = .imported(data: decoder.decodeObjectForKey("d", decoder: { ImportableDeviceContactData(decoder: $0) }) as! ImportableDeviceContactData, importedByCount: decoder.decodeInt32ForKey("c", orElse: 0), peerId: decoder.decodeOptionalInt64ForKey("pid").flatMap(PeerId.init))
case 1: case 1:
self = .retryLater self = .retryLater
default: default:
@ -18,10 +18,15 @@ public enum TelegramDeviceContactImportedData: PostboxCoding {
public func encode(_ encoder: PostboxEncoder) { public func encode(_ encoder: PostboxEncoder) {
switch self { switch self {
case let .imported(data, importedByCount): case let .imported(data, importedByCount, peerId):
encoder.encodeInt32(0, forKey: "_t") encoder.encodeInt32(0, forKey: "_t")
encoder.encodeObject(data, forKey: "d") encoder.encodeObject(data, forKey: "d")
encoder.encodeInt32(importedByCount, forKey: "c") encoder.encodeInt32(importedByCount, forKey: "c")
if let peerId = peerId {
encoder.encodeInt64(peerId.toInt64(), forKey: "pid")
} else {
encoder.encodeNil(forKey: "pid")
}
case .retryLater: case .retryLater:
encoder.encodeInt32(1, forKey: "_t") encoder.encodeInt32(1, forKey: "_t")
} }

View File

@ -53,7 +53,7 @@ func _internal_deviceContactsImportedByCount(postbox: Postbox, contacts: [(Strin
for (id, numbers) in contacts { for (id, numbers) in contacts {
var maxCount: Int32 = 0 var maxCount: Int32 = 0
for number in numbers { for number in numbers {
if let value = transaction.getDeviceContactImportInfo(TelegramDeviceContactImportIdentifier.phoneNumber(number).key) as? TelegramDeviceContactImportedData, case let .imported(_, importedByCount) = value { if let value = transaction.getDeviceContactImportInfo(TelegramDeviceContactImportIdentifier.phoneNumber(number).key) as? TelegramDeviceContactImportedData, case let .imported(_, importedByCount, _) = value {
maxCount = max(maxCount, importedByCount) maxCount = max(maxCount, importedByCount)
} }
} }

View File

@ -69,5 +69,32 @@ public extension TelegramEngine {
} }
|> ignoreValues |> ignoreValues
} }
public func findPeerByLocalContactIdentifier(identifier: String) -> Signal<EnginePeer?, NoError> {
return self.account.postbox.transaction { transaction -> EnginePeer? in
var foundPeerId: PeerId?
transaction.enumerateDeviceContactImportInfoItems({ _, value in
if let value = value as? TelegramDeviceContactImportedData {
switch value {
case let .imported(data, _, peerId):
if data.localIdentifiers.contains(identifier) {
if let peerId = peerId {
foundPeerId = peerId
return false
}
}
default:
break
}
}
return true
})
if let foundPeerId = foundPeerId {
return transaction.getPeer(foundPeerId).flatMap(EnginePeer.init)
} else {
return nil
}
}
}
} }
} }

View File

@ -167,7 +167,7 @@ func _internal_createStickerSet(account: Account, title: String, shortName: Stri
switch result { switch result {
case .stickerSetNotModified: case .stickerSetNotModified:
return .complete() return .complete()
case let .stickerSet(set, packs, documents): case let .stickerSet(set, packs, keywords, documents):
let namespace: ItemCollectionId.Namespace let namespace: ItemCollectionId.Namespace
switch set { switch set {
case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _, _): case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _, _):
@ -195,6 +195,20 @@ func _internal_createStickerSet(account: Account, title: String, shortName: Stri
} }
} }
} }
for keyword in keywords {
switch keyword {
case let .stickerKeyword(documentId, texts):
for text in texts {
let key = ValueBoxKey(text).toMemoryBuffer()
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: documentId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
}
}
for apiDocument in documents { for apiDocument in documents {
if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id {

View File

@ -52,7 +52,7 @@ func updatedRemoteStickerPack(postbox: Postbox, network: Network, reference: Sti
switch result { switch result {
case .stickerSetNotModified: case .stickerSetNotModified:
return .complete() return .complete()
case let .stickerSet(set, packs, documents): case let .stickerSet(set, packs, keywords, documents):
let namespace: ItemCollectionId.Namespace let namespace: ItemCollectionId.Namespace
switch set { switch set {
case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _, _): case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _, _):
@ -80,6 +80,20 @@ func updatedRemoteStickerPack(postbox: Postbox, network: Network, reference: Sti
} }
} }
} }
for keyword in keywords {
switch keyword {
case let .stickerKeyword(documentId, texts):
for text in texts {
let key = ValueBoxKey(text).toMemoryBuffer()
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: documentId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
}
}
for apiDocument in documents { for apiDocument in documents {
if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id {

View File

@ -103,7 +103,7 @@ func _internal_stickerPacksAttachedToMedia(account: Account, media: AnyMediaRefe
|> map { result -> [StickerPackReference] in |> map { result -> [StickerPackReference] in
return result.map { pack in return result.map { pack in
switch pack { switch pack {
case let .stickerSetCovered(set, _), let .stickerSetMultiCovered(set, _), let .stickerSetFullCovered(set, _, _): case let .stickerSetCovered(set, _), let .stickerSetMultiCovered(set, _), let .stickerSetFullCovered(set, _, _, _):
let info = StickerPackCollectionInfo(apiSet: set, namespace: Namespaces.ItemCollection.CloudStickerPacks) let info = StickerPackCollectionInfo(apiSet: set, namespace: Namespaces.ItemCollection.CloudStickerPacks)
return .id(id: info.id.id, accessHash: info.accessHash) return .id(id: info.id.id, accessHash: info.accessHash)
} }

View File

@ -71,7 +71,7 @@ func _internal_requestStickerSet(postbox: Postbox, network: Network, reference:
switch result { switch result {
case .stickerSetNotModified: case .stickerSetNotModified:
return .complete() return .complete()
case let .stickerSet(set, packs, documents): case let .stickerSet(set, packs, keywords, documents):
info = StickerPackCollectionInfo(apiSet: set, namespace: Namespaces.ItemCollection.CloudStickerPacks) info = StickerPackCollectionInfo(apiSet: set, namespace: Namespaces.ItemCollection.CloudStickerPacks)
switch set { switch set {
@ -95,6 +95,20 @@ func _internal_requestStickerSet(postbox: Postbox, network: Network, reference:
break break
} }
} }
for keyword in keywords {
switch keyword {
case let .stickerKeyword(documentId, texts):
for text in texts {
let key = ValueBoxKey(text).toMemoryBuffer()
let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: documentId)
if indexKeysByFile[mediaId] == nil {
indexKeysByFile[mediaId] = [key]
} else {
indexKeysByFile[mediaId]!.append(key)
}
}
}
}
for apiDocument in documents { for apiDocument in documents {
if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id {
@ -167,7 +181,7 @@ func _internal_installStickerSetInteractively(account: Account, info: StickerPac
case let .stickerSetMultiCovered(set: set, covers: covers): case let .stickerSetMultiCovered(set: set, covers: covers):
apiSet = set apiSet = set
apiDocuments = covers apiDocuments = covers
case let .stickerSetFullCovered(set, _, documents): case let .stickerSetFullCovered(set, _, _, documents):
apiSet = set apiSet = set
apiDocuments = documents apiDocuments = documents
} }

View File

@ -34,6 +34,7 @@ import DebugSettingsUI
import BackgroundTasks import BackgroundTasks
import UIKitRuntimeUtils import UIKitRuntimeUtils
import StoreKit import StoreKit
import PhoneNumberFormat
#if canImport(AppCenter) #if canImport(AppCenter)
import AppCenter import AppCenter
@ -1557,11 +1558,14 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
completion() completion()
return return
} }
let phoneNumber = payloadJson["phoneNumber"] as? String
callKitIntegration.reportIncomingCall( callKitIntegration.reportIncomingCall(
uuid: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId), uuid: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId),
stableId: callUpdate.callId, stableId: callUpdate.callId,
handle: "\(callUpdate.peer.id.id._internalGetInt64Value())", handle: "\(callUpdate.peer.id.id._internalGetInt64Value())",
phoneNumber: phoneNumber.flatMap(formatPhoneNumber),
isVideo: false, isVideo: false,
displayTitle: callUpdate.peer.debugDisplayTitle, displayTitle: callUpdate.peer.debugDisplayTitle,
completion: { error in completion: { error in
@ -1745,52 +1749,65 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
} }
if let contact = startCallContacts.first { if let contact = startCallContacts.first {
var processed = false let contactByIdentifier: Signal<EnginePeer?, NoError>
if let handle = contact.customIdentifier, handle.hasPrefix("tg") { if let context = self.contextValue?.context, let contactIdentifier = contact.contactIdentifier {
let string = handle.suffix(from: handle.index(handle.startIndex, offsetBy: 2)) contactByIdentifier = context.engine.contacts.findPeerByLocalContactIdentifier(identifier: contactIdentifier)
if let userId = Int64(string) { } else {
startCall(userId) contactByIdentifier = .single(nil)
processed = true
}
} }
if !processed, let handle = contact.personHandle, let value = handle.value {
switch handle.type { let _ = (contactByIdentifier |> deliverOnMainQueue).start(next: { peerByContact in
case .unknown: var processed = false
if let userId = Int64(value) { if let peerByContact = peerByContact {
startCall(userId) startCall(peerByContact.id.id._internalGetInt64Value())
processed = true processed = true
} } else if let handle = contact.customIdentifier, handle.hasPrefix("tg") {
case .phoneNumber: let string = handle.suffix(from: handle.index(handle.startIndex, offsetBy: 2))
let phoneNumber = cleanPhoneNumber(value) if let userId = Int64(string) {
if !phoneNumber.isEmpty { startCall(userId)
guard let context = self.contextValue?.context else { processed = true
return true }
}
if !processed, let handle = contact.personHandle, let value = handle.value {
switch handle.type {
case .unknown:
if let userId = Int64(value) {
startCall(userId)
processed = true
} }
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false)) case .phoneNumber:
|> map { contactList -> PeerId? in let phoneNumber = cleanPhoneNumber(value)
var result: PeerId? if !phoneNumber.isEmpty {
for peer in contactList.peers { guard let context = self.contextValue?.context else {
if case let .user(peer) = peer, let peerPhoneNumber = peer.phone { return
if matchPhoneNumbers(phoneNumber, peerPhoneNumber) { }
result = peer.id let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false))
break |> map { contactList -> PeerId? in
var result: PeerId?
for peer in contactList.peers {
if case let .user(peer) = peer, let peerPhoneNumber = peer.phone {
if matchPhoneNumbers(phoneNumber, peerPhoneNumber) {
result = peer.id
break
}
} }
} }
return result
} }
return result |> deliverOnMainQueue).start(next: { peerId in
if let peerId = peerId {
startCall(peerId.id._internalGetInt64Value())
}
})
processed = true
} }
|> deliverOnMainQueue).start(next: { peerId in default:
if let peerId = peerId { break
startCall(peerId.id._internalGetInt64Value()) }
}
})
processed = true
}
default:
break
} }
} })
return true
} }
} else if let sendMessageIntent = userActivity.interaction?.intent as? INSendMessageIntent { } else if let sendMessageIntent = userActivity.interaction?.intent as? INSendMessageIntent {
if let contact = sendMessageIntent.recipients?.first, let handle = contact.customIdentifier, handle.hasPrefix("tg") { if let contact = sendMessageIntent.recipients?.first, let handle = contact.customIdentifier, handle.hasPrefix("tg") {

View File

@ -1498,8 +1498,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if packReferences.count > 1 { if packReferences.count > 1 {
items.tip = .animatedEmoji(text: presentationData.strings.ChatContextMenu_EmojiSet(Int32(packReferences.count)), arguments: nil, file: nil, action: action) items.tip = .animatedEmoji(text: presentationData.strings.ChatContextMenu_EmojiSet(Int32(packReferences.count)), arguments: nil, file: nil, action: action)
} else if let reference = packReferences.first { } else if let reference = packReferences.first {
items.tipSignal = context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false) var tipSignal: Signal<LoadedStickerPack, NoError>
|> delay(1.0, queue: .mainQueue()) tipSignal = context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false)
items.tipSignal = tipSignal
|> filter { result in |> filter { result in
if case .result = result { if case .result = result {
return true return true

View File

@ -499,7 +499,10 @@ final class ChatReportPeerTitlePanelNode: ChatTitleAccessoryPanelNode {
var emojiStatus: PeerEmojiStatus? var emojiStatus: PeerEmojiStatus?
if let user = interfaceState.renderedPeer?.peer as? TelegramUser, let emojiStatusValue = user.emojiStatus { if let user = interfaceState.renderedPeer?.peer as? TelegramUser, let emojiStatusValue = user.emojiStatus {
emojiStatus = emojiStatusValue if user.isFake || user.isScam {
} else {
emojiStatus = emojiStatusValue
}
} }
/*#if DEBUG /*#if DEBUG

View File

@ -150,12 +150,12 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
} }
if peer.id != self.context.account.peerId { if peer.id != self.context.account.peerId {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus { if peer.isFake {
titleCredibilityIcon = .emojiStatus(emojiStatus)
} else if peer.isFake {
titleCredibilityIcon = .fake titleCredibilityIcon = .fake
} else if peer.isScam { } else if peer.isScam {
titleCredibilityIcon = .scam titleCredibilityIcon = .scam
} else if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus {
titleCredibilityIcon = .emojiStatus(emojiStatus)
} else if peer.isVerified { } else if peer.isVerified {
titleCredibilityIcon = .verified titleCredibilityIcon = .verified
} else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled {

View File

@ -429,15 +429,20 @@ private final class DeviceContactDataManagerPrivateImpl {
for (stableId, basicData) in self.stableIdToBasicContactData { for (stableId, basicData) in self.stableIdToBasicContactData {
for phoneNumber in basicData.phoneNumbers { for phoneNumber in basicData.phoneNumbers {
var replace = false var replace = false
var currentLocalIdentifiers: [String] = []
if let current = importableContactData[phoneNumber.value] { if let current = importableContactData[phoneNumber.value] {
if stableId < current.0 { if stableId < current.0 {
replace = true replace = true
currentLocalIdentifiers = current.1.localIdentifiers
} }
} else { } else {
replace = true replace = true
} }
if replace { if replace {
importableContactData[phoneNumber.value] = (stableId, ImportableDeviceContactData(firstName: basicData.firstName, lastName: basicData.lastName)) if !currentLocalIdentifiers.contains(stableId) {
currentLocalIdentifiers.append(stableId)
}
importableContactData[phoneNumber.value] = (stableId, ImportableDeviceContactData(firstName: basicData.firstName, lastName: basicData.lastName, localIdentifiers: currentLocalIdentifiers))
} }
} }
} }

View File

@ -2311,13 +2311,13 @@ final class PeerInfoHeaderNode: ASDisplayNode {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 }) let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
let credibilityIcon: CredibilityIcon let credibilityIcon: CredibilityIcon
if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus { if let peer = peer {
credibilityIcon = .emojiStatus(emojiStatus)
} else if let peer = peer {
if peer.isFake { if peer.isFake {
credibilityIcon = .fake credibilityIcon = .fake
} else if peer.isScam { } else if peer.isScam {
credibilityIcon = .scam credibilityIcon = .scam
} else if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus {
credibilityIcon = .emojiStatus(emojiStatus)
} else if peer.isVerified { } else if peer.isVerified {
credibilityIcon = .verified credibilityIcon = .verified
} else if peer.isPremium && !premiumConfiguration.isPremiumDisabled && (peer.id != self.context.account.peerId || self.isSettings) { } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled && (peer.id != self.context.account.peerId || self.isSettings) {

View File

@ -104,6 +104,11 @@ private final class PrefetchManagerInnerImpl {
} }
private func updateOrderedPreloadMedia(_ items: [PrefetchMediaItem], automaticDownloadSettings: MediaAutoDownloadSettings, networkType: MediaAutoDownloadNetworkType) { private func updateOrderedPreloadMedia(_ items: [PrefetchMediaItem], automaticDownloadSettings: MediaAutoDownloadSettings, networkType: MediaAutoDownloadNetworkType) {
#if DEBUG
if "".isEmpty {
return
}
#endif
var validIds = Set<MediaId>() var validIds = Set<MediaId>()
var order: Int32 = 0 var order: Int32 = 0
for mediaItem in items { for mediaItem in items {

View File

@ -5,6 +5,7 @@
#import "NSWeakReference.h" #import "NSWeakReference.h"
@interface UIViewControllerPresentingProxy : UIViewController @interface UIViewControllerPresentingProxy : UIViewController
@property (nonatomic, copy) void (^dismiss)(); @property (nonatomic, copy) void (^dismiss)();
@ -139,6 +140,18 @@ static bool notyfyingShiftState = false;
@end @end
@interface UIWindow (Telegram)
@end
@implementation UIWindow (Telegram)
- (instancetype)_65087dc8_initWithFrame:(CGRect)frame {
return [self _65087dc8_initWithFrame:frame];
}
@end
@protocol UIRemoteKeyboardWindowProtocol @protocol UIRemoteKeyboardWindowProtocol
+ (UIWindow * _Nullable)remoteKeyboardWindowForScreen:(UIScreen * _Nullable)screen create:(BOOL)create; + (UIWindow * _Nullable)remoteKeyboardWindowForScreen:(UIScreen * _Nullable)screen create:(BOOL)create;
@ -161,7 +174,7 @@ static bool notyfyingShiftState = false;
[RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(presentViewController:animated:completion:) newSelector:@selector(_65087dc8_presentViewController:animated:completion:)]; [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(presentViewController:animated:completion:) newSelector:@selector(_65087dc8_presentViewController:animated:completion:)];
[RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(setNeedsStatusBarAppearanceUpdate) newSelector:@selector(_65087dc8_setNeedsStatusBarAppearanceUpdate)]; [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(setNeedsStatusBarAppearanceUpdate) newSelector:@selector(_65087dc8_setNeedsStatusBarAppearanceUpdate)];
[RuntimeUtils swizzleClassMethodOfClass:NSClassFromString(@"UIRemoteKeyboardWindow") currentSelector:NSSelectorFromString(@"remoteKeyboardWindowForScreen:create:") newSelector:NSSelectorFromString(@"_65087dc8_remoteKeyboardWindowForScreen:create:")]; [RuntimeUtils swizzleInstanceMethodOfClass:[UIWindow class] currentSelector:@selector(initWithFrame:) newSelector:@selector(_65087dc8_initWithFrame:)];
if (@available(iOS 15.0, *)) { if (@available(iOS 15.0, *)) {
[RuntimeUtils swizzleInstanceMethodOfClass:[CADisplayLink class] currentSelector:@selector(setPreferredFrameRateRange:) newSelector:@selector(_65087dc8_setPreferredFrameRateRange:)]; [RuntimeUtils swizzleInstanceMethodOfClass:[CADisplayLink class] currentSelector:@selector(setPreferredFrameRateRange:) newSelector:@selector(_65087dc8_setPreferredFrameRateRange:)];
@ -304,7 +317,8 @@ static bool notyfyingShiftState = false;
if (!windowClass) { if (!windowClass) {
return nil; return nil;
} }
return [(id<UIRemoteKeyboardWindowProtocol>)windowClass remoteKeyboardWindowForScreen:[UIScreen mainScreen] create:false]; UIWindow *result = [(id<UIRemoteKeyboardWindowProtocol>)windowClass remoteKeyboardWindowForScreen:[UIScreen mainScreen] create:false];
return result;
} }
@end @end

View File

@ -1,5 +1,5 @@
{ {
"app": "9.0", "app": "9.0",
"bazel": "5.1.0", "bazel": "5.1.0",
"xcode": "13.4.1" "xcode": "14.0"
} }