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
build-input/*
*/*.pyc
**/*.pyc
*.pyc

View File

@ -18,8 +18,8 @@ internal:
except:
- tags
script:
- bash buildbox/build-telegram.sh hockeyapp
- bash buildbox/deploy-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
- 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:
name: internal
artifacts:
@ -37,8 +37,8 @@ appstore_development:
except:
- tags
script:
- bash buildbox/build-telegram.sh appstore-development
- bash buildbox/deploy-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
- 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:
name: appstore-development
artifacts:
@ -48,15 +48,15 @@ appstore_development:
experimental_i:
tags:
- ios_internal
- ios_experimental
stage: build
only:
- experimental-3
except:
- tags
script:
- bash buildbox/build-telegram.sh appcenter-experimental
- bash buildbox/deploy-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
- 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:
name: experimental
artifacts:
@ -73,8 +73,8 @@ experimental:
except:
- tags
script:
- bash buildbox/build-telegram.sh appcenter-experimental-2
- bash buildbox/deploy-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
- 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:
name: experimental-2
artifacts:
@ -92,7 +92,7 @@ beta_testflight:
except:
- tags
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:
name: testflight_llc
artifacts:

View File

@ -464,7 +464,7 @@ private struct NotificationContent: CustomStringConvertible {
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, *) {
let image = peerAvatar(mediaBox: mediaBox, accountPeerId: accountPeerId, peer: peer)
@ -483,7 +483,7 @@ private struct NotificationContent: CustomStringConvertible {
nameComponents: personNameComponents,
displayName: displayName,
image: image,
contactIdentifier: nil,
contactIdentifier: contactIdentifier,
customIdentifier: "\(peer.id.toInt64())",
isMe: false,
suggestionType: .none
@ -818,6 +818,7 @@ private final class NotificationServiceHandler {
var updates: String
var accountId: Int64
var peer: EnginePeer?
var localContactId: String?
}
var callData: CallData?
@ -1033,35 +1034,51 @@ private final class NotificationServiceHandler {
if let action = action {
switch action {
case let .call(callData):
let voipPayload: [AnyHashable: Any] = [
"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")
if let stateManager = strongSelf.stateManager {
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))")
let _ = (stateManager.postbox.transaction { transaction -> String? in
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:
Logger.shared.log("NotificationService \(episode)", "Will logout")
@ -1334,7 +1351,23 @@ private final class NotificationServiceHandler {
if let interactionAuthorId = 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 platform
import subprocess
import sys
def is_apple_silicon():
if platform.processor() == 'arm':
@ -28,20 +28,41 @@ def resolve_executable(program):
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)
if executable_path is None:
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(
[executable_path] + arguments,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
stderr=stderr_assignment,
stdin=subprocess.PIPE,
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')
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):
@ -62,6 +83,12 @@ def call_executable(arguments, use_clean_environment=True, check_result=True):
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):
command_result = run_executable_with_output(bazel_path, ['--version']).strip('\n')
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 tempfile
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 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:
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()
def resolve_configuration(bazel_command_line: BazelCommandLine, arguments):
if arguments.configurationGenerator is not None:
configuration_generator_arguments = shlex.split(arguments.configurationGenerator)
def resolve_codesigning(arguments, base_path, build_configuration, provisioning_profiles_path, additional_codesigning_output_path) -> ResolvedCodesigningData:
profile_source = None
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:
print('{} is not a valid executable'.format(configuration_generator_arguments[0]))
exit(1)
private_key = os.getenv('TELEGRAM_CODESIGNING_PRIVATE_KEY')
temp_configuration_path = tempfile.mkdtemp()
resolved_configuration_generator_arguments = [configuration_generator_executable]
resolved_configuration_generator_arguments += configuration_generator_arguments[1:]
resolved_configuration_generator_arguments += [temp_configuration_path]
call_executable(resolved_configuration_generator_arguments, use_clean_environment=False)
print('TelegramBuild: using generated configuration in {}'.format(temp_configuration_path))
bazel_command_line.set_configuration_path(temp_configuration_path)
elif arguments.configurationPath is not None:
absolute_configuration_path = os.path.abspath(arguments.configurationPath)
if not os.path.isdir(absolute_configuration_path):
print('Error: {} does not exist'.format(absolute_configuration_path))
exit(1)
bazel_command_line.set_configuration_path(absolute_configuration_path)
profile_source = GitCodesigningSource(
repo_url=arguments.gitCodesigningRepository,
private_key=private_key,
team_id=build_configuration.team_id,
bundle_id=build_configuration.bundle_id,
codesigning_type=arguments.gitCodesigningType,
password=password,
always_fetch=arguments.gitCodesigningAlwaysFetch
)
elif arguments.codesigningInformationPath is not None:
profile_source = DirectoryCodesigningSource(
directory_path=arguments.codesigningInformationPath,
team_id=build_configuration.team_id,
bundle_id=build_configuration.bundle_id
)
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):
@ -425,7 +496,12 @@ def generate_project(bazel, arguments):
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)
@ -469,7 +545,12 @@ def build(bazel, arguments):
elif arguments.cacheHost is not None:
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_build_number(arguments.buildNumber)
@ -481,6 +562,38 @@ def build(bazel, arguments):
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):
bazel_command_line = BazelCommandLine(
@ -495,7 +608,12 @@ def test(bazel, arguments):
elif arguments.cacheHost is not None:
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_build_number('10000')
@ -503,29 +621,64 @@ def test(bazel, arguments):
bazel_command_line.invoke_test()
def add_project_and_build_common_arguments(current_parser: argparse.ArgumentParser):
group = current_parser.add_mutually_exclusive_group(required=True)
group.add_argument(
def add_codesigning_common_arguments(current_parser: argparse.ArgumentParser):
configuration_group = current_parser.add_mutually_exclusive_group(required=True)
configuration_group.add_argument(
'--configurationPath',
help='''
Path to a folder containing build configuration and provisioning profiles.
See build-system/example-configuration for an example.
Path to a json containing build configuration.
See build-system/appstore-configuration.json for an example.
''',
metavar='path'
)
group.add_argument(
'--configurationGenerator',
codesigning_group = current_parser.add_mutually_exclusive_group(required=True)
codesigning_group.add_argument(
'--gitCodesigningRepository',
help='''
A command line invocation that will dynamically generate the configuration data
(project constants and provisioning profiles).
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.
See build-system/generate-configuration.sh for an example.
Example: --configurationGenerator="sh ~/my_script.sh argument1"
If specified, certificates and provisioning profiles will be loaded from git.
TELEGRAM_CODESIGNING_GIT_PASSWORD environment variable must be set.
''',
metavar='path'
)
codesigning_group.add_argument(
'--codesigningInformationPath',
help='''
Use signing certificates and provisioning profiles from a local directory.
''',
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__':
parser = argparse.ArgumentParser(prog='Make')
@ -575,7 +728,7 @@ if __name__ == '__main__':
'--cacheHost',
required=False,
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(
'--cacheDir',
@ -700,6 +853,40 @@ if __name__ == '__main__':
default=False,
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:
parser.print_help()
@ -726,6 +913,30 @@ if __name__ == '__main__':
generate_project(bazel=bazel_path, arguments=args)
elif args.commandName == 'build':
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':
test(bazel=bazel_path, arguments=args)
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" \
--buildNumber="$BUILD_NUMBER" \
--disableParallelSwiftmoduleGeneration \
--configuration="$APP_CONFIGURATION"
--configuration="$APP_CONFIGURATION" \
--apsEnvironment=production
OUTPUT_PATH="build/artifacts"
rm -rf "$OUTPUT_PATH"

View File

@ -57,7 +57,7 @@ public func messageMediaImageInteractiveFetched(fetchManager: FetchManager, mess
if let range = range {
ranges = RangeSet(range.lowerBound ..< range.upperBound)
} 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)
}

View File

@ -132,6 +132,10 @@ public final class AppLockContextImpl: AppLockContext {
if !strongSelf.lastActiveValue {
strongSelf.lastActiveValue = true
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 {

View File

@ -482,7 +482,7 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
for node in nodes {
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()
})
}
@ -491,12 +491,11 @@ final class InnerTextSelectionTipContainerNode: ASDisplayNode {
func animateContentIn() {
let nodes: [ASDisplayNode] = [
self.textNode.textNode,
self.iconNode,
self.placeholderNode
self.iconNode
]
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
var globalOverlayLayout = layout
globalOverlayLayout.inputHeight = nil
if let globalOverlayBelowKeyboardContainerParent = self.globalOverlayBelowKeyboardContainerParent {
if globalOverlayBelowKeyboardContainerParent.view.superview != self.displayNode.view {

View File

@ -226,7 +226,7 @@ private final class FetchManagerCategoryContext {
let storeManager = self.storeManager
let parsedRanges: [(Range<Int64>, MediaBoxFetchPriority)]?
if ranges == RangeSet<Int64>(0 ..< Int64.max) {
if ranges == RangeSet<Int64>(0 ..< Int64.max), !"".isEmpty {
parsedRanges = nil
} else {
var resultRanges: [(Range<Int64>, MediaBoxFetchPriority)] = []
@ -325,7 +325,7 @@ private final class FetchManagerCategoryContext {
isVideoPreload = true
}
if count == 1 && isCompleteRange {
if count == 1 && isCompleteRange && !"".isEmpty {
parsedRanges = nil
} else {
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 request = SFSpeechURLRecognitionRequest(url: URL(fileURLWithPath: tempFilePath))
if #available(iOS 16.0, *) {
request.addsPunctuation = true
}
request.requiresOnDeviceRecognition = speechRecognizer.supportsOnDeviceRecognition
request.shouldReportPartialResults = true

View File

@ -574,6 +574,12 @@ public final class MediaBox {
let disposable = MetaDisposable()
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 {
subscriber.putCompletion()
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 {
switch self.content {
case .complete:
completed()
return EmptyDisposable
case let .partial(file):
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 {
switch attribute {
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))
} else if alt == query {
result.append((alt, item.file, alt))
}
}
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 existingIds = Set<MediaId>()

View File

@ -730,10 +730,11 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1660637285] = { return Api.StatsGroupTopPoster.parse_statsGroupTopPoster($0) }
dict[-875679776] = { return Api.StatsPercentValue.parse_statsPercentValue($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[768691932] = { return Api.StickerSet.parse_stickerSet($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[-1609668650] = { return Api.Theme.parse_theme($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[1443858741] = { return Api.messages.SentEncryptedMessage.parse_sentEncryptedMessage($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[904138920] = { return Api.messages.StickerSetInstallResult.parse_stickerSetInstallResultArchive($0) }
dict[946083368] = { return Api.messages.StickerSetInstallResult.parse_stickerSetInstallResultSuccess($0) }
@ -1584,6 +1585,8 @@ public extension Api {
_1.serialize(buffer, boxed)
case let _1 as Api.StatsURL:
_1.serialize(buffer, boxed)
case let _1 as Api.StickerKeyword:
_1.serialize(buffer, boxed)
case let _1 as Api.StickerPack:
_1.serialize(buffer, boxed)
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 {
enum StickerPack: TypeConstructorDescription {
case stickerPack(emoticon: String, documents: [Int64])
@ -133,7 +179,7 @@ public extension Api {
public extension Api {
enum StickerSetCovered: TypeConstructorDescription {
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])
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
@ -145,9 +191,9 @@ public extension Api {
set.serialize(buffer, true)
cover.serialize(buffer, true)
break
case .stickerSetFullCovered(let set, let packs, let documents):
case .stickerSetFullCovered(let set, let packs, let keywords, let documents):
if boxed {
buffer.appendInt32(451763941)
buffer.appendInt32(1087454222)
}
set.serialize(buffer, true)
buffer.appendInt32(481674261)
@ -156,6 +202,11 @@ public extension Api {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(keywords.count))
for item in keywords {
item.serialize(buffer, true)
}
buffer.appendInt32(481674261)
buffer.appendInt32(Int32(documents.count))
for item in documents {
item.serialize(buffer, true)
@ -179,8 +230,8 @@ public extension Api {
switch self {
case .stickerSetCovered(let set, let cover):
return ("stickerSetCovered", [("set", String(describing: set)), ("cover", String(describing: cover))])
case .stickerSetFullCovered(let set, let packs, let documents):
return ("stickerSetFullCovered", [("set", String(describing: set)), ("packs", String(describing: packs)), ("documents", String(describing: documents))])
case .stickerSetFullCovered(let set, let packs, let keywords, let 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):
return ("stickerSetMultiCovered", [("set", String(describing: set)), ("covers", String(describing: covers))])
}
@ -213,15 +264,20 @@ public extension Api {
if let _ = reader.readInt32() {
_2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self)
}
var _3: [Api.Document]?
var _3: [Api.StickerKeyword]?
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 _c2 = _2 != nil
let _c3 = _3 != nil
if _c1 && _c2 && _c3 {
return Api.StickerSetCovered.stickerSetFullCovered(set: _1!, packs: _2!, documents: _3!)
let _c4 = _4 != nil
if _c1 && _c2 && _c3 && _c4 {
return Api.StickerSetCovered.stickerSetFullCovered(set: _1!, packs: _2!, keywords: _3!, documents: _4!)
}
else {
return nil

View File

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

View File

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

View File

@ -41,7 +41,7 @@ public final class CallKitIntegration {
}
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,
endCall: @escaping (UUID) -> Signal<Bool, NoError>,
setCallMuted: @escaping (UUID, Bool) -> Void,
@ -56,22 +56,17 @@ public final class CallKitIntegration {
if !CallKitIntegration.isAvailable {
return nil
}
#if targetEnvironment(simulator)
return nil
#else
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
} else {
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, *) {
(sharedProviderDelegate as? CallKitProviderDelegate)?.startCall(context: context, peerId: peerId, isVideo: isVideo, displayTitle: displayTitle)
self.donateIntent(peerId: peerId, displayTitle: displayTitle)
(sharedProviderDelegate as? CallKitProviderDelegate)?.startCall(context: context, peerId: peerId, phoneNumber: phoneNumber, isVideo: isVideo, 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, *) {
(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, *) {
let handle = INPersonHandle(value: "tg\(peerId.id)", type: .unknown)
let contact = INPerson(personHandle: handle, nameComponents: nil, displayName: displayTitle, image: nil, contactIdentifier: nil, customIdentifier: "tg\(peerId.id)")
let handle = INPersonHandle(value: "tg\(peerId.id._internalGetInt64Value())", type: .unknown)
let contact = INPerson(personHandle: handle, nameComponents: nil, displayName: displayTitle, image: nil, contactIdentifier: localContactId, customIdentifier: "tg\(peerId.id._internalGetInt64Value())")
let intent = INStartAudioCallIntent(contacts: [contact])
@ -122,8 +117,9 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
private var currentStartCallAccount: (UUID, AccountContext)?
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 endCall: ((UUID) -> Signal<Bool, NoError>)?
private var setCallMuted: ((UUID, Bool) -> Void)?
@ -141,7 +137,7 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
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.startCall = startCall
self.answerCall = answerCall
@ -189,10 +185,18 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
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()
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)
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) {
completion?(nil)
return
@ -220,7 +224,13 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
self.alreadyReportedIncomingCalls.insert(uuid)
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.supportsHolding = false
update.supportsGrouping = false
@ -252,7 +262,10 @@ class CallKitProviderDelegate: NSObject, CXProviderDelegate {
self.currentStartCallAccount = nil
let disposable = MetaDisposable()
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
|> afterDisposed { [weak self, weak disposable] in
if let strongSelf = self, let disposable = disposable {

View File

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

View File

@ -11,6 +11,7 @@ import TelegramVoip
import TelegramUIPreferences
import AccountContext
import CallKit
import PhoneNumberFormat
private func callKitIntegrationIfEnabled(_ integration: CallKitIntegration?, settings: VoiceCallSettings?) -> CallKitIntegration? {
let enabled = settings?.enableSystemIntegration ?? true
@ -128,16 +129,16 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
self.isMediaPlaying = isMediaPlaying
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 endCallImpl: ((UUID) -> Signal<Bool, NoError>)?
var setCallMutedImpl: ((UUID, Bool) -> Void)?
var audioSessionActivationChangedImpl: ((Bool) -> Void)?
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 {
return startCallImpl(context, uuid, handle, isVideo)
return startCallImpl(context, uuid, maybePeerId, handle, isVideo)
} else {
return .single(false)
}
@ -215,16 +216,26 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
self?.ringingStatesUpdated(ringingStates, enableCallKit: enableCallKit)
})
startCallImpl = { [weak self] context, uuid, handle, isVideo in
if let strongSelf = self, let userId = Int64(handle) {
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 {
startCallImpl = { [weak self] context, uuid, maybePeerId, handle, isVideo in
guard let strongSelf = self else {
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
@ -398,19 +409,39 @@ public final class PresentationCallManagerImpl: PresentationCallManager {
|> runOn(Queue.mainQueue())
let postbox = context.account.postbox
strongSelf.startCallDisposable.set((accessEnabledSignal
|> mapToSignal { accessEnabled -> Signal<Peer?, NoError> in
|> mapToSignal { accessEnabled -> Signal<(Peer?, String?), NoError> in
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 {
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 {

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)
//|> delay(5.0, queue: self.queue)
|> deliverOn(self.queue)
let partDisposable = MetaDisposable()
self.fetchingParts[downloadRange.lowerBound] = (Int64(downloadRange.count), partDisposable)

View File

@ -3604,7 +3604,7 @@ func replayFinalState(
let namespace: ItemCollectionId.Namespace
var items: [ItemCollectionItem] = []
let info: StickerPackCollectionInfo
if case let .stickerSet(set, packs, documents) = apiSet {
if case let .stickerSet(set, packs, keywords, documents) = apiSet {
var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:]
for pack in packs {
switch pack {
@ -3621,6 +3621,20 @@ func replayFinalState(
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 {
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
for result in results {
switch result {
case let .stickerSet(_, _, documents)?:
case let .stickerSet(_, _, _, documents)?:
for document in documents {
if let file = telegramMediaFileFromApiDocument(document) {
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 value = value as? TelegramDeviceContactImportedData {
switch value {
case let .imported(data, _):
case let .imported(data, _, _):
if data != updatedData {
updatedDataIdentifiers.insert(identifier)
}
@ -289,7 +289,10 @@ private func pushDeviceContacts(postbox: Postbox, network: Network, importableCo
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 (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)
continue outer
}
@ -334,6 +337,7 @@ private func pushDeviceContactData(postbox: Postbox, network: Network, contacts:
var addedContactPeerIds = Set<PeerId>()
var retryIndices = Set<Int>()
var importedCounts: [Int: Int32] = [:]
var peerIdByClientId: [Int64: PeerId] = [:]
switch result {
case let .importedContacts(imported, popularInvites, retryContacts, users):
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 {
switch item {
case let .importedContact(userId, _):
addedContactPeerIds.insert(PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)))
case let .importedContact(userId, clientId):
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
addedContactPeerIds.insert(peerId)
peerIdByClientId[clientId] = peerId
}
}
for item in retryContacts {
@ -363,7 +369,10 @@ private func pushDeviceContactData(postbox: Postbox, network: Network, contacts:
importedData = .retryLater
addedReimportAttempts[.phoneNumber(batch[i].0)] = timestamp
} 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)
}

View File

@ -147,7 +147,7 @@ private func fetchStickerPack(network: Network, info: StickerPackCollectionInfo)
switch result {
case .stickerSetNotModified:
break
case let .stickerSet(stickerSet, packs, documents):
case let .stickerSet(stickerSet, packs, keywords, documents):
updatedInfo = StickerPackCollectionInfo(apiSet: stickerSet, namespace: info.id.namespace)
var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:]
for pack in packs {
@ -165,6 +165,20 @@ private func fetchStickerPack(network: Network, info: StickerPackCollectionInfo)
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 {
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)
case let .stickerSetMultiCovered(set, _):
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)
}
}

View File

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

View File

@ -227,7 +227,7 @@ func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollect
}
}
return (info, items)
case let .stickerSetFullCovered(set, packs, documents):
case let .stickerSetFullCovered(set, packs, keywords, documents):
var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:]
for pack in packs {
switch pack {
@ -244,6 +244,20 @@ func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollect
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)
var items: [StickerPackItem] = []

View File

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

View File

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

View File

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

View File

@ -53,7 +53,7 @@ func _internal_deviceContactsImportedByCount(postbox: Postbox, contacts: [(Strin
for (id, numbers) in contacts {
var maxCount: Int32 = 0
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)
}
}

View File

@ -69,5 +69,32 @@ public extension TelegramEngine {
}
|> 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 {
case .stickerSetNotModified:
return .complete()
case let .stickerSet(set, packs, documents):
case let .stickerSet(set, packs, keywords, documents):
let namespace: ItemCollectionId.Namespace
switch set {
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 {
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 {
case .stickerSetNotModified:
return .complete()
case let .stickerSet(set, packs, documents):
case let .stickerSet(set, packs, keywords, documents):
let namespace: ItemCollectionId.Namespace
switch set {
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 {
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
return result.map { pack in
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)
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 {
case .stickerSetNotModified:
return .complete()
case let .stickerSet(set, packs, documents):
case let .stickerSet(set, packs, keywords, documents):
info = StickerPackCollectionInfo(apiSet: set, namespace: Namespaces.ItemCollection.CloudStickerPacks)
switch set {
@ -95,6 +95,20 @@ func _internal_requestStickerSet(postbox: Postbox, network: Network, reference:
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 {
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):
apiSet = set
apiDocuments = covers
case let .stickerSetFullCovered(set, _, documents):
case let .stickerSetFullCovered(set, _, _, documents):
apiSet = set
apiDocuments = documents
}

View File

@ -34,6 +34,7 @@ import DebugSettingsUI
import BackgroundTasks
import UIKitRuntimeUtils
import StoreKit
import PhoneNumberFormat
#if canImport(AppCenter)
import AppCenter
@ -1557,11 +1558,14 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
completion()
return
}
let phoneNumber = payloadJson["phoneNumber"] as? String
callKitIntegration.reportIncomingCall(
uuid: CallSessionManager.getStableIncomingUUID(stableId: callUpdate.callId),
stableId: callUpdate.callId,
handle: "\(callUpdate.peer.id.id._internalGetInt64Value())",
phoneNumber: phoneNumber.flatMap(formatPhoneNumber),
isVideo: false,
displayTitle: callUpdate.peer.debugDisplayTitle,
completion: { error in
@ -1745,52 +1749,65 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
}
if let contact = startCallContacts.first {
var processed = false
if let handle = contact.customIdentifier, handle.hasPrefix("tg") {
let string = handle.suffix(from: handle.index(handle.startIndex, offsetBy: 2))
if let userId = Int64(string) {
startCall(userId)
processed = true
}
let contactByIdentifier: Signal<EnginePeer?, NoError>
if let context = self.contextValue?.context, let contactIdentifier = contact.contactIdentifier {
contactByIdentifier = context.engine.contacts.findPeerByLocalContactIdentifier(identifier: contactIdentifier)
} else {
contactByIdentifier = .single(nil)
}
if !processed, let handle = contact.personHandle, let value = handle.value {
switch handle.type {
case .unknown:
if let userId = Int64(value) {
startCall(userId)
processed = true
}
case .phoneNumber:
let phoneNumber = cleanPhoneNumber(value)
if !phoneNumber.isEmpty {
guard let context = self.contextValue?.context else {
return true
let _ = (contactByIdentifier |> deliverOnMainQueue).start(next: { peerByContact in
var processed = false
if let peerByContact = peerByContact {
startCall(peerByContact.id.id._internalGetInt64Value())
processed = true
} else if let handle = contact.customIdentifier, handle.hasPrefix("tg") {
let string = handle.suffix(from: handle.index(handle.startIndex, offsetBy: 2))
if let userId = Int64(string) {
startCall(userId)
processed = 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))
|> 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
case .phoneNumber:
let phoneNumber = cleanPhoneNumber(value)
if !phoneNumber.isEmpty {
guard let context = self.contextValue?.context else {
return
}
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: false))
|> 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
if let peerId = peerId {
startCall(peerId.id._internalGetInt64Value())
}
})
processed = true
}
default:
break
default:
break
}
}
}
})
return true
}
} else if let sendMessageIntent = userActivity.interaction?.intent as? INSendMessageIntent {
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 {
items.tip = .animatedEmoji(text: presentationData.strings.ChatContextMenu_EmojiSet(Int32(packReferences.count)), arguments: nil, file: nil, action: action)
} else if let reference = packReferences.first {
items.tipSignal = context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false)
|> delay(1.0, queue: .mainQueue())
var tipSignal: Signal<LoadedStickerPack, NoError>
tipSignal = context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false)
items.tipSignal = tipSignal
|> filter { result in
if case .result = result {
return true

View File

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

View File

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

View File

@ -429,15 +429,20 @@ private final class DeviceContactDataManagerPrivateImpl {
for (stableId, basicData) in self.stableIdToBasicContactData {
for phoneNumber in basicData.phoneNumbers {
var replace = false
var currentLocalIdentifiers: [String] = []
if let current = importableContactData[phoneNumber.value] {
if stableId < current.0 {
replace = true
currentLocalIdentifiers = current.1.localIdentifiers
}
} else {
replace = true
}
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 credibilityIcon: CredibilityIcon
if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus {
credibilityIcon = .emojiStatus(emojiStatus)
} else if let peer = peer {
if let peer = peer {
if peer.isFake {
credibilityIcon = .fake
} else if peer.isScam {
credibilityIcon = .scam
} else if let user = peer as? TelegramUser, let emojiStatus = user.emojiStatus {
credibilityIcon = .emojiStatus(emojiStatus)
} else if peer.isVerified {
credibilityIcon = .verified
} 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) {
#if DEBUG
if "".isEmpty {
return
}
#endif
var validIds = Set<MediaId>()
var order: Int32 = 0
for mediaItem in items {

View File

@ -5,6 +5,7 @@
#import "NSWeakReference.h"
@interface UIViewControllerPresentingProxy : UIViewController
@property (nonatomic, copy) void (^dismiss)();
@ -139,6 +140,18 @@ static bool notyfyingShiftState = false;
@end
@interface UIWindow (Telegram)
@end
@implementation UIWindow (Telegram)
- (instancetype)_65087dc8_initWithFrame:(CGRect)frame {
return [self _65087dc8_initWithFrame:frame];
}
@end
@protocol UIRemoteKeyboardWindowProtocol
+ (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(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, *)) {
[RuntimeUtils swizzleInstanceMethodOfClass:[CADisplayLink class] currentSelector:@selector(setPreferredFrameRateRange:) newSelector:@selector(_65087dc8_setPreferredFrameRateRange:)];
@ -304,7 +317,8 @@ static bool notyfyingShiftState = false;
if (!windowClass) {
return nil;
}
return [(id<UIRemoteKeyboardWindowProtocol>)windowClass remoteKeyboardWindowForScreen:[UIScreen mainScreen] create:false];
UIWindow *result = [(id<UIRemoteKeyboardWindowProtocol>)windowClass remoteKeyboardWindowForScreen:[UIScreen mainScreen] create:false];
return result;
}
@end

View File

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