Move app to a dedicated folder
550
Telegram/BUCK
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
load("//Config:utils.bzl",
|
||||
"library_configs",
|
||||
)
|
||||
|
||||
load("//Config:configs.bzl",
|
||||
"app_binary_configs",
|
||||
"share_extension_configs",
|
||||
"widget_extension_configs",
|
||||
"notification_content_extension_configs",
|
||||
"notification_service_extension_configs",
|
||||
"intents_extension_configs",
|
||||
"watch_extension_binary_configs",
|
||||
"watch_binary_configs",
|
||||
"info_plist_substitutions",
|
||||
"app_info_plist_substitutions",
|
||||
"share_extension_info_plist_substitutions",
|
||||
"widget_extension_info_plist_substitutions",
|
||||
"notification_content_extension_info_plist_substitutions",
|
||||
"notification_service_extension_info_plist_substitutions",
|
||||
"intents_extension_info_plist_substitutions",
|
||||
"watch_extension_info_plist_substitutions",
|
||||
"watch_info_plist_substitutions",
|
||||
"DEVELOPMENT_LANGUAGE",
|
||||
)
|
||||
|
||||
load("//Config:buck_rule_macros.bzl",
|
||||
"apple_lib",
|
||||
"framework_binary_dependencies",
|
||||
"framework_bundle_dependencies",
|
||||
"glob_map",
|
||||
"glob_sub_map",
|
||||
"merge_maps",
|
||||
)
|
||||
|
||||
framework_dependencies = [
|
||||
"//submodules/MtProtoKit:MtProtoKit",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramApi:TelegramApi",
|
||||
"//submodules/SyncCore:SyncCore",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/TelegramUI:TelegramUI",
|
||||
]
|
||||
|
||||
resource_dependencies = [
|
||||
"//submodules/LegacyComponents:LegacyComponentsResources",
|
||||
"//submodules/TelegramUI:TelegramUIAssets",
|
||||
"//submodules/TelegramUI:TelegramUIResources",
|
||||
#"//submodules/WalletUI:WalletUIAssets",
|
||||
#"//submodules/WalletUI:WalletUIResources",
|
||||
"//submodules/PasswordSetupUI:PasswordSetupUIResources",
|
||||
"//submodules/PasswordSetupUI:PasswordSetupUIAssets",
|
||||
"//submodules/OverlayStatusController:OverlayStatusControllerResources",
|
||||
":AppResources",
|
||||
":AppStringResources",
|
||||
":InfoPlistStringResources",
|
||||
":AppIntentVocabularyResources",
|
||||
":Icons",
|
||||
":AdditionalIcons",
|
||||
":LaunchScreen",
|
||||
]
|
||||
|
||||
build_phase_scripts = [
|
||||
]
|
||||
|
||||
apple_resource(
|
||||
name = "AppResources",
|
||||
files = glob([
|
||||
"Telegram-iOS/Resources/**/*",
|
||||
], exclude = ["Telegram-iOS/Resources/**/.*"]),
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_resource(
|
||||
name = "AppStringResources",
|
||||
files = [],
|
||||
variants = glob([
|
||||
"Telegram-iOS/*.lproj/Localizable.strings",
|
||||
]),
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_resource(
|
||||
name = "AppIntentVocabularyResources",
|
||||
files = [],
|
||||
variants = glob([
|
||||
"Telegram-iOS/*.lproj/AppIntentVocabulary.plist",
|
||||
]),
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_resource(
|
||||
name = "InfoPlistStringResources",
|
||||
files = [],
|
||||
variants = glob([
|
||||
"Telegram-iOS/*.lproj/InfoPlist.strings",
|
||||
]),
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_asset_catalog(
|
||||
name = "Icons",
|
||||
dirs = [
|
||||
"Telegram-iOS/Icons.xcassets",
|
||||
"Telegram-iOS/AppIcons.xcassets",
|
||||
],
|
||||
app_icon = "AppIconLLC",
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_resource(
|
||||
name = "AdditionalIcons",
|
||||
files = glob([
|
||||
"Telegram-iOS/*.png",
|
||||
]),
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_resource(
|
||||
name = "LaunchScreen",
|
||||
files = [
|
||||
"Telegram-iOS/Base.lproj/LaunchScreen.xib",
|
||||
],
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_library(
|
||||
name = "AppLibrary",
|
||||
visibility = [
|
||||
"//...",
|
||||
],
|
||||
configs = library_configs(),
|
||||
swift_version = native.read_config("swift", "version"),
|
||||
srcs = [
|
||||
"Telegram-iOS/main.m",
|
||||
"Telegram-iOS/Application.swift"
|
||||
],
|
||||
deps = [
|
||||
]
|
||||
+ framework_binary_dependencies(framework_dependencies),
|
||||
)
|
||||
|
||||
apple_binary(
|
||||
name = "AppBinary",
|
||||
visibility = [
|
||||
"//...",
|
||||
],
|
||||
configs = app_binary_configs(),
|
||||
swift_version = native.read_config("swift", "version"),
|
||||
srcs = [
|
||||
"SupportFiles/Empty.swift",
|
||||
],
|
||||
deps = [
|
||||
":AppLibrary",
|
||||
]
|
||||
+ resource_dependencies,
|
||||
)
|
||||
|
||||
apple_bundle(
|
||||
name = "Telegram",
|
||||
visibility = [
|
||||
"//Telegram:...",
|
||||
],
|
||||
extension = "app",
|
||||
binary = ":AppBinary",
|
||||
product_name = "Telegram",
|
||||
info_plist = "Telegram-iOS/Info.plist",
|
||||
info_plist_substitutions = app_info_plist_substitutions(),
|
||||
deps = [
|
||||
":ShareExtension",
|
||||
":WidgetExtension",
|
||||
":NotificationContentExtension",
|
||||
":NotificationServiceExtension",
|
||||
":IntentsExtension",
|
||||
":WatchApp#watch",
|
||||
]
|
||||
+ framework_bundle_dependencies(framework_dependencies),
|
||||
)
|
||||
|
||||
# Share Extension
|
||||
|
||||
apple_binary(
|
||||
name = "ShareBinary",
|
||||
srcs = glob([
|
||||
"Share/**/*.swift",
|
||||
]),
|
||||
configs = share_extension_configs(),
|
||||
linker_flags = [
|
||||
"-e",
|
||||
"_NSExtensionMain",
|
||||
"-Xlinker",
|
||||
"-rpath",
|
||||
"-Xlinker",
|
||||
"/usr/lib/swift",
|
||||
"-Xlinker",
|
||||
"-rpath",
|
||||
"-Xlinker",
|
||||
"@executable_path/../../Frameworks",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/TelegramUI:TelegramUI#shared",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
],
|
||||
)
|
||||
|
||||
apple_bundle(
|
||||
name = "ShareExtension",
|
||||
binary = ":ShareBinary",
|
||||
extension = "appex",
|
||||
info_plist = "Share/Info.plist",
|
||||
info_plist_substitutions = share_extension_info_plist_substitutions(),
|
||||
deps = [
|
||||
],
|
||||
xcode_product_type = "com.apple.product-type.app-extension",
|
||||
)
|
||||
|
||||
# Widget
|
||||
|
||||
apple_binary(
|
||||
name = "WidgetBinary",
|
||||
srcs = glob([
|
||||
"Widget/**/*.swift",
|
||||
]),
|
||||
configs = widget_extension_configs(),
|
||||
swift_compiler_flags = [
|
||||
"-application-extension",
|
||||
],
|
||||
linker_flags = [
|
||||
"-e",
|
||||
"_NSExtensionMain",
|
||||
"-Xlinker",
|
||||
"-rpath",
|
||||
"-Xlinker",
|
||||
"/usr/lib/swift",
|
||||
"-Xlinker",
|
||||
"-rpath",
|
||||
"-Xlinker",
|
||||
"@executable_path/../../Frameworks",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/BuildConfig:BuildConfig",
|
||||
"//submodules/WidgetItems:WidgetItems",
|
||||
"//submodules/AppLockState:AppLockState",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/NotificationCenter.framework",
|
||||
],
|
||||
)
|
||||
|
||||
apple_bundle(
|
||||
name = "WidgetExtension",
|
||||
binary = ":WidgetBinary",
|
||||
extension = "appex",
|
||||
info_plist = "Widget/Info.plist",
|
||||
info_plist_substitutions = widget_extension_info_plist_substitutions(),
|
||||
deps = [
|
||||
],
|
||||
xcode_product_type = "com.apple.product-type.app-extension",
|
||||
)
|
||||
|
||||
# Notification Content
|
||||
|
||||
apple_binary(
|
||||
name = "NotificationContentBinary",
|
||||
srcs = glob([
|
||||
"NotificationContent/**/*.swift",
|
||||
]),
|
||||
configs = notification_content_extension_configs(),
|
||||
swift_compiler_flags = [
|
||||
"-application-extension",
|
||||
],
|
||||
linker_flags = [
|
||||
"-e",
|
||||
"_NSExtensionMain",
|
||||
"-Xlinker",
|
||||
"-rpath",
|
||||
"-Xlinker",
|
||||
"/usr/lib/swift",
|
||||
"-Xlinker",
|
||||
"-rpath",
|
||||
"-Xlinker",
|
||||
"@executable_path/../../Frameworks",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/TelegramUI:TelegramUI#shared",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/UserNotificationsUI.framework",
|
||||
],
|
||||
)
|
||||
|
||||
apple_bundle(
|
||||
name = "NotificationContentExtension",
|
||||
binary = ":NotificationContentBinary",
|
||||
extension = "appex",
|
||||
info_plist = "NotificationContent/Info.plist",
|
||||
info_plist_substitutions = notification_content_extension_info_plist_substitutions(),
|
||||
deps = [
|
||||
],
|
||||
xcode_product_type = "com.apple.product-type.app-extension",
|
||||
)
|
||||
|
||||
#Notification Service
|
||||
|
||||
apple_binary(
|
||||
name = "NotificationServiceBinary",
|
||||
srcs = glob([
|
||||
"NotificationService/**/*.m",
|
||||
"NotificationService/**/*.swift",
|
||||
]),
|
||||
headers = glob([
|
||||
"NotificationService/**/*.h",
|
||||
]),
|
||||
bridging_header = "NotificationService/NotificationService-Bridging-Header.h",
|
||||
configs = notification_service_extension_configs(),
|
||||
swift_compiler_flags = [
|
||||
"-application-extension",
|
||||
],
|
||||
linker_flags = [
|
||||
"-e",
|
||||
"_NSExtensionMain",
|
||||
"-Xlinker",
|
||||
"-rpath",
|
||||
"-Xlinker",
|
||||
"/usr/lib/swift",
|
||||
"-Xlinker",
|
||||
"-rpath",
|
||||
"-Xlinker",
|
||||
"@executable_path/../../Frameworks",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/BuildConfig:BuildConfig",
|
||||
"//submodules/MtProtoKit:MtProtoKit#shared",
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
|
||||
"//submodules/EncryptionProvider:EncryptionProvider",
|
||||
"//submodules/Database/ValueBox:ValueBox",
|
||||
"//submodules/Database/PostboxDataTypes:PostboxDataTypes",
|
||||
"//submodules/Database/MessageHistoryReadStateTable:MessageHistoryReadStateTable",
|
||||
"//submodules/Database/MessageHistoryMetadataTable:MessageHistoryMetadataTable",
|
||||
"//submodules/Database/PreferencesTable:PreferencesTable",
|
||||
"//submodules/Database/PeerTable:PeerTable",
|
||||
"//submodules/sqlcipher:sqlcipher",
|
||||
"//submodules/AppLockState:AppLockState",
|
||||
"//submodules/NotificationsPresentationData:NotificationsPresentationData",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/UserNotifications.framework",
|
||||
],
|
||||
)
|
||||
|
||||
apple_bundle(
|
||||
name = "NotificationServiceExtension",
|
||||
binary = ":NotificationServiceBinary",
|
||||
extension = "appex",
|
||||
info_plist = "NotificationService/Info.plist",
|
||||
info_plist_substitutions = notification_service_extension_info_plist_substitutions(),
|
||||
deps = [
|
||||
],
|
||||
xcode_product_type = "com.apple.product-type.app-extension",
|
||||
)
|
||||
|
||||
# Intents
|
||||
|
||||
apple_binary(
|
||||
name = "IntentsBinary",
|
||||
srcs = glob([
|
||||
"SiriIntents/**/*.swift",
|
||||
]),
|
||||
configs = intents_extension_configs(),
|
||||
swift_compiler_flags = [
|
||||
"-application-extension",
|
||||
],
|
||||
linker_flags = [
|
||||
"-e",
|
||||
"_NSExtensionMain",
|
||||
"-Xlinker",
|
||||
"-rpath",
|
||||
"-Xlinker",
|
||||
"/usr/lib/swift",
|
||||
"-Xlinker",
|
||||
"-rpath",
|
||||
"-Xlinker",
|
||||
"@executable_path/../../Frameworks",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
|
||||
"//submodules/Postbox:Postbox#shared",
|
||||
"//submodules/TelegramApi:TelegramApi#shared",
|
||||
"//submodules/SyncCore:SyncCore#shared",
|
||||
"//submodules/TelegramCore:TelegramCore#shared",
|
||||
"//submodules/BuildConfig:BuildConfig",
|
||||
"//submodules/OpenSSLEncryptionProvider:OpenSSLEncryptionProvider",
|
||||
],
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/Intents.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/Contacts.framework",
|
||||
],
|
||||
)
|
||||
|
||||
apple_bundle(
|
||||
name = "IntentsExtension",
|
||||
binary = ":IntentsBinary",
|
||||
extension = "appex",
|
||||
info_plist = "SiriIntents/Info.plist",
|
||||
info_plist_substitutions = intents_extension_info_plist_substitutions(),
|
||||
deps = [
|
||||
],
|
||||
xcode_product_type = "com.apple.product-type.app-extension",
|
||||
)
|
||||
|
||||
# Watch
|
||||
|
||||
apple_resource(
|
||||
name = "WatchAppStringResources",
|
||||
files = [],
|
||||
variants = glob([
|
||||
"Telegram-iOS/*.lproj/Localizable.strings",
|
||||
]),
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_resource(
|
||||
name = "WatchAppExtensionResources",
|
||||
files = glob([
|
||||
"Watch/Extension/Resources/**/*",
|
||||
], exclude = ["Watch/Extension/Resources/**/.*"]),
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_binary(
|
||||
name = "WatchAppExtensionBinary",
|
||||
srcs = glob([
|
||||
"Watch/Extension/**/*.m",
|
||||
"Watch/SSignalKit/**/*.m",
|
||||
"Watch/Bridge/**/*.m",
|
||||
"Watch/WatchCommonWatch/**/*.m",
|
||||
]),
|
||||
headers = merge_maps([
|
||||
glob_map(glob([
|
||||
"Watch/Extension/*.h",
|
||||
"Watch/Bridge/*.h",
|
||||
])),
|
||||
glob_sub_map("Watch/Extension/", glob([
|
||||
"Watch/Extension/SSignalKit/*.h",
|
||||
])),
|
||||
glob_sub_map("Watch/", glob([
|
||||
"Watch/WatchCommonWatch/*.h",
|
||||
])),
|
||||
]),
|
||||
compiler_flags = [
|
||||
"-DTARGET_OS_WATCH=1",
|
||||
],
|
||||
linker_flags = [
|
||||
"-e",
|
||||
"_WKExtensionMain",
|
||||
"-lWKExtensionMainLegacy",
|
||||
],
|
||||
configs = watch_extension_binary_configs(),
|
||||
frameworks = [
|
||||
"$SDKROOT/System/Library/Frameworks/UserNotifications.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/CoreLocation.framework",
|
||||
"$SDKROOT/System/Library/Frameworks/CoreGraphics.framework",
|
||||
],
|
||||
deps = [
|
||||
":WatchAppStringResources",
|
||||
":WatchAppExtensionResources",
|
||||
],
|
||||
)
|
||||
|
||||
apple_bundle(
|
||||
name = "WatchAppExtension",
|
||||
binary = ":WatchAppExtensionBinary",
|
||||
extension = "appex",
|
||||
info_plist = "Watch/Extension/Info.plist",
|
||||
info_plist_substitutions = watch_extension_info_plist_substitutions(),
|
||||
xcode_product_type = "com.apple.product-type.watchkit2-extension",
|
||||
)
|
||||
|
||||
apple_resource(
|
||||
name = "WatchAppResources",
|
||||
dirs = [],
|
||||
files = glob(["Watch/Extension/Resources/*.png"])
|
||||
)
|
||||
|
||||
apple_asset_catalog(
|
||||
name = "WatchAppAssets",
|
||||
dirs = [
|
||||
"Watch/App/Assets.xcassets",
|
||||
],
|
||||
app_icon = "AppIcon",
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_resource(
|
||||
name = "WatchAppInterface",
|
||||
files = [
|
||||
"Watch/App/Base.lproj/Interface.storyboard",
|
||||
],
|
||||
visibility = ["PUBLIC"],
|
||||
)
|
||||
|
||||
apple_binary(
|
||||
name = "WatchAppBinary",
|
||||
configs = watch_binary_configs(),
|
||||
deps = [
|
||||
":WatchAppResources",
|
||||
":WatchAppAssets",
|
||||
":WatchAppInterface",
|
||||
":WatchAppStringResources",
|
||||
],
|
||||
)
|
||||
|
||||
apple_bundle(
|
||||
name = "WatchApp",
|
||||
binary = ":WatchAppBinary",
|
||||
visibility = [
|
||||
"//Telegram:...",
|
||||
],
|
||||
extension = "app",
|
||||
info_plist = "Watch/App/Info.plist",
|
||||
info_plist_substitutions = watch_info_plist_substitutions(),
|
||||
xcode_product_type = "com.apple.product-type.application.watchapp2",
|
||||
deps = [
|
||||
":WatchAppExtension",
|
||||
],
|
||||
)
|
||||
|
||||
# Package
|
||||
|
||||
apple_package(
|
||||
name = "AppPackage",
|
||||
bundle = ":Telegram",
|
||||
)
|
||||
|
||||
xcode_workspace_config(
|
||||
name = "workspace",
|
||||
workspace_name = "Telegram_Buck",
|
||||
src_target = ":Telegram",
|
||||
)
|
||||
41
Telegram/NotificationContent/Info.plist
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>NotificationContent</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(PRODUCT_BUNDLE_SHORT_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${BUILD_NUMBER}</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>UNNotificationExtensionCategory</key>
|
||||
<array>
|
||||
<string>withReplyMedia</string>
|
||||
<string>withMuteMedia</string>
|
||||
</array>
|
||||
<key>UNNotificationExtensionInitialContentSizeRatio</key>
|
||||
<real>0.0001</real>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.usernotifications.content-extension</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>NotificationViewController</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.telegram.TelegramHD</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.ph.telegra.Telegraph</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#ifndef Share_Bridging_Header_h
|
||||
#define Share_Bridging_Header_h
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.fork.telegram.Telegram-iOS</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.telegram.Telegram-iOS</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import UIKit
|
||||
import UserNotifications
|
||||
import UserNotificationsUI
|
||||
import TelegramUI
|
||||
import BuildConfig
|
||||
|
||||
@objc(NotificationViewController)
|
||||
@available(iOSApplicationExtension 10.0, *)
|
||||
class NotificationViewController: UIViewController, UNNotificationContentExtension {
|
||||
private var impl: NotificationViewControllerImpl?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
if self.impl == nil {
|
||||
let appBundleIdentifier = Bundle.main.bundleIdentifier!
|
||||
guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
|
||||
return
|
||||
}
|
||||
|
||||
let baseAppBundleId = String(appBundleIdentifier[..<lastDotRange.lowerBound])
|
||||
|
||||
let buildConfig = BuildConfig(baseAppBundleId: baseAppBundleId)
|
||||
|
||||
let languagesCategory = "ios"
|
||||
|
||||
let appGroupName = "group.\(baseAppBundleId)"
|
||||
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
||||
|
||||
guard let appGroupUrl = maybeAppGroupUrl else {
|
||||
return
|
||||
}
|
||||
|
||||
let rootPath = appGroupUrl.path + "/telegram-data"
|
||||
|
||||
let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId)
|
||||
let encryptionParameters: (Data, Data) = (deviceSpecificEncryptionParameters.key, deviceSpecificEncryptionParameters.salt)
|
||||
|
||||
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
|
||||
|
||||
self.impl = NotificationViewControllerImpl(initializationData: NotificationViewControllerInitializationData(appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), setPreferredContentSize: { [weak self] size in
|
||||
self?.preferredContentSize = size
|
||||
})
|
||||
}
|
||||
|
||||
self.impl?.viewDidLoad(view: self.view)
|
||||
}
|
||||
|
||||
func didReceive(_ notification: UNNotification) {
|
||||
self.impl?.didReceive(notification, view: self.view)
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
self.impl?.viewWillTransition(to: size)
|
||||
}
|
||||
}
|
||||
298
Telegram/NotificationService/Api.h
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
/*
|
||||
* Layer 1
|
||||
*/
|
||||
|
||||
@class Api1_Photo;
|
||||
@class Api1_Photo_photoEmpty;
|
||||
@class Api1_Photo_photo;
|
||||
|
||||
@class Api1_PhotoSize;
|
||||
@class Api1_PhotoSize_photoSizeEmpty;
|
||||
@class Api1_PhotoSize_photoSize;
|
||||
@class Api1_PhotoSize_photoCachedSize;
|
||||
@class Api1_PhotoSize_photoStrippedSize;
|
||||
|
||||
@class Api1_FileLocation;
|
||||
@class Api1_FileLocation_fileLocationToBeDeprecated;
|
||||
|
||||
@class Api1_DocumentAttribute;
|
||||
@class Api1_DocumentAttribute_documentAttributeImageSize;
|
||||
@class Api1_DocumentAttribute_documentAttributeAnimated;
|
||||
@class Api1_DocumentAttribute_documentAttributeSticker;
|
||||
@class Api1_DocumentAttribute_documentAttributeVideo;
|
||||
@class Api1_DocumentAttribute_documentAttributeAudio;
|
||||
@class Api1_DocumentAttribute_documentAttributeFilename;
|
||||
@class Api1_DocumentAttribute_documentAttributeHasStickers;
|
||||
|
||||
@class Api1_InputStickerSet;
|
||||
@class Api1_InputStickerSet_inputStickerSetEmpty;
|
||||
@class Api1_InputStickerSet_inputStickerSetID;
|
||||
@class Api1_InputStickerSet_inputStickerSetShortName;
|
||||
|
||||
@class Api1_InputFileLocation;
|
||||
@class Api1_InputFileLocation_inputPhotoFileLocation;
|
||||
@class Api1_InputFileLocation_inputDocumentFileLocation;
|
||||
|
||||
@class Api1_MaskCoords;
|
||||
@class Api1_MaskCoords_maskCoords;
|
||||
|
||||
@class Api1_Document;
|
||||
@class Api1_Document_document;
|
||||
|
||||
|
||||
@interface Api1__Environment : NSObject
|
||||
|
||||
+ (NSData *)serializeObject:(id)object;
|
||||
+ (id)parseObject:(NSData *)data;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_FunctionContext : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSData *payload;
|
||||
@property (nonatomic, copy, readonly) id (^responseParser)(NSData *);
|
||||
@property (nonatomic, strong, readonly) id metadata;
|
||||
|
||||
- (instancetype)initWithPayload:(NSData *)payload responseParser:(id (^)(NSData *))responseParser metadata:(id)metadata;
|
||||
|
||||
@end
|
||||
|
||||
/*
|
||||
* Types 1
|
||||
*/
|
||||
|
||||
@interface Api1_Photo : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * pid;
|
||||
|
||||
+ (Api1_Photo_photoEmpty *)photoEmptyWithPid:(NSNumber *)pid;
|
||||
+ (Api1_Photo_photo *)photoWithFlags:(NSNumber *)flags pid:(NSNumber *)pid accessHash:(NSNumber *)accessHash fileReference:(NSData *)fileReference date:(NSNumber *)date sizes:(NSArray *)sizes dcId:(NSNumber *)dcId;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_Photo_photoEmpty : Api1_Photo
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_Photo_photo : Api1_Photo
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * flags;
|
||||
@property (nonatomic, strong, readonly) NSNumber * accessHash;
|
||||
@property (nonatomic, strong, readonly) NSData * fileReference;
|
||||
@property (nonatomic, strong, readonly) NSNumber * date;
|
||||
@property (nonatomic, strong, readonly) NSArray * sizes;
|
||||
@property (nonatomic, strong, readonly) NSNumber * dcId;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface Api1_PhotoSize : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSString * type;
|
||||
|
||||
+ (Api1_PhotoSize_photoSizeEmpty *)photoSizeEmptyWithType:(NSString *)type;
|
||||
+ (Api1_PhotoSize_photoSize *)photoSizeWithType:(NSString *)type location:(Api1_FileLocation *)location w:(NSNumber *)w h:(NSNumber *)h size:(NSNumber *)size;
|
||||
+ (Api1_PhotoSize_photoCachedSize *)photoCachedSizeWithType:(NSString *)type location:(Api1_FileLocation *)location w:(NSNumber *)w h:(NSNumber *)h bytes:(NSData *)bytes;
|
||||
+ (Api1_PhotoSize_photoStrippedSize *)photoStrippedSizeWithType:(NSString *)type bytes:(NSData *)bytes;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_PhotoSize_photoSizeEmpty : Api1_PhotoSize
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_PhotoSize_photoSize : Api1_PhotoSize
|
||||
|
||||
@property (nonatomic, strong, readonly) Api1_FileLocation * location;
|
||||
@property (nonatomic, strong, readonly) NSNumber * w;
|
||||
@property (nonatomic, strong, readonly) NSNumber * h;
|
||||
@property (nonatomic, strong, readonly) NSNumber * size;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_PhotoSize_photoCachedSize : Api1_PhotoSize
|
||||
|
||||
@property (nonatomic, strong, readonly) Api1_FileLocation * location;
|
||||
@property (nonatomic, strong, readonly) NSNumber * w;
|
||||
@property (nonatomic, strong, readonly) NSNumber * h;
|
||||
@property (nonatomic, strong, readonly) NSData * bytes;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_PhotoSize_photoStrippedSize : Api1_PhotoSize
|
||||
|
||||
@property (nonatomic, strong, readonly) NSData * bytes;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface Api1_FileLocation : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * volumeId;
|
||||
@property (nonatomic, strong, readonly) NSNumber * localId;
|
||||
|
||||
+ (Api1_FileLocation_fileLocationToBeDeprecated *)fileLocationToBeDeprecatedWithVolumeId:(NSNumber *)volumeId localId:(NSNumber *)localId;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_FileLocation_fileLocationToBeDeprecated : Api1_FileLocation
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface Api1_DocumentAttribute : NSObject
|
||||
|
||||
+ (Api1_DocumentAttribute_documentAttributeImageSize *)documentAttributeImageSizeWithW:(NSNumber *)w h:(NSNumber *)h;
|
||||
+ (Api1_DocumentAttribute_documentAttributeAnimated *)documentAttributeAnimated;
|
||||
+ (Api1_DocumentAttribute_documentAttributeSticker *)documentAttributeStickerWithFlags:(NSNumber *)flags alt:(NSString *)alt stickerset:(Api1_InputStickerSet *)stickerset maskCoords:(Api1_MaskCoords *)maskCoords;
|
||||
+ (Api1_DocumentAttribute_documentAttributeVideo *)documentAttributeVideoWithFlags:(NSNumber *)flags duration:(NSNumber *)duration w:(NSNumber *)w h:(NSNumber *)h;
|
||||
+ (Api1_DocumentAttribute_documentAttributeAudio *)documentAttributeAudioWithFlags:(NSNumber *)flags duration:(NSNumber *)duration title:(NSString *)title performer:(NSString *)performer waveform:(NSData *)waveform;
|
||||
+ (Api1_DocumentAttribute_documentAttributeFilename *)documentAttributeFilenameWithFileName:(NSString *)fileName;
|
||||
+ (Api1_DocumentAttribute_documentAttributeHasStickers *)documentAttributeHasStickers;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_DocumentAttribute_documentAttributeImageSize : Api1_DocumentAttribute
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * w;
|
||||
@property (nonatomic, strong, readonly) NSNumber * h;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_DocumentAttribute_documentAttributeAnimated : Api1_DocumentAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_DocumentAttribute_documentAttributeSticker : Api1_DocumentAttribute
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * flags;
|
||||
@property (nonatomic, strong, readonly) NSString * alt;
|
||||
@property (nonatomic, strong, readonly) Api1_InputStickerSet * stickerset;
|
||||
@property (nonatomic, strong, readonly) Api1_MaskCoords * maskCoords;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_DocumentAttribute_documentAttributeVideo : Api1_DocumentAttribute
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * flags;
|
||||
@property (nonatomic, strong, readonly) NSNumber * duration;
|
||||
@property (nonatomic, strong, readonly) NSNumber * w;
|
||||
@property (nonatomic, strong, readonly) NSNumber * h;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_DocumentAttribute_documentAttributeAudio : Api1_DocumentAttribute
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * flags;
|
||||
@property (nonatomic, strong, readonly) NSNumber * duration;
|
||||
@property (nonatomic, strong, readonly) NSString * title;
|
||||
@property (nonatomic, strong, readonly) NSString * performer;
|
||||
@property (nonatomic, strong, readonly) NSData * waveform;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_DocumentAttribute_documentAttributeFilename : Api1_DocumentAttribute
|
||||
|
||||
@property (nonatomic, strong, readonly) NSString * fileName;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_DocumentAttribute_documentAttributeHasStickers : Api1_DocumentAttribute
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface Api1_InputStickerSet : NSObject
|
||||
|
||||
+ (Api1_InputStickerSet_inputStickerSetEmpty *)inputStickerSetEmpty;
|
||||
+ (Api1_InputStickerSet_inputStickerSetID *)inputStickerSetIDWithPid:(NSNumber *)pid accessHash:(NSNumber *)accessHash;
|
||||
+ (Api1_InputStickerSet_inputStickerSetShortName *)inputStickerSetShortNameWithShortName:(NSString *)shortName;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_InputStickerSet_inputStickerSetEmpty : Api1_InputStickerSet
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_InputStickerSet_inputStickerSetID : Api1_InputStickerSet
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * pid;
|
||||
@property (nonatomic, strong, readonly) NSNumber * accessHash;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_InputStickerSet_inputStickerSetShortName : Api1_InputStickerSet
|
||||
|
||||
@property (nonatomic, strong, readonly) NSString * shortName;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface Api1_InputFileLocation : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * pid;
|
||||
@property (nonatomic, strong, readonly) NSNumber * accessHash;
|
||||
@property (nonatomic, strong, readonly) NSData * fileReference;
|
||||
@property (nonatomic, strong, readonly) NSString * thumbSize;
|
||||
|
||||
+ (Api1_InputFileLocation_inputPhotoFileLocation *)inputPhotoFileLocationWithPid:(NSNumber *)pid accessHash:(NSNumber *)accessHash fileReference:(NSData *)fileReference thumbSize:(NSString *)thumbSize;
|
||||
+ (Api1_InputFileLocation_inputDocumentFileLocation *)inputDocumentFileLocationWithPid:(NSNumber *)pid accessHash:(NSNumber *)accessHash fileReference:(NSData *)fileReference thumbSize:(NSString *)thumbSize;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_InputFileLocation_inputPhotoFileLocation : Api1_InputFileLocation
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_InputFileLocation_inputDocumentFileLocation : Api1_InputFileLocation
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface Api1_MaskCoords : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * n;
|
||||
@property (nonatomic, strong, readonly) NSNumber * x;
|
||||
@property (nonatomic, strong, readonly) NSNumber * y;
|
||||
@property (nonatomic, strong, readonly) NSNumber * zoom;
|
||||
|
||||
+ (Api1_MaskCoords_maskCoords *)maskCoordsWithN:(NSNumber *)n x:(NSNumber *)x y:(NSNumber *)y zoom:(NSNumber *)zoom;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_MaskCoords_maskCoords : Api1_MaskCoords
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface Api1_Document : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * flags;
|
||||
@property (nonatomic, strong, readonly) NSNumber * pid;
|
||||
@property (nonatomic, strong, readonly) NSNumber * accessHash;
|
||||
@property (nonatomic, strong, readonly) NSData * fileReference;
|
||||
@property (nonatomic, strong, readonly) NSNumber * date;
|
||||
@property (nonatomic, strong, readonly) NSString * mimeType;
|
||||
@property (nonatomic, strong, readonly) NSNumber * size;
|
||||
@property (nonatomic, strong, readonly) NSArray * thumbs;
|
||||
@property (nonatomic, strong, readonly) NSNumber * dcId;
|
||||
@property (nonatomic, strong, readonly) NSArray * attributes;
|
||||
|
||||
+ (Api1_Document_document *)documentWithFlags:(NSNumber *)flags pid:(NSNumber *)pid accessHash:(NSNumber *)accessHash fileReference:(NSData *)fileReference date:(NSNumber *)date mimeType:(NSString *)mimeType size:(NSNumber *)size thumbs:(NSArray *)thumbs dcId:(NSNumber *)dcId attributes:(NSArray *)attributes;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_Document_document : Api1_Document
|
||||
|
||||
@end
|
||||
|
||||
|
||||
/*
|
||||
* Functions 1
|
||||
*/
|
||||
|
||||
@interface Api1: NSObject
|
||||
|
||||
@end
|
||||
1979
Telegram/NotificationService/Api.m
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import Foundation
|
||||
import ValueBox
|
||||
|
||||
private func applicationSpecificSharedDataKey(_ value: Int32) -> ValueBoxKey {
|
||||
let key = ValueBoxKey(length: 4)
|
||||
key.setInt32(0, value: value + 1000)
|
||||
return key
|
||||
}
|
||||
|
||||
private enum ApplicationSpecificSharedDataKeyValues: Int32 {
|
||||
case inAppNotificationSettings = 0
|
||||
}
|
||||
|
||||
public struct ApplicationSpecificSharedDataKeys {
|
||||
public static let inAppNotificationSettings = applicationSpecificSharedDataKey(ApplicationSpecificSharedDataKeyValues.inAppNotificationSettings.rawValue)
|
||||
}
|
||||
7
Telegram/NotificationService/Attachments.h
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
id _Nullable parseAttachment(NSData * _Nonnull data);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
35
Telegram/NotificationService/Attachments.m
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#import "Attachments.h"
|
||||
|
||||
#ifdef BUCK
|
||||
#import <MTProtoKit/MTProtoKit.h>
|
||||
#else
|
||||
#import <MTProtoKitDynamic/MTProtoKitDynamic.h>
|
||||
#endif
|
||||
|
||||
#import "Api.h"
|
||||
|
||||
id _Nullable parseAttachment(NSData * _Nonnull data) {
|
||||
if (data.length < 4) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
MTInputStream *inputStream = [[MTInputStream alloc] initWithData:data];
|
||||
|
||||
int32_t signature = [inputStream readInt32];
|
||||
|
||||
NSData *dataToParse = nil;
|
||||
if (signature == 0x3072cfa1) {
|
||||
NSData *bytes = [inputStream readBytes];
|
||||
if (bytes != nil) {
|
||||
dataToParse = [MTGzip decompress:bytes];
|
||||
}
|
||||
} else {
|
||||
dataToParse = data;
|
||||
}
|
||||
|
||||
if (dataToParse == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [Api1__Environment parseObject:dataToParse];
|
||||
}
|
||||
11
Telegram/NotificationService/FetchImage.h
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "StoredAccountInfos.h"
|
||||
#import "Api.h"
|
||||
#import <BuildConfig/BuildConfig.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
dispatch_block_t fetchImage(BuildConfig *buildConfig, AccountProxyConnection * _Nullable proxyConnection, StoredAccountInfo *account, Api1_InputFileLocation *inputFileLocation, int32_t datacenterId, void (^_completion)(NSData * _Nullable));
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
220
Telegram/NotificationService/FetchImage.m
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
#import "FetchImage.h"
|
||||
|
||||
#import <MTProtoKit/MTProtoKit.h>
|
||||
#import <EncryptionProvider/EncryptionProvider.h>
|
||||
|
||||
#import "Serialization.h"
|
||||
|
||||
@interface EmptyEncryptionProvider: NSObject <EncryptionProvider>
|
||||
|
||||
@end
|
||||
|
||||
@implementation EmptyEncryptionProvider
|
||||
|
||||
- (id<MTBignumContext>)createBignumContext {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSData * _Nullable)rsaEncryptWithPublicKey:(NSString *)publicKey data:(NSData *)data {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSData * _Nullable)rsaEncryptPKCS1OAEPWithPublicKey:(NSString *)publicKey data:(NSData *)data {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (id<MTRsaPublicKey>)parseRSAPublicKey:(NSString *)publicKey {
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface InMemoryKeychain : NSObject <MTKeychain> {
|
||||
NSMutableDictionary *_dict;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation InMemoryKeychain
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_dict = [[NSMutableDictionary alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setObject:(id)object forKey:(NSString *)aKey group:(NSString *)group {
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object];
|
||||
_dict[[NSString stringWithFormat:@"%@:%@", group, aKey]] = data;
|
||||
}
|
||||
|
||||
- (id)objectForKey:(NSString *)aKey group:(NSString *)group {
|
||||
NSData *data = _dict[[NSString stringWithFormat:@"%@:%@", group, aKey]];
|
||||
if (data != nil) {
|
||||
return [NSKeyedUnarchiver unarchiveObjectWithData:data];
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeObjectForKey:(NSString *)aKey group:(NSString *)group {
|
||||
[_dict removeObjectForKey:[NSString stringWithFormat:@"%@:%@", group, aKey]];
|
||||
}
|
||||
|
||||
- (void)dropGroup:(NSString *)group {
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static void MTLoggingFunction(NSString *string, va_list args) {
|
||||
NSLogv(string, args);
|
||||
}
|
||||
|
||||
@interface ParsedFile : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSData * _Nullable data;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ParsedFile
|
||||
|
||||
- (instancetype)initWithData:(NSData * _Nullable)data {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_data = data;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
dispatch_block_t fetchImage(BuildConfig *buildConfig, AccountProxyConnection * _Nullable proxyConnection, StoredAccountInfo *account, Api1_InputFileLocation *inputFileLocation, int32_t datacenterId, void (^completion)(NSData * _Nullable)) {
|
||||
MTLogSetEnabled(true);
|
||||
MTLogSetLoggingFunction(&MTLoggingFunction);
|
||||
|
||||
Serialization *serialization = [[Serialization alloc] init];
|
||||
|
||||
MTApiEnvironment *apiEnvironment = [[MTApiEnvironment alloc] init];
|
||||
|
||||
apiEnvironment.apiId = buildConfig.apiId;
|
||||
apiEnvironment.langPack = @"ios";
|
||||
apiEnvironment.layer = @([serialization currentLayer]);
|
||||
apiEnvironment.disableUpdates = true;
|
||||
apiEnvironment = [apiEnvironment withUpdatedLangPackCode:@"en"];
|
||||
|
||||
if (proxyConnection != nil) {
|
||||
apiEnvironment = [apiEnvironment withUpdatedSocksProxySettings:[[MTSocksProxySettings alloc] initWithIp:proxyConnection.host port:(uint16_t)proxyConnection.port username:proxyConnection.username password:proxyConnection.password secret:proxyConnection.secret]];
|
||||
}
|
||||
|
||||
MTContext *context = [[MTContext alloc] initWithSerialization:serialization encryptionProvider:[[EmptyEncryptionProvider alloc] init] apiEnvironment:apiEnvironment isTestingEnvironment:account.isTestingEnvironment useTempAuthKeys:false];
|
||||
|
||||
NSDictionary *seedAddressList = @{};
|
||||
|
||||
if (account.isTestingEnvironment) {
|
||||
seedAddressList = @{
|
||||
@(1): @[@"149.154.175.10"],
|
||||
@(2): @[@"149.154.167.40"]
|
||||
};
|
||||
} else {
|
||||
seedAddressList = @{
|
||||
@(1): @[@"149.154.175.50", @"2001:b28:f23d:f001::a"],
|
||||
@(2): @[@"149.154.167.50", @"2001:67c:4e8:f002::a"],
|
||||
@(3): @[@"149.154.175.100", @"2001:b28:f23d:f003::a"],
|
||||
@(4): @[@"149.154.167.91", @"2001:67c:4e8:f004::a"],
|
||||
@(5): @[@"149.154.171.5", @"2001:b28:f23f:f005::a"]
|
||||
};
|
||||
}
|
||||
|
||||
for (NSNumber *datacenterId in seedAddressList) {
|
||||
NSMutableArray *addressList = [[NSMutableArray alloc] init];
|
||||
for (NSString *host in seedAddressList[datacenterId]) {
|
||||
[addressList addObject:[[MTDatacenterAddress alloc] initWithIp:host port:443 preferForMedia:false restrictToTcp:false cdn:false preferForProxy:false secret:nil]];
|
||||
}
|
||||
[context setSeedAddressSetForDatacenterWithId:[datacenterId intValue] seedAddressSet:[[MTDatacenterAddressSet alloc] initWithAddressList:addressList]];
|
||||
}
|
||||
|
||||
InMemoryKeychain *keychain = [[InMemoryKeychain alloc] init];
|
||||
context.keychain = keychain;
|
||||
|
||||
[context performBatchUpdates:^{
|
||||
for (NSNumber *datacenterId in account.datacenters) {
|
||||
AccountDatacenterInfo *info = account.datacenters[datacenterId];
|
||||
if (info.addressList.count != 0) {
|
||||
NSMutableArray *list = [[NSMutableArray alloc] init];
|
||||
for (AccountDatacenterAddress *address in info.addressList) {
|
||||
[list addObject:[[MTDatacenterAddress alloc] initWithIp:address.host port:address.port preferForMedia:address.isMedia restrictToTcp:false cdn:false preferForProxy:address.isProxy secret:address.secret]];
|
||||
}
|
||||
[context updateAddressSetForDatacenterWithId:[datacenterId intValue] addressSet:[[MTDatacenterAddressSet alloc] initWithAddressList:list] forceUpdateSchemes:true];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
for (NSNumber *datacenterId in account.datacenters) {
|
||||
AccountDatacenterInfo *info = account.datacenters[datacenterId];
|
||||
[context updateAuthInfoForDatacenterWithId:[datacenterId intValue] authInfo:[[MTDatacenterAuthInfo alloc] initWithAuthKey:info.masterKey.data authKeyId:info.masterKey.keyId saltSet:@[] authKeyAttributes:@{} mainTempAuthKey:nil mediaTempAuthKey:nil]];
|
||||
}
|
||||
|
||||
MTProto *mtProto = [[MTProto alloc] initWithContext:context datacenterId:datacenterId usageCalculationInfo:nil requiredAuthToken:nil authTokenMasterDatacenterId:0];
|
||||
mtProto.useTempAuthKeys = context.useTempAuthKeys;
|
||||
mtProto.checkForProxyConnectionIssues = false;
|
||||
|
||||
MTRequestMessageService *requestService = [[MTRequestMessageService alloc] initWithContext:context];
|
||||
[mtProto addMessageService:requestService];
|
||||
|
||||
MTRequest *request = [[MTRequest alloc] init];
|
||||
|
||||
MTOutputStream *outputStream = [[MTOutputStream alloc] init];
|
||||
[outputStream writeInt32:-475607115]; //upload.getFile
|
||||
[outputStream writeData:[Api1__Environment serializeObject:inputFileLocation]];
|
||||
|
||||
[outputStream writeInt32:0];
|
||||
[outputStream writeInt32:32 * 1024];
|
||||
|
||||
[request setPayload:[outputStream currentBytes] metadata:@"getFile" shortMetadata:@"getFile" responseParser:^id(NSData *response) {
|
||||
MTInputStream *inputStream = [[MTInputStream alloc] initWithData:response];
|
||||
int32_t signature = [inputStream readInt32];
|
||||
if (signature != 157948117) {
|
||||
return [[ParsedFile alloc] initWithData:nil];
|
||||
}
|
||||
[inputStream readInt32]; //type
|
||||
[inputStream readInt32]; //mtime
|
||||
|
||||
return [[ParsedFile alloc] initWithData:[inputStream readBytes]];
|
||||
}];
|
||||
|
||||
request.dependsOnPasswordEntry = false;
|
||||
request.shouldContinueExecutionWithErrorContext = ^bool (__unused MTRequestErrorContext *errorContext) {
|
||||
return true;
|
||||
};
|
||||
|
||||
request.completed = ^(id boxedResponse, __unused NSTimeInterval completionTimestamp, MTRpcError *error) {
|
||||
if (error != nil) {
|
||||
if (completion) {
|
||||
completion(nil);
|
||||
}
|
||||
} else {
|
||||
if ([boxedResponse isKindOfClass:[ParsedFile class]]) {
|
||||
if (completion) {
|
||||
completion(((ParsedFile *)boxedResponse).data);
|
||||
}
|
||||
} else {
|
||||
if (completion) {
|
||||
completion(nil);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[requestService addRequest:request];
|
||||
[mtProto resume];
|
||||
|
||||
id internalId = request.internalId;
|
||||
return ^{
|
||||
[requestService removeRequestByInternalId:internalId];
|
||||
[context performBatchUpdates:^{
|
||||
}];
|
||||
[mtProto stop];
|
||||
};
|
||||
}
|
||||
105
Telegram/NotificationService/InAppNotificationSettings.swift
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import Foundation
|
||||
import PostboxCoding
|
||||
import PreferencesTable
|
||||
import MessageHistoryMetadataTable
|
||||
import PostboxDataTypes
|
||||
|
||||
public enum TotalUnreadCountDisplayStyle: Int32 {
|
||||
case filtered = 0
|
||||
|
||||
var category: ChatListTotalUnreadStateCategory {
|
||||
switch self {
|
||||
case .filtered:
|
||||
return .filtered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum TotalUnreadCountDisplayCategory: Int32 {
|
||||
case chats = 0
|
||||
case messages = 1
|
||||
|
||||
var statsType: ChatListTotalUnreadStateStats {
|
||||
switch self {
|
||||
case .chats:
|
||||
return .chats
|
||||
case .messages:
|
||||
return .messages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct InAppNotificationSettings: PreferencesEntry, Equatable {
|
||||
public var playSounds: Bool
|
||||
public var vibrate: Bool
|
||||
public var displayPreviews: Bool
|
||||
public var totalUnreadCountDisplayStyle: TotalUnreadCountDisplayStyle
|
||||
public var totalUnreadCountDisplayCategory: TotalUnreadCountDisplayCategory
|
||||
public var totalUnreadCountIncludeTags: PeerSummaryCounterTags
|
||||
public var displayNameOnLockscreen: Bool
|
||||
public var displayNotificationsFromAllAccounts: Bool
|
||||
|
||||
public static var defaultSettings: InAppNotificationSettings {
|
||||
return InAppNotificationSettings(playSounds: true, vibrate: false, displayPreviews: true, totalUnreadCountDisplayStyle: .filtered, totalUnreadCountDisplayCategory: .messages, totalUnreadCountIncludeTags: [.privateChat, .secretChat, .bot, .privateGroup], displayNameOnLockscreen: true, displayNotificationsFromAllAccounts: true)
|
||||
}
|
||||
|
||||
public init(playSounds: Bool, vibrate: Bool, displayPreviews: Bool, totalUnreadCountDisplayStyle: TotalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: TotalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: PeerSummaryCounterTags, displayNameOnLockscreen: Bool, displayNotificationsFromAllAccounts: Bool) {
|
||||
self.playSounds = playSounds
|
||||
self.vibrate = vibrate
|
||||
self.displayPreviews = displayPreviews
|
||||
self.totalUnreadCountDisplayStyle = totalUnreadCountDisplayStyle
|
||||
self.totalUnreadCountDisplayCategory = totalUnreadCountDisplayCategory
|
||||
self.totalUnreadCountIncludeTags = totalUnreadCountIncludeTags
|
||||
self.displayNameOnLockscreen = displayNameOnLockscreen
|
||||
self.displayNotificationsFromAllAccounts = displayNotificationsFromAllAccounts
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.playSounds = decoder.decodeInt32ForKey("s", orElse: 0) != 0
|
||||
self.vibrate = decoder.decodeInt32ForKey("v", orElse: 0) != 0
|
||||
self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0
|
||||
self.totalUnreadCountDisplayStyle = TotalUnreadCountDisplayStyle(rawValue: decoder.decodeInt32ForKey("cds", orElse: 0)) ?? .filtered
|
||||
self.totalUnreadCountDisplayCategory = TotalUnreadCountDisplayCategory(rawValue: decoder.decodeInt32ForKey("totalUnreadCountDisplayCategory", orElse: 1)) ?? .messages
|
||||
if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags_2") {
|
||||
self.totalUnreadCountIncludeTags = PeerSummaryCounterTags(rawValue: value)
|
||||
} else if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags") {
|
||||
var resultTags: PeerSummaryCounterTags = []
|
||||
for legacyTag in LegacyPeerSummaryCounterTags(rawValue: value) {
|
||||
if legacyTag == .regularChatsAndPrivateGroups {
|
||||
resultTags.insert(.privateChat)
|
||||
resultTags.insert(.secretChat)
|
||||
resultTags.insert(.bot)
|
||||
resultTags.insert(.privateGroup)
|
||||
} else if legacyTag == .publicGroups {
|
||||
resultTags.insert(.publicGroup)
|
||||
} else if legacyTag == .channels {
|
||||
resultTags.insert(.channel)
|
||||
}
|
||||
}
|
||||
self.totalUnreadCountIncludeTags = resultTags
|
||||
} else {
|
||||
self.totalUnreadCountIncludeTags = [.privateChat, .secretChat, .bot, .privateGroup]
|
||||
}
|
||||
self.displayNameOnLockscreen = decoder.decodeInt32ForKey("displayNameOnLockscreen", orElse: 1) != 0
|
||||
self.displayNotificationsFromAllAccounts = decoder.decodeInt32ForKey("displayNotificationsFromAllAccounts", orElse: 1) != 0
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeInt32(self.playSounds ? 1 : 0, forKey: "s")
|
||||
encoder.encodeInt32(self.vibrate ? 1 : 0, forKey: "v")
|
||||
encoder.encodeInt32(self.displayPreviews ? 1 : 0, forKey: "p")
|
||||
encoder.encodeInt32(self.totalUnreadCountDisplayStyle.rawValue, forKey: "cds")
|
||||
encoder.encodeInt32(self.totalUnreadCountDisplayCategory.rawValue, forKey: "totalUnreadCountDisplayCategory")
|
||||
encoder.encodeInt32(self.totalUnreadCountIncludeTags.rawValue, forKey: "totalUnreadCountIncludeTags_2")
|
||||
encoder.encodeInt32(self.displayNameOnLockscreen ? 1 : 0, forKey: "displayNameOnLockscreen")
|
||||
encoder.encodeInt32(self.displayNotificationsFromAllAccounts ? 1 : 0, forKey: "displayNotificationsFromAllAccounts")
|
||||
}
|
||||
|
||||
public func isEqual(to: PreferencesEntry) -> Bool {
|
||||
if let to = to as? InAppNotificationSettings {
|
||||
return self == to
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Telegram/NotificationService/Info.plist
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>NotificationService</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(PRODUCT_BUNDLE_SHORT_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${BUILD_NUMBER}</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.usernotifications.service</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>NotificationService</string>
|
||||
</dict>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>10.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
54
Telegram/NotificationService/Namespaces.swift
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import PostboxDataTypes
|
||||
|
||||
struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable {
|
||||
var rawValue: Int32
|
||||
|
||||
init(rawValue: Int32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
static let regularChatsAndPrivateGroups = LegacyPeerSummaryCounterTags(rawValue: 1 << 0)
|
||||
static let publicGroups = LegacyPeerSummaryCounterTags(rawValue: 1 << 1)
|
||||
static let channels = LegacyPeerSummaryCounterTags(rawValue: 1 << 2)
|
||||
|
||||
public func makeIterator() -> AnyIterator<LegacyPeerSummaryCounterTags> {
|
||||
var index = 0
|
||||
return AnyIterator { () -> LegacyPeerSummaryCounterTags? in
|
||||
while index < 31 {
|
||||
let currentTags = self.rawValue >> UInt32(index)
|
||||
let tag = LegacyPeerSummaryCounterTags(rawValue: 1 << UInt32(index))
|
||||
index += 1
|
||||
if currentTags == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if (currentTags & 1) != 0 {
|
||||
return tag
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PeerSummaryCounterTags {
|
||||
static let privateChat = PeerSummaryCounterTags(rawValue: 1 << 3)
|
||||
static let secretChat = PeerSummaryCounterTags(rawValue: 1 << 4)
|
||||
static let privateGroup = PeerSummaryCounterTags(rawValue: 1 << 5)
|
||||
static let bot = PeerSummaryCounterTags(rawValue: 1 << 6)
|
||||
static let channel = PeerSummaryCounterTags(rawValue: 1 << 7)
|
||||
static let publicGroup = PeerSummaryCounterTags(rawValue: 1 << 8)
|
||||
}
|
||||
|
||||
struct Namespaces {
|
||||
struct Message {
|
||||
static let Cloud: Int32 = 0
|
||||
}
|
||||
|
||||
struct Peer {
|
||||
static let CloudUser: Int32 = 0
|
||||
static let CloudGroup: Int32 = 1
|
||||
static let CloudChannel: Int32 = 2
|
||||
static let SecretChat: Int32 = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.telegram.TelegramHD</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.ph.telegra.Telegraph</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
#ifndef NotificationService_BridgingHeader_h
|
||||
#define NotificationService_BridgingHeader_h
|
||||
|
||||
#import "NotificationService.h"
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.fork.telegram.Telegram-iOS</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.telegram.Telegram-iOS</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
17
Telegram/NotificationService/NotificationService.h
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
#import <BuildConfig/BuildConfig.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NotificationServiceImpl : NSObject
|
||||
|
||||
- (instancetype)initWithSerialDispatch:(void (^)(dispatch_block_t))serialDispatch countIncomingMessage:(void (^)(NSString *, int64_t, DeviceSpecificEncryptionParameters *, int64_t, int32_t))countIncomingMessage isLocked:(bool (^)(NSString *))isLocked lockedMessageText:(NSString *(^)(NSString *))lockedMessageText;
|
||||
|
||||
- (void)updateUnreadCount:(int32_t)unreadCount;
|
||||
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler;
|
||||
- (void)serviceExtensionTimeWillExpire;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
482
Telegram/NotificationService/NotificationService.m
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
#import "NotificationService.h"
|
||||
|
||||
#import <mach/mach.h>
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <BuildConfig/BuildConfig.h>
|
||||
|
||||
#ifdef __IPHONE_13_0
|
||||
#import <BackgroundTasks/BackgroundTasks.h>
|
||||
#endif
|
||||
|
||||
#import "StoredAccountInfos.h"
|
||||
#import "Attachments.h"
|
||||
#import "Api.h"
|
||||
#import "FetchImage.h"
|
||||
|
||||
#import "NotificationService-Bridging-Header.h"
|
||||
|
||||
static NSData * _Nullable parseBase64(NSString *string) {
|
||||
string = [string stringByReplacingOccurrencesOfString:@"-" withString:@"+"];
|
||||
string = [string stringByReplacingOccurrencesOfString:@"_" withString:@"/"];
|
||||
while (string.length % 4 != 0) {
|
||||
string = [string stringByAppendingString:@"="];
|
||||
}
|
||||
return [[NSData alloc] initWithBase64EncodedString:string options:0];
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
PeerNamespaceCloudUser = 0,
|
||||
PeerNamespaceCloudGroup = 1,
|
||||
PeerNamespaceCloudChannel = 2,
|
||||
PeerNamespaceSecretChat = 3
|
||||
} PeerNamespace;
|
||||
|
||||
static int64_t makePeerId(int32_t namespace, int32_t value) {
|
||||
return (((int64_t)(namespace)) << 32) | ((int64_t)((uint64_t)((uint32_t)value)));
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
static void reportMemory() {
|
||||
struct task_basic_info info;
|
||||
mach_msg_type_number_t size = TASK_BASIC_INFO_COUNT;
|
||||
kern_return_t kerr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);
|
||||
if (kerr == KERN_SUCCESS) {
|
||||
NSLog(@"Memory in use (in bytes): %lu", info.resident_size);
|
||||
NSLog(@"Memory in use (in MiB): %f", ((CGFloat)info.resident_size / 1048576));
|
||||
} else {
|
||||
NSLog(@"Error with task_info(): %s", mach_error_string(kerr));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@interface NotificationServiceImpl () {
|
||||
void (^_serialDispatch)(dispatch_block_t);
|
||||
void (^_countIncomingMessage)(NSString *, int64_t, DeviceSpecificEncryptionParameters *, int64_t, int32_t);
|
||||
|
||||
NSString * _Nullable _rootPath;
|
||||
DeviceSpecificEncryptionParameters * _Nullable _deviceSpecificEncryptionParameters;
|
||||
bool _isLockedValue;
|
||||
NSString *_lockedMessageTextValue;
|
||||
NSString * _Nullable _baseAppBundleId;
|
||||
void (^_contentHandler)(UNNotificationContent *);
|
||||
UNMutableNotificationContent * _Nullable _bestAttemptContent;
|
||||
void (^_cancelFetch)(void);
|
||||
|
||||
NSNumber * _Nullable _updatedUnreadCount;
|
||||
bool _contentReady;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NotificationServiceImpl
|
||||
|
||||
- (instancetype)initWithSerialDispatch:(void (^)(dispatch_block_t))serialDispatch countIncomingMessage:(void (^)(NSString *, int64_t, DeviceSpecificEncryptionParameters *, int64_t, int32_t))countIncomingMessage isLocked:(nonnull bool (^)(NSString * _Nonnull))isLocked lockedMessageText:(NSString *(^)(NSString *))lockedMessageText {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
#if DEBUG
|
||||
reportMemory();
|
||||
#endif
|
||||
|
||||
_serialDispatch = [serialDispatch copy];
|
||||
_countIncomingMessage = [countIncomingMessage copy];
|
||||
|
||||
NSString *appBundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
|
||||
NSRange lastDotRange = [appBundleIdentifier rangeOfString:@"." options:NSBackwardsSearch];
|
||||
if (lastDotRange.location != NSNotFound) {
|
||||
_baseAppBundleId = [appBundleIdentifier substringToIndex:lastDotRange.location];
|
||||
NSString *appGroupName = [@"group." stringByAppendingString:_baseAppBundleId];
|
||||
NSURL *appGroupUrl = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroupName];
|
||||
|
||||
if (appGroupUrl != nil) {
|
||||
NSString *rootPath = [[appGroupUrl path] stringByAppendingPathComponent:@"telegram-data"];
|
||||
_rootPath = rootPath;
|
||||
if (rootPath != nil) {
|
||||
_deviceSpecificEncryptionParameters = [BuildConfig deviceSpecificEncryptionParameters:rootPath baseAppBundleId:_baseAppBundleId];
|
||||
|
||||
_isLockedValue = isLocked(rootPath);
|
||||
if (_isLockedValue) {
|
||||
_lockedMessageTextValue = lockedMessageText(rootPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NSAssert(false, @"appGroupUrl == nil");
|
||||
}
|
||||
} else {
|
||||
NSAssert(false, @"Invalid bundle id");
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)completeWithBestAttemptContent {
|
||||
_contentReady = true;
|
||||
_updatedUnreadCount = @(-1);
|
||||
if (_contentReady && _updatedUnreadCount) {
|
||||
[self _internalComplete];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateUnreadCount:(int32_t)unreadCount {
|
||||
_updatedUnreadCount = @(unreadCount);
|
||||
if (_contentReady && _updatedUnreadCount) {
|
||||
[self _internalComplete];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_internalComplete {
|
||||
#if DEBUG
|
||||
reportMemory();
|
||||
#endif
|
||||
|
||||
NSString *baseAppBundleId = _baseAppBundleId;
|
||||
void (^contentHandler)(UNNotificationContent *) = [_contentHandler copy];
|
||||
UNMutableNotificationContent *bestAttemptContent = _bestAttemptContent;
|
||||
NSNumber *updatedUnreadCount = updatedUnreadCount;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
#ifdef __IPHONE_13_0
|
||||
if (baseAppBundleId != nil && false) {
|
||||
BGAppRefreshTaskRequest *request = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:[baseAppBundleId stringByAppendingString:@".refresh"]];
|
||||
request.earliestBeginDate = nil;
|
||||
NSError *error = nil;
|
||||
[[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error];
|
||||
if (error != nil) {
|
||||
NSLog(@"Error: %@", error);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (updatedUnreadCount != nil) {
|
||||
int32_t unreadCount = (int32_t)[updatedUnreadCount intValue];
|
||||
if (unreadCount > 0) {
|
||||
bestAttemptContent.badge = @(unreadCount);
|
||||
}
|
||||
}
|
||||
contentHandler(bestAttemptContent);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
|
||||
if (_rootPath == nil) {
|
||||
_bestAttemptContent = request.content;
|
||||
[self completeWithBestAttemptContent];
|
||||
return;
|
||||
}
|
||||
|
||||
_contentHandler = [contentHandler copy];
|
||||
_bestAttemptContent = (UNMutableNotificationContent *)[request.content mutableCopy];
|
||||
|
||||
NSString * _Nullable encryptedPayload = request.content.userInfo[@"p"];
|
||||
NSData * _Nullable encryptedData = nil;
|
||||
if (encryptedPayload != nil && [encryptedPayload isKindOfClass:[NSString class]]) {
|
||||
encryptedData = parseBase64(encryptedPayload);
|
||||
}
|
||||
|
||||
StoredAccountInfos * _Nullable accountInfos = [StoredAccountInfos loadFromPath:[_rootPath stringByAppendingPathComponent:@"accounts-shared-data"]];
|
||||
|
||||
int selectedAccountIndex = -1;
|
||||
NSDictionary *decryptedPayload = decryptedNotificationPayload(accountInfos.accounts, encryptedData, &selectedAccountIndex);
|
||||
|
||||
if (decryptedPayload != nil && selectedAccountIndex != -1) {
|
||||
StoredAccountInfo *account = accountInfos.accounts[selectedAccountIndex];
|
||||
|
||||
NSMutableDictionary *userInfo = nil;
|
||||
if (_bestAttemptContent.userInfo != nil) {
|
||||
userInfo = [[NSMutableDictionary alloc] initWithDictionary:_bestAttemptContent.userInfo];
|
||||
} else {
|
||||
userInfo = [[NSMutableDictionary alloc] init];
|
||||
}
|
||||
userInfo[@"accountId"] = @(account.accountId);
|
||||
|
||||
int64_t peerId = 0;
|
||||
int32_t messageId = 0;
|
||||
bool silent = false;
|
||||
|
||||
NSString *messageIdString = decryptedPayload[@"msg_id"];
|
||||
if ([messageIdString isKindOfClass:[NSString class]]) {
|
||||
userInfo[@"msg_id"] = messageIdString;
|
||||
messageId = [messageIdString intValue];
|
||||
}
|
||||
|
||||
NSString *fromIdString = decryptedPayload[@"from_id"];
|
||||
if ([fromIdString isKindOfClass:[NSString class]]) {
|
||||
userInfo[@"from_id"] = fromIdString;
|
||||
peerId = makePeerId(PeerNamespaceCloudUser, [fromIdString intValue]);
|
||||
}
|
||||
|
||||
NSString *chatIdString = decryptedPayload[@"chat_id"];
|
||||
if ([chatIdString isKindOfClass:[NSString class]]) {
|
||||
userInfo[@"chat_id"] = chatIdString;
|
||||
peerId = makePeerId(PeerNamespaceCloudGroup, [chatIdString intValue]);
|
||||
}
|
||||
|
||||
NSString *channelIdString = decryptedPayload[@"channel_id"];
|
||||
if ([channelIdString isKindOfClass:[NSString class]]) {
|
||||
userInfo[@"channel_id"] = channelIdString;
|
||||
peerId = makePeerId(PeerNamespaceCloudChannel, [channelIdString intValue]);
|
||||
}
|
||||
|
||||
/*if (_countIncomingMessage && _deviceSpecificEncryptionParameters) {
|
||||
_countIncomingMessage(_rootPath, account.accountId, _deviceSpecificEncryptionParameters, peerId, messageId);
|
||||
}*/
|
||||
|
||||
NSString *silentString = decryptedPayload[@"silent"];
|
||||
if ([silentString isKindOfClass:[NSString class]]) {
|
||||
silent = [silentString intValue] != 0;
|
||||
}
|
||||
|
||||
NSData *attachmentData = nil;
|
||||
id parsedAttachment = nil;
|
||||
|
||||
if (!_isLockedValue) {
|
||||
NSString *attachmentDataString = decryptedPayload[@"attachb64"];
|
||||
if ([attachmentDataString isKindOfClass:[NSString class]]) {
|
||||
attachmentData = parseBase64(attachmentDataString);
|
||||
if (attachmentData != nil) {
|
||||
parsedAttachment = parseAttachment(attachmentData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSString *imagesPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"aps-data"];
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:imagesPath withIntermediateDirectories:true attributes:nil error:nil];
|
||||
NSString *accountBasePath = [_rootPath stringByAppendingPathComponent:[NSString stringWithFormat:@"account-%llud", account.accountId]];
|
||||
|
||||
NSString *mediaBoxPath = [accountBasePath stringByAppendingPathComponent:@"/postbox/media"];
|
||||
|
||||
NSString *tempImagePath = nil;
|
||||
NSString *mediaBoxThumbnailImagePath = nil;
|
||||
|
||||
int32_t fileDatacenterId = 0;
|
||||
Api1_InputFileLocation *inputFileLocation = nil;
|
||||
|
||||
NSString *fetchResourceId = nil;
|
||||
bool isPng = false;
|
||||
bool isExpandableMedia = false;
|
||||
|
||||
if (parsedAttachment != nil) {
|
||||
if ([parsedAttachment isKindOfClass:[Api1_Photo_photo class]]) {
|
||||
Api1_Photo_photo *photo = parsedAttachment;
|
||||
isExpandableMedia = true;
|
||||
for (id size in photo.sizes) {
|
||||
if ([size isKindOfClass:[Api1_PhotoSize_photoSize class]]) {
|
||||
Api1_PhotoSize_photoSize *sizeValue = size;
|
||||
if ([sizeValue.type isEqualToString:@"m"]) {
|
||||
inputFileLocation = [Api1_InputFileLocation inputPhotoFileLocationWithPid:photo.pid accessHash:photo.accessHash fileReference:photo.fileReference thumbSize:sizeValue.type];
|
||||
fileDatacenterId = [photo.dcId intValue];
|
||||
fetchResourceId = [NSString stringWithFormat:@"telegram-cloud-photo-size-%@-%@-%@", photo.dcId, photo.pid, sizeValue.type];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ([parsedAttachment isKindOfClass:[Api1_Document_document class]]) {
|
||||
Api1_Document_document *document = parsedAttachment;
|
||||
|
||||
bool isSticker = false;
|
||||
for (id attribute in document.attributes) {
|
||||
if ([attribute isKindOfClass:[Api1_DocumentAttribute_documentAttributeSticker class]]) {
|
||||
isSticker = true;
|
||||
}
|
||||
}
|
||||
bool isAnimatedSticker = [document.mimeType isEqualToString:@"application/x-tgsticker"];
|
||||
if (isSticker || isAnimatedSticker) {
|
||||
isExpandableMedia = true;
|
||||
}
|
||||
for (id size in document.thumbs) {
|
||||
if ([size isKindOfClass:[Api1_PhotoSize_photoSize class]]) {
|
||||
Api1_PhotoSize_photoSize *photoSize = size;
|
||||
if ((isSticker && [photoSize.type isEqualToString:@"s"]) || [photoSize.type isEqualToString:@"m"]) {
|
||||
if (isSticker) {
|
||||
isPng = true;
|
||||
}
|
||||
inputFileLocation = [Api1_InputFileLocation inputDocumentFileLocationWithPid:document.pid accessHash:document.accessHash fileReference:document.fileReference thumbSize:photoSize.type];
|
||||
fileDatacenterId = [document.dcId intValue];
|
||||
fetchResourceId = [NSString stringWithFormat:@"telegram-cloud-document-size-%@-%@-%@", document.dcId, document.pid, photoSize.type];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fetchResourceId != nil) {
|
||||
tempImagePath = [imagesPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", fetchResourceId, isPng ? @"png" : @"jpg"]];
|
||||
mediaBoxThumbnailImagePath = [mediaBoxPath stringByAppendingPathComponent:fetchResourceId];
|
||||
}
|
||||
|
||||
NSDictionary *aps = decryptedPayload[@"aps"];
|
||||
if ([aps isKindOfClass:[NSDictionary class]]) {
|
||||
id alert = aps[@"alert"];
|
||||
if ([alert isKindOfClass:[NSDictionary class]]) {
|
||||
NSDictionary *alertDict = alert;
|
||||
NSString *title = alertDict[@"title"];
|
||||
NSString *subtitle = alertDict[@"subtitle"];
|
||||
NSString *body = alertDict[@"body"];
|
||||
if (![title isKindOfClass:[NSString class]]) {
|
||||
title = @"";
|
||||
}
|
||||
if (![subtitle isKindOfClass:[NSString class]]) {
|
||||
subtitle = @"";
|
||||
}
|
||||
if (![body isKindOfClass:[NSString class]]) {
|
||||
body = nil;
|
||||
}
|
||||
if (title.length != 0 && silent) {
|
||||
title = [title stringByAppendingString:@" 🔕"];
|
||||
}
|
||||
_bestAttemptContent.title = title;
|
||||
if (_isLockedValue) {
|
||||
_bestAttemptContent.title = @"";
|
||||
_bestAttemptContent.subtitle = @"";
|
||||
if (_lockedMessageTextValue != nil) {
|
||||
_bestAttemptContent.body = _lockedMessageTextValue;
|
||||
} else {
|
||||
_bestAttemptContent.body = @"^You have a new message";
|
||||
}
|
||||
} else {
|
||||
_bestAttemptContent.subtitle = subtitle;
|
||||
_bestAttemptContent.body = body;
|
||||
}
|
||||
} else if ([alert isKindOfClass:[NSString class]]) {
|
||||
_bestAttemptContent.title = @"";
|
||||
_bestAttemptContent.subtitle = @"";
|
||||
if (_isLockedValue) {
|
||||
if (_lockedMessageTextValue != nil) {
|
||||
_bestAttemptContent.body = _lockedMessageTextValue;
|
||||
} else {
|
||||
_bestAttemptContent.body = @"^You have a new message";
|
||||
}
|
||||
} else {
|
||||
_bestAttemptContent.body = alert;
|
||||
}
|
||||
}
|
||||
|
||||
if (_isLockedValue) {
|
||||
_bestAttemptContent.threadIdentifier = @"locked";
|
||||
} else {
|
||||
NSString *threadIdString = aps[@"thread-id"];
|
||||
if ([threadIdString isKindOfClass:[NSString class]]) {
|
||||
_bestAttemptContent.threadIdentifier = threadIdString;
|
||||
}
|
||||
}
|
||||
NSString *soundString = aps[@"sound"];
|
||||
if ([soundString isKindOfClass:[NSString class]]) {
|
||||
_bestAttemptContent.sound = [UNNotificationSound soundNamed:soundString];
|
||||
}
|
||||
if (_isLockedValue) {
|
||||
_bestAttemptContent.categoryIdentifier = @"locked";
|
||||
} else {
|
||||
NSString *categoryString = aps[@"category"];
|
||||
if ([categoryString isKindOfClass:[NSString class]]) {
|
||||
_bestAttemptContent.categoryIdentifier = categoryString;
|
||||
if (peerId != 0 && messageId != 0 && parsedAttachment != nil && attachmentData != nil) {
|
||||
userInfo[@"peerId"] = @(peerId);
|
||||
userInfo[@"messageId.namespace"] = @(0);
|
||||
userInfo[@"messageId.id"] = @(messageId);
|
||||
|
||||
userInfo[@"media"] = [attachmentData base64EncodedStringWithOptions:0];
|
||||
|
||||
if (isExpandableMedia) {
|
||||
if ([categoryString isEqualToString:@"r"]) {
|
||||
_bestAttemptContent.categoryIdentifier = @"withReplyMedia";
|
||||
} else if ([categoryString isEqualToString:@"m"]) {
|
||||
_bestAttemptContent.categoryIdentifier = @"withMuteMedia";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (accountInfos.accounts.count > 1) {
|
||||
if (_bestAttemptContent.title.length != 0 && account.peerName.length != 0) {
|
||||
_bestAttemptContent.title = [NSString stringWithFormat:@"%@ → %@", _bestAttemptContent.title, account.peerName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_bestAttemptContent.userInfo = userInfo;
|
||||
|
||||
if (_cancelFetch) {
|
||||
_cancelFetch();
|
||||
_cancelFetch = nil;
|
||||
}
|
||||
|
||||
if (mediaBoxThumbnailImagePath != nil && tempImagePath != nil && inputFileLocation != nil) {
|
||||
NSData *data = [NSData dataWithContentsOfFile:mediaBoxThumbnailImagePath];
|
||||
if (data != nil) {
|
||||
NSData *tempData = data;
|
||||
if (isPng) {
|
||||
/*if let image = WebP.convert(fromWebP: data), let imageData = image.pngData() {
|
||||
tempData = imageData
|
||||
}*/
|
||||
}
|
||||
if ([tempData writeToFile:tempImagePath atomically:true]) {
|
||||
UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"image" URL:[NSURL fileURLWithPath:tempImagePath] options:nil error:nil];
|
||||
if (attachment != nil) {
|
||||
_bestAttemptContent.attachments = @[attachment];
|
||||
}
|
||||
}
|
||||
[self completeWithBestAttemptContent];
|
||||
} else {
|
||||
BuildConfig *buildConfig = [[BuildConfig alloc] initWithBaseAppBundleId:_baseAppBundleId];
|
||||
|
||||
void (^serialDispatch)(dispatch_block_t) = _serialDispatch;
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
_cancelFetch = fetchImage(buildConfig, accountInfos.proxy, account, inputFileLocation, fileDatacenterId, ^(NSData * _Nullable data) {
|
||||
serialDispatch(^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (strongSelf == nil) {
|
||||
return;
|
||||
}
|
||||
if (strongSelf->_cancelFetch) {
|
||||
strongSelf->_cancelFetch();
|
||||
strongSelf->_cancelFetch = nil;
|
||||
|
||||
if (data != nil) {
|
||||
[data writeToFile:mediaBoxThumbnailImagePath atomically:true];
|
||||
NSData *tempData = data;
|
||||
if (isPng) {
|
||||
/*if let image = WebP.convert(fromWebP: data), let imageData = image.pngData() {
|
||||
tempData = imageData
|
||||
}*/
|
||||
}
|
||||
if ([tempData writeToFile:tempImagePath atomically:true]) {
|
||||
UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"image" URL:[NSURL fileURLWithPath:tempImagePath] options:nil error:nil];
|
||||
if (attachment != nil) {
|
||||
strongSelf->_bestAttemptContent.attachments = @[attachment];
|
||||
}
|
||||
}
|
||||
}
|
||||
[strongSelf completeWithBestAttemptContent];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
[self completeWithBestAttemptContent];
|
||||
}
|
||||
} else {
|
||||
[self completeWithBestAttemptContent];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)serviceExtensionTimeWillExpire {
|
||||
if (_cancelFetch) {
|
||||
_cancelFetch();
|
||||
_cancelFetch = nil;
|
||||
}
|
||||
|
||||
if (_contentHandler) {
|
||||
if(_bestAttemptContent) {
|
||||
if (_updatedUnreadCount == nil) {
|
||||
_updatedUnreadCount = @(-1);
|
||||
}
|
||||
[self completeWithBestAttemptContent];
|
||||
}
|
||||
_contentHandler = nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
52
Telegram/NotificationService/NotificationService.swift
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import Foundation
|
||||
import UserNotifications
|
||||
import SwiftSignalKit
|
||||
|
||||
private let queue = Queue()
|
||||
|
||||
@available(iOSApplicationExtension 10.0, *)
|
||||
@objc(NotificationService)
|
||||
final class NotificationService: UNNotificationServiceExtension {
|
||||
private let impl: QueueLocalObject<NotificationServiceImpl>
|
||||
|
||||
override init() {
|
||||
self.impl = QueueLocalObject(queue: queue, generate: {
|
||||
var completion: ((Int32) -> Void)?
|
||||
let impl = NotificationServiceImpl(serialDispatch: { f in
|
||||
queue.async {
|
||||
f()
|
||||
}
|
||||
}, countIncomingMessage: { rootPath, accountId, encryptionParameters, peerId, messageId in
|
||||
SyncProviderImpl.addIncomingMessage(queue: queue, withRootPath: rootPath, accountId: accountId, encryptionParameters: encryptionParameters, peerId: peerId, messageId: messageId, completion: { count in
|
||||
completion?(count)
|
||||
})
|
||||
}, isLocked: { rootPath in
|
||||
return SyncProviderImpl.isLocked(withRootPath: rootPath)
|
||||
}, lockedMessageText: { rootPath in
|
||||
return SyncProviderImpl.lockedMessageText(withRootPath: rootPath)
|
||||
})
|
||||
|
||||
completion = { [weak impl] count in
|
||||
queue.async {
|
||||
impl?.updateUnreadCount(count)
|
||||
}
|
||||
}
|
||||
|
||||
return impl
|
||||
})
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
self.impl.with { impl in
|
||||
impl.didReceive(request, withContentHandler: contentHandler)
|
||||
}
|
||||
}
|
||||
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
self.impl.with { impl in
|
||||
impl.serviceExtensionTimeWillExpire()
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Telegram/NotificationService/Serialization.h
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
#ifdef BUCK
|
||||
#import <MTProtoKit/MTProtoKit.h>
|
||||
#else
|
||||
#import <MTProtoKitDynamic/MTProtoKitDynamic.h>
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface Serialization : NSObject <MTSerialization>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
35
Telegram/NotificationService/Serialization.m
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#import "Serialization.h"
|
||||
|
||||
@implementation Serialization
|
||||
|
||||
- (NSUInteger)currentLayer {
|
||||
return 110;
|
||||
}
|
||||
|
||||
- (id _Nullable)parseMessage:(NSData * _Nullable)data {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (MTExportAuthorizationResponseParser _Nonnull)exportAuthorization:(int32_t)datacenterId data:(__autoreleasing NSData **)data {
|
||||
return ^MTExportedAuthorizationData *(NSData *resultData) {
|
||||
return nil;
|
||||
};
|
||||
}
|
||||
|
||||
- (NSData * _Nonnull)importAuthorization:(int32_t)authId bytes:(NSData * _Nonnull)bytes {
|
||||
return [NSData data];
|
||||
}
|
||||
|
||||
- (MTRequestDatacenterAddressListParser)requestDatacenterAddressWithData:(__autoreleasing NSData **)data {
|
||||
return ^MTDatacenterAddressListData *(NSData *resultData) {
|
||||
return nil;
|
||||
};
|
||||
}
|
||||
|
||||
- (MTRequestNoopParser)requestNoop:(__autoreleasing NSData **)data {
|
||||
return ^id(NSData *resultData) {
|
||||
return nil;
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
67
Telegram/NotificationService/StoredAccountInfos.h
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AccountNotificationKey: NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSData *keyId;
|
||||
@property (nonatomic, strong, readonly) NSData *data;
|
||||
|
||||
@end
|
||||
|
||||
@interface AccountDatacenterKey: NSObject
|
||||
|
||||
@property (nonatomic, readonly) int64_t keyId;
|
||||
@property (nonatomic, strong, readonly) NSData *data;
|
||||
|
||||
@end
|
||||
|
||||
@interface AccountDatacenterAddress: NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSString *host;
|
||||
@property (nonatomic, readonly) int32_t port;
|
||||
@property (nonatomic, readonly) bool isMedia;
|
||||
@property (nonatomic, strong, readonly) NSData * _Nullable secret;
|
||||
|
||||
@end
|
||||
|
||||
@interface AccountDatacenterInfo: NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) AccountDatacenterKey *masterKey;
|
||||
@property (nonatomic, strong, readonly) NSArray<AccountDatacenterAddress *> *addressList;
|
||||
|
||||
@end
|
||||
|
||||
@interface AccountProxyConnection: NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSString *host;
|
||||
@property (nonatomic, readonly) int32_t port;
|
||||
@property (nonatomic, strong) NSString * _Nullable username;
|
||||
@property (nonatomic, strong) NSString * _Nullable password;
|
||||
@property (nonatomic, strong) NSData * _Nullable secret;
|
||||
|
||||
@end
|
||||
|
||||
@interface StoredAccountInfo : NSObject
|
||||
|
||||
@property (nonatomic, readonly) int64_t accountId;
|
||||
@property (nonatomic, readonly) int32_t primaryId;
|
||||
@property (nonatomic, readonly) bool isTestingEnvironment;
|
||||
@property (nonatomic, strong, readonly) NSString *peerName;
|
||||
@property (nonatomic, strong, readonly) NSDictionary<NSNumber *, AccountDatacenterInfo *> *datacenters;
|
||||
@property (nonatomic, strong, readonly) AccountNotificationKey *notificationKey;
|
||||
|
||||
@end
|
||||
|
||||
@interface StoredAccountInfos : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) AccountProxyConnection * _Nullable proxy;
|
||||
@property (nonatomic, strong, readonly) NSArray<StoredAccountInfo *> *accounts;
|
||||
|
||||
+ (StoredAccountInfos * _Nullable)loadFromPath:(NSString *)path;
|
||||
|
||||
@end
|
||||
|
||||
NSDictionary * _Nullable decryptedNotificationPayload(NSArray<StoredAccountInfo *> *accounts, NSData *data, int *selectedAccountIndex);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
417
Telegram/NotificationService/StoredAccountInfos.m
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
#import "StoredAccountInfos.h"
|
||||
|
||||
#ifdef BUCK
|
||||
#import <MTProtoKit/MTProtoKit.h>
|
||||
#else
|
||||
#import <MTProtoKitDynamic/MTProtoKitDynamic.h>
|
||||
#endif
|
||||
|
||||
#import <CommonCrypto/CommonDigest.h>
|
||||
|
||||
@implementation AccountNotificationKey
|
||||
|
||||
- (instancetype)initWithKeyId:(NSData *)keyId data:(NSData *)data {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_keyId = keyId;
|
||||
_data = data;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype _Nullable)parse:(NSDictionary *)dict {
|
||||
NSString *keyIdString = dict[@"id"];
|
||||
NSData *keyId = nil;
|
||||
if ([keyIdString isKindOfClass:[NSString class]]) {
|
||||
keyId = [[NSData alloc] initWithBase64EncodedString:keyIdString options:0];
|
||||
}
|
||||
if (keyId == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *dataString = dict[@"data"];
|
||||
NSData *data = nil;
|
||||
if ([dataString isKindOfClass:[NSString class]]) {
|
||||
data = [[NSData alloc] initWithBase64EncodedString:dataString options:0];
|
||||
}
|
||||
if (data == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [[AccountNotificationKey alloc] initWithKeyId:keyId data:data];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation AccountDatacenterKey
|
||||
|
||||
- (instancetype)initWithKeyId:(int64_t)keyId data:(NSData *)data {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_keyId = keyId;
|
||||
_data = data;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype _Nullable)parse:(NSDictionary *)dict {
|
||||
NSNumber *keyIdNumber = dict[@"id"];
|
||||
if (![keyIdNumber isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
int64_t keyId = [keyIdNumber longLongValue];
|
||||
|
||||
NSString *dataString = dict[@"data"];
|
||||
NSData *data = nil;
|
||||
if ([dataString isKindOfClass:[NSString class]]) {
|
||||
data = [[NSData alloc] initWithBase64EncodedString:dataString options:0];
|
||||
}
|
||||
if (data == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [[AccountDatacenterKey alloc] initWithKeyId:keyId data:data];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation AccountDatacenterAddress
|
||||
|
||||
- (instancetype)initWithHost:(NSString *)host port:(int32_t)port isMedia:(bool)isMedia secret:(NSData * _Nullable)secret {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_host = host;
|
||||
_port = port;
|
||||
_isMedia = isMedia;
|
||||
_secret = secret;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype _Nullable)parse:(NSDictionary *)dict {
|
||||
NSString *hostString = dict[@"host"];
|
||||
if (![hostString isKindOfClass:[NSString class]]) {
|
||||
return nil;
|
||||
}
|
||||
NSString *host = hostString;
|
||||
|
||||
NSNumber *portNumber = dict[@"port"];
|
||||
if (![portNumber isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
int32_t port = [portNumber intValue];
|
||||
|
||||
NSNumber *isMediaNumber = dict[@"isMedia"];
|
||||
if (![isMediaNumber isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
bool isMedia = [isMediaNumber intValue] != 0;
|
||||
|
||||
NSString *secretString = dict[@"secret"];
|
||||
NSData *secret = nil;
|
||||
if ([secretString isKindOfClass:[NSString class]]) {
|
||||
secret = [[NSData alloc] initWithBase64EncodedString:secretString options:0];
|
||||
}
|
||||
|
||||
return [[AccountDatacenterAddress alloc] initWithHost:host port:port isMedia:isMedia secret:secret];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation AccountDatacenterInfo
|
||||
|
||||
- (instancetype)initWithMasterKey:(AccountDatacenterKey *)masterKey addressList:(NSArray<AccountDatacenterAddress *> *)addressList {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_masterKey = masterKey;
|
||||
_addressList = addressList;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype _Nullable)parse:(NSDictionary *)dict {
|
||||
NSDictionary *masterKeyDict = dict[@"masterKey"];
|
||||
if (![masterKeyDict isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
AccountDatacenterKey *masterKey = [AccountDatacenterKey parse:masterKeyDict];
|
||||
if (masterKey == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSArray *addressListArray = dict[@"addressList"];
|
||||
if (![addressListArray isKindOfClass:[NSArray class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray<AccountDatacenterAddress *> *addressList = [[NSMutableArray alloc] init];
|
||||
for (NSDictionary *addressListItem in addressListArray) {
|
||||
if (![addressListItem isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
AccountDatacenterAddress *address = [AccountDatacenterAddress parse:addressListItem];
|
||||
if (address == nil) {
|
||||
return nil;
|
||||
}
|
||||
[addressList addObject:address];
|
||||
}
|
||||
|
||||
return [[AccountDatacenterInfo alloc] initWithMasterKey:masterKey addressList:addressList];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation AccountProxyConnection
|
||||
|
||||
- (instancetype)initWithHost:(NSString *)host port:(int32_t)port username:(NSString * _Nullable)username password:(NSString * _Nullable)password secret:(NSData * _Nullable)secret {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_host = host;
|
||||
_port = port;
|
||||
username = _username;
|
||||
password = _password;
|
||||
secret = _secret;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype _Nullable)parse:(NSDictionary *)dict {
|
||||
NSString *hostString = dict[@"host"];
|
||||
if (![hostString isKindOfClass:[NSString class]]) {
|
||||
return nil;
|
||||
}
|
||||
NSString *host = hostString;
|
||||
|
||||
NSNumber *portNumber = dict[@"port"];
|
||||
if (![portNumber isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
int32_t port = [portNumber intValue];
|
||||
|
||||
NSString *usernameString = dict[@"username"];
|
||||
NSString *username = nil;
|
||||
if ([usernameString isKindOfClass:[NSString class]]) {
|
||||
username = usernameString;
|
||||
}
|
||||
|
||||
NSString *passwordString = dict[@"password"];
|
||||
NSString *password = nil;
|
||||
if ([passwordString isKindOfClass:[NSString class]]) {
|
||||
password = passwordString;
|
||||
}
|
||||
|
||||
NSString *secretString = dict[@"secret"];
|
||||
NSData *secret = nil;
|
||||
if ([secretString isKindOfClass:[NSString class]]) {
|
||||
secret = [[NSData alloc] initWithBase64EncodedString:secretString options:0];
|
||||
}
|
||||
|
||||
return [[AccountProxyConnection alloc] initWithHost:host port:port username:username password:password secret:secret];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation StoredAccountInfo
|
||||
|
||||
- (instancetype)initWithAccountId:(int64_t)accountId primaryId:(int32_t)primaryId isTestingEnvironment:(bool)isTestingEnvironment peerName:(NSString *)peerName datacenters:(NSDictionary<NSNumber *, AccountDatacenterInfo *> *)datacenters notificationKey:(AccountNotificationKey *)notificationKey {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_accountId = accountId;
|
||||
_primaryId = primaryId;
|
||||
_isTestingEnvironment = isTestingEnvironment;
|
||||
_peerName = peerName;
|
||||
_datacenters = datacenters;
|
||||
_notificationKey = notificationKey;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype _Nullable)parse:(NSDictionary *)dict {
|
||||
NSNumber *idNumber = dict[@"id"];
|
||||
if (![idNumber isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
int64_t accountId = [idNumber longLongValue];
|
||||
|
||||
NSNumber *primaryIdNumber = dict[@"primaryId"];
|
||||
if (![primaryIdNumber isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
int32_t primaryId = [primaryIdNumber intValue];
|
||||
|
||||
NSNumber *isTestingEnvironmentNumber = dict[@"isTestingEnvironment"];
|
||||
if (![isTestingEnvironmentNumber isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
bool isTestingEnvironment = [isTestingEnvironmentNumber intValue] != 0;
|
||||
|
||||
NSString *peerNameString = dict[@"peerName"];
|
||||
if (![peerNameString isKindOfClass:[NSString class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *peerName = peerNameString;
|
||||
|
||||
NSArray *datacentersArray = dict[@"datacenters"];
|
||||
if (![datacentersArray isKindOfClass:[NSArray class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableDictionary<NSNumber *, AccountDatacenterInfo *> *datacenters = [[NSMutableDictionary alloc] init];
|
||||
|
||||
for (NSInteger i = 0; i < datacentersArray.count; i += 2) {
|
||||
NSNumber *datacenterKey = datacentersArray[i];
|
||||
NSDictionary *datacenterData = datacentersArray[i + 1];
|
||||
|
||||
if (![datacenterKey isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
if (![datacenterData isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
AccountDatacenterInfo *parsedDatacenter = [AccountDatacenterInfo parse:datacenterData];
|
||||
if (parsedDatacenter != nil) {
|
||||
datacenters[datacenterKey] = parsedDatacenter;
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary *notificationKeyDict = dict[@"notificationKey"];
|
||||
if (![notificationKeyDict isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
AccountNotificationKey *notificationKey = [AccountNotificationKey parse:notificationKeyDict];
|
||||
if (notificationKey == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [[StoredAccountInfo alloc] initWithAccountId:accountId primaryId:primaryId isTestingEnvironment:isTestingEnvironment peerName:peerName datacenters:datacenters notificationKey:notificationKey];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation StoredAccountInfos
|
||||
|
||||
- (instancetype)initWithProxy:(AccountProxyConnection * _Nullable)proxy accounts:(NSArray<StoredAccountInfo *> *)accounts {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_proxy = proxy;
|
||||
_accounts = accounts;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (StoredAccountInfos * _Nullable)loadFromPath:(NSString *)path {
|
||||
NSData *data = [NSData dataWithContentsOfFile:path];
|
||||
if (data == nil) {
|
||||
return nil;
|
||||
}
|
||||
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
||||
if (![dict isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
AccountProxyConnection * _Nullable proxy = nil;
|
||||
NSDictionary *proxyDict = dict[@"proxy"];
|
||||
if ([proxyDict isKindOfClass:[NSDictionary class]]) {
|
||||
proxy = [AccountProxyConnection parse:proxyDict];
|
||||
}
|
||||
|
||||
NSMutableArray<StoredAccountInfo *> *accounts = [[NSMutableArray alloc] init];
|
||||
|
||||
NSArray *accountsObject = dict[@"accounts"];
|
||||
if ([accountsObject isKindOfClass:[NSArray class]]) {
|
||||
for (NSDictionary *object in accountsObject) {
|
||||
if ([object isKindOfClass:[NSDictionary class]]) {
|
||||
StoredAccountInfo *account = [StoredAccountInfo parse:object];
|
||||
if (account != nil) {
|
||||
[accounts addObject:account];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [[StoredAccountInfos alloc] initWithProxy:proxy accounts:accounts];;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static NSData *sha256Digest(NSData *data) {
|
||||
uint8_t digest[CC_SHA256_DIGEST_LENGTH];
|
||||
CC_SHA256(data.bytes, (CC_LONG)data.length, digest);
|
||||
|
||||
return [[NSData alloc] initWithBytes:digest length:CC_SHA256_DIGEST_LENGTH];
|
||||
}
|
||||
|
||||
static NSData *concatData(NSData *data1, NSData *data2) {
|
||||
NSMutableData *data = [[NSMutableData alloc] initWithCapacity:data1.length + data2.length];
|
||||
[data appendData:data1];
|
||||
[data appendData:data2];
|
||||
return data;
|
||||
}
|
||||
|
||||
static NSData *concatData3(NSData *data1, NSData *data2, NSData *data3) {
|
||||
NSMutableData *data = [[NSMutableData alloc] initWithCapacity:data1.length + data2.length + data3.length];
|
||||
[data appendData:data1];
|
||||
[data appendData:data2];
|
||||
[data appendData:data3];
|
||||
return data;
|
||||
}
|
||||
|
||||
NSDictionary * _Nullable decryptedNotificationPayload(NSArray<StoredAccountInfo *> *accounts, NSData *data, int *selectedAccountIndex) {
|
||||
if (data.length < 8 + 16) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
int accountIndex = -1;
|
||||
for (StoredAccountInfo *account in accounts) {
|
||||
accountIndex += 1;
|
||||
|
||||
AccountNotificationKey *notificationKey = account.notificationKey;
|
||||
if (![[data subdataWithRange:NSMakeRange(0, 8)] isEqualToData:notificationKey.keyId]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int x = 8;
|
||||
NSData *msgKey = [data subdataWithRange:NSMakeRange(8, 16)];
|
||||
NSData *rawData = [data subdataWithRange:NSMakeRange(8 + 16, data.length - (8 + 16))];
|
||||
|
||||
NSData *sha256_a = sha256Digest(concatData(msgKey, [notificationKey.data subdataWithRange:NSMakeRange(x, 36)]));
|
||||
NSData *sha256_b = sha256Digest(concatData([notificationKey.data subdataWithRange:NSMakeRange(40 + x, 36)], msgKey));
|
||||
NSData *aesKey = concatData3([sha256_a subdataWithRange:NSMakeRange(0, 8)], [sha256_b subdataWithRange:NSMakeRange(8, 16)], [sha256_a subdataWithRange:NSMakeRange(24, 8)]);
|
||||
NSData *aesIv = concatData3([sha256_b subdataWithRange:NSMakeRange(0, 8)], [sha256_a subdataWithRange:NSMakeRange(8, 16)], [sha256_b subdataWithRange:NSMakeRange(24, 8)]);
|
||||
|
||||
NSData *decryptedData = MTAesDecrypt(rawData, aesKey, aesIv);
|
||||
if (decryptedData.length <= 4) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
int32_t dataLength = 0;
|
||||
[decryptedData getBytes:&dataLength range:NSMakeRange(0, 4)];
|
||||
|
||||
if (dataLength < 0 || dataLength > decryptedData.length - 4) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *checkMsgKeyLarge = sha256Digest(concatData([notificationKey.data subdataWithRange:NSMakeRange(88 + x, 32)], decryptedData));
|
||||
NSData *checkMsgKey = [checkMsgKeyLarge subdataWithRange:NSMakeRange(8, 16)];
|
||||
|
||||
if (![checkMsgKey isEqualToData:msgKey]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *contentData = [decryptedData subdataWithRange:NSMakeRange(4, dataLength)];
|
||||
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:contentData options:0 error:nil];
|
||||
if (![dict isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
if (selectedAccountIndex != nil) {
|
||||
*selectedAccountIndex = accountIndex;
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
147
Telegram/NotificationService/Sync.swift
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import ValueBox
|
||||
import PostboxDataTypes
|
||||
import MessageHistoryReadStateTable
|
||||
import MessageHistoryMetadataTable
|
||||
import PreferencesTable
|
||||
import PeerTable
|
||||
import PostboxCoding
|
||||
import AppLockState
|
||||
import NotificationsPresentationData
|
||||
|
||||
private let registeredTypes: Void = {
|
||||
declareEncodable(InAppNotificationSettings.self, f: InAppNotificationSettings.init(decoder:))
|
||||
declareEncodable(TelegramChannel.self, f: TelegramChannel.init(decoder:))
|
||||
}()
|
||||
|
||||
private func accountRecordIdPathName(_ id: Int64) -> String {
|
||||
return "account-\(UInt64(bitPattern: id))"
|
||||
}
|
||||
|
||||
private final class ValueBoxLoggerImpl: ValueBoxLogger {
|
||||
func log(_ what: String) {
|
||||
print("ValueBox: \(what)")
|
||||
}
|
||||
}
|
||||
|
||||
enum SyncProviderImpl {
|
||||
static func isLocked(withRootPath rootPath: String) -> Bool {
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static func lockedMessageText(withRootPath rootPath: String) -> String {
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: notificationsPresentationDataPath(rootPath: rootPath))), let value = try? JSONDecoder().decode(NotificationsPresentationData.self, from: data) {
|
||||
return value.applicationLockedMessageString
|
||||
} else {
|
||||
return "You have a new message"
|
||||
}
|
||||
}
|
||||
|
||||
static func addIncomingMessage(queue: Queue, withRootPath rootPath: String, accountId: Int64, encryptionParameters: DeviceSpecificEncryptionParameters, peerId: Int64, messageId: Int32, completion: @escaping (Int32) -> Void) {
|
||||
queue.async {
|
||||
let _ = registeredTypes
|
||||
|
||||
let sharedBasePath = rootPath + "/accounts-metadata"
|
||||
let basePath = rootPath + "/" + accountRecordIdPathName(accountId) + "/postbox"
|
||||
|
||||
let sharedValueBox = SqliteValueBox(basePath: sharedBasePath + "/db", queue: queue, logger: ValueBoxLoggerImpl(), encryptionParameters: nil, disableCache: true, upgradeProgress: { _ in
|
||||
})
|
||||
|
||||
let valueBox = SqliteValueBox(basePath: basePath + "/db", queue: queue, logger: ValueBoxLoggerImpl(), encryptionParameters: ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: encryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: encryptionParameters.salt)!), disableCache: true, upgradeProgress: { _ in
|
||||
})
|
||||
|
||||
let metadataTable = MessageHistoryMetadataTable(valueBox: valueBox, table: MessageHistoryMetadataTable.tableSpec(10))
|
||||
let readStateTable = MessageHistoryReadStateTable(valueBox: valueBox, table: MessageHistoryReadStateTable.tableSpec(14), defaultMessageNamespaceReadStates: [:])
|
||||
let peerTable = PeerTable(valueBox: valueBox, table: PeerTable.tableSpec(2), reverseAssociatedTable: nil)
|
||||
|
||||
let preferencesTable = PreferencesTable(valueBox: sharedValueBox, table: PreferencesTable.tableSpec(2))
|
||||
|
||||
let peerId = PeerId(peerId)
|
||||
|
||||
let initialCombinedState = readStateTable.getCombinedState(peerId)
|
||||
|
||||
let combinedState = initialCombinedState.flatMap { state -> CombinedPeerReadState in
|
||||
var state = state
|
||||
for i in 0 ..< state.states.count {
|
||||
if state.states[i].0 == Namespaces.Message.Cloud {
|
||||
switch state.states[i].1 {
|
||||
case .idBased(let maxIncomingReadId, let maxOutgoingReadId, var maxKnownId, var count, let markedUnread):
|
||||
if messageId > maxIncomingReadId {
|
||||
count += 1
|
||||
}
|
||||
maxKnownId = max(maxKnownId, messageId)
|
||||
state.states[i] = (state.states[i].0, .idBased(maxIncomingReadId: maxIncomingReadId, maxOutgoingReadId: maxOutgoingReadId, maxKnownId: maxKnownId, count: count, markedUnread: markedUnread))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
if let combinedState = combinedState {
|
||||
let initialCount = initialCombinedState?.count ?? 0
|
||||
let updatedCount = combinedState.count
|
||||
let deltaCount = max(0, updatedCount - initialCount)
|
||||
|
||||
let tag: PeerSummaryCounterTags
|
||||
if peerId.namespace == Namespaces.Peer.CloudChannel {
|
||||
if let channel = peerTable.get(peerId) as? TelegramChannel {
|
||||
switch channel.info {
|
||||
case .broadcast:
|
||||
tag = .channel
|
||||
case .group:
|
||||
if channel.username != nil {
|
||||
tag = .publicGroup
|
||||
} else {
|
||||
tag = .privateGroup
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tag = .channel
|
||||
}
|
||||
} else if peerId.namespace == Namespaces.Peer.CloudGroup {
|
||||
tag = .privateGroup
|
||||
} else {
|
||||
tag = .privateChat
|
||||
}
|
||||
|
||||
var totalCount: Int32 = -1
|
||||
|
||||
var totalUnreadState = metadataTable.getChatListTotalUnreadState()
|
||||
if var counters = totalUnreadState.absoluteCounters[tag] {
|
||||
if initialCount == 0 && updatedCount > 0 {
|
||||
counters.chatCount += 1
|
||||
}
|
||||
counters.messageCount += deltaCount
|
||||
totalUnreadState.absoluteCounters[tag] = counters
|
||||
}
|
||||
if var counters = totalUnreadState.filteredCounters[tag] {
|
||||
if initialCount == 0 && updatedCount > 0 {
|
||||
counters.chatCount += 1
|
||||
}
|
||||
counters.messageCount += deltaCount
|
||||
totalUnreadState.filteredCounters[tag] = counters
|
||||
}
|
||||
|
||||
let inAppSettings = preferencesTable.get(key: ApplicationSpecificSharedDataKeys.inAppNotificationSettings) as? InAppNotificationSettings ?? InAppNotificationSettings.defaultSettings
|
||||
|
||||
totalCount = totalUnreadState.count(for: inAppSettings.totalUnreadCountDisplayStyle.category, in: inAppSettings.totalUnreadCountDisplayCategory.statsType, with: inAppSettings.totalUnreadCountIncludeTags)
|
||||
metadataTable.setChatListTotalUnreadState(totalUnreadState)
|
||||
metadataTable.setShouldReindexUnreadCounts(value: true)
|
||||
|
||||
metadataTable.beforeCommit()
|
||||
readStateTable.beforeCommit()
|
||||
|
||||
completion(totalCount)
|
||||
} else {
|
||||
completion(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Telegram/NotificationService/TelegramChannel.swift
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import PostboxDataTypes
|
||||
import PostboxCoding
|
||||
|
||||
public enum TelegramChannelInfo: Int32 {
|
||||
case broadcast = 0
|
||||
case group = 1
|
||||
}
|
||||
|
||||
public final class TelegramChannel: Peer {
|
||||
public let id: PeerId
|
||||
public let username: String?
|
||||
public let info: TelegramChannelInfo
|
||||
|
||||
public let associatedPeerId: PeerId? = nil
|
||||
public let notificationSettingsPeerId: PeerId? = nil
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.id = PeerId(decoder.decodeInt64ForKey("i", orElse: 0))
|
||||
self.username = decoder.decodeOptionalStringForKey("un")
|
||||
self.info = TelegramChannelInfo(rawValue: decoder.decodeInt32ForKey("i.t", orElse: 0)) ?? .broadcast
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
public func isEqual(_ other: Peer) -> Bool {
|
||||
guard let other = other as? TelegramChannel else {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.username != other.username {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
56
Telegram/Share/Info.plist
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${APP_NAME}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(PRODUCT_BUNDLE_SHORT_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${BUILD_NUMBER}</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>IntentsSupported</key>
|
||||
<array>
|
||||
<string>INSendMessageIntent</string>
|
||||
</array>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<string>SUBQUERY (
|
||||
extensionItems,
|
||||
$extensionItem,
|
||||
SUBQUERY (
|
||||
$extensionItem.attachments,
|
||||
$attachment,
|
||||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url" ||
|
||||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.movie" ||
|
||||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image" ||
|
||||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" ||
|
||||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text" ||
|
||||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.audio" ||
|
||||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data" ||
|
||||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.vcard" ||
|
||||
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.apple.pkpass"
|
||||
).@count == $extensionItem.attachments.@count
|
||||
).@count > 0</string>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>ShareRootController</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
14
Telegram/Share/Share-AppStore.entitlements
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.telegram.TelegramHD</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)org.telegram.TelegramHD</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
14
Telegram/Share/Share-AppStoreLLC.entitlements
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.ph.telegra.Telegraph</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)ph.telegra.Telegraph</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
4
Telegram/Share/Share-Bridging-Header.h
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#ifndef Share_Bridging_Header_h
|
||||
#define Share_Bridging_Header_h
|
||||
|
||||
#endif
|
||||
10
Telegram/Share/Share-Fork.entitlements
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.fork.telegram.Telegram-iOS</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
14
Telegram/Share/Share-HockeyApp.entitlements
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.telegram.Telegram-iOS</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)org.telegram.Telegram-iOS</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
70
Telegram/Share/ShareRootController.swift
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import UIKit
|
||||
import TelegramUI
|
||||
import BuildConfig
|
||||
|
||||
@objc(ShareRootController)
|
||||
class ShareRootController: UIViewController {
|
||||
private var impl: ShareRootControllerImpl?
|
||||
|
||||
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
|
||||
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
||||
|
||||
self.modalPresentationStyle = .fullScreen
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
super.loadView()
|
||||
|
||||
if self.impl == nil {
|
||||
let appBundleIdentifier = Bundle.main.bundleIdentifier!
|
||||
guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
|
||||
return
|
||||
}
|
||||
|
||||
let baseAppBundleId = String(appBundleIdentifier[..<lastDotRange.lowerBound])
|
||||
|
||||
let buildConfig = BuildConfig(baseAppBundleId: baseAppBundleId)
|
||||
|
||||
let languagesCategory = "ios"
|
||||
|
||||
let appGroupName = "group.\(baseAppBundleId)"
|
||||
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
||||
|
||||
guard let appGroupUrl = maybeAppGroupUrl else {
|
||||
return
|
||||
}
|
||||
|
||||
let rootPath = appGroupUrl.path + "/telegram-data"
|
||||
|
||||
let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId)
|
||||
let encryptionParameters: (Data, Data) = (deviceSpecificEncryptionParameters.key, deviceSpecificEncryptionParameters.salt)
|
||||
|
||||
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
|
||||
|
||||
self.impl = ShareRootControllerImpl(initializationData: ShareRootControllerInitializationData(appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), getExtensionContext: { [weak self] in
|
||||
return self?.extensionContext
|
||||
})
|
||||
}
|
||||
|
||||
self.impl?.loadView()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
self.impl?.viewWillAppear()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
self.impl?.viewWillDisappear()
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
self.impl?.viewDidLayoutSubviews(view: self.view, traitCollection: self.traitCollection)
|
||||
}
|
||||
}
|
||||
3
Telegram/Share/en.lproj/Localizable.strings
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
"Common.OK" = "OK";
|
||||
"Share.AuthTitle" = "Log in to Telegram";
|
||||
"Share.AuthDescription" = "Open Telegram and log in to share.";
|
||||
46
Telegram/SiriIntents/Info.plist
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${APP_NAME}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(PRODUCT_BUNDLE_SHORT_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${BUILD_NUMBER}</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>IntentsRestrictedWhileLocked</key>
|
||||
<array/>
|
||||
<key>IntentsRestrictedWhileProtectedDataUnavailable</key>
|
||||
<array/>
|
||||
<key>IntentsSupported</key>
|
||||
<array>
|
||||
<string>INSendMessageIntent</string>
|
||||
<string>INStartAudioCallIntent</string>
|
||||
<string>INSearchForMessagesIntent</string>
|
||||
<string>INSetMessageAttributeIntent</string>
|
||||
<string>INSearchCallHistoryIntent</string>
|
||||
</array>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.intents-service</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>IntentHandler</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
146
Telegram/SiriIntents/IntentContacts.swift
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import Contacts
|
||||
import Intents
|
||||
|
||||
func formatPhoneNumber(_ value: String) -> String {
|
||||
if value.hasPrefix("+") {
|
||||
return value
|
||||
} else {
|
||||
return "+\(value)"
|
||||
}
|
||||
}
|
||||
|
||||
struct MatchingDeviceContact {
|
||||
let stableId: String
|
||||
let firstName: String
|
||||
let lastName: String
|
||||
let phoneNumbers: [String]
|
||||
let peerId: PeerId?
|
||||
}
|
||||
|
||||
enum IntentContactsError {
|
||||
case generic
|
||||
}
|
||||
|
||||
private let phonebookUsernamePathPrefix = "@id"
|
||||
private let phonebookUsernamePrefix = "https://t.me/" + phonebookUsernamePathPrefix
|
||||
|
||||
private func parseAppSpecificContactReference(_ value: String) -> PeerId? {
|
||||
if !value.hasPrefix(phonebookUsernamePrefix) {
|
||||
return nil
|
||||
}
|
||||
let idString = String(value[value.index(value.startIndex, offsetBy: phonebookUsernamePrefix.count)...])
|
||||
if let id = Int32(idString) {
|
||||
return PeerId(namespace: Namespaces.Peer.CloudUser, id: id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func cleanPhoneNumber(_ text: String) -> String {
|
||||
var result = ""
|
||||
for c in text {
|
||||
if c == "+" {
|
||||
if result.isEmpty {
|
||||
result += String(c)
|
||||
}
|
||||
} else if c >= "0" && c <= "9" {
|
||||
result += String(c)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 10.0, *)
|
||||
func matchingDeviceContacts(stableIds: [String]) -> Signal<[MatchingDeviceContact], IntentContactsError> {
|
||||
guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
let store = CNContactStore()
|
||||
guard let contacts = try? store.unifiedContacts(matching: CNContact.predicateForContacts(withIdentifiers: stableIds), keysToFetch: [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactPhoneNumbersKey as CNKeyDescriptor, CNContactUrlAddressesKey as CNKeyDescriptor]) else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
||||
return .single(contacts.map({ contact in
|
||||
let phoneNumbers = contact.phoneNumbers.compactMap({ number -> String? in
|
||||
if !number.value.stringValue.isEmpty {
|
||||
return cleanPhoneNumber(number.value.stringValue)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
var contactPeerId: PeerId?
|
||||
for address in contact.urlAddresses {
|
||||
if address.label == "Telegram", let peerId = parseAppSpecificContactReference(address.value as String) {
|
||||
contactPeerId = peerId
|
||||
}
|
||||
}
|
||||
|
||||
return MatchingDeviceContact(stableId: contact.identifier, firstName: contact.givenName, lastName: contact.familyName, phoneNumbers: phoneNumbers, peerId: contactPeerId)
|
||||
}))
|
||||
}
|
||||
|
||||
private func matchPhoneNumbers(_ lhs: String, _ rhs: String) -> Bool {
|
||||
if lhs.count < 10 && lhs.count == rhs.count {
|
||||
return lhs == rhs
|
||||
} else if lhs.count >= 10 && rhs.count >= 10 && lhs.suffix(10) == rhs.suffix(10) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func matchingCloudContacts(postbox: Postbox, contacts: [MatchingDeviceContact]) -> Signal<[(String, TelegramUser)], NoError> {
|
||||
return postbox.transaction { transaction -> [(String, TelegramUser)] in
|
||||
var result: [(String, TelegramUser)] = []
|
||||
outer: for peerId in transaction.getContactPeerIds() {
|
||||
if let peer = transaction.getPeer(peerId) as? TelegramUser {
|
||||
for contact in contacts {
|
||||
if let contactPeerId = contact.peerId, contactPeerId == peerId {
|
||||
result.append((contact.stableId, peer))
|
||||
continue outer
|
||||
} else if let peerPhoneNumber = peer.phone {
|
||||
for contactPhoneNumber in contact.phoneNumbers {
|
||||
if matchPhoneNumbers(contactPhoneNumber, peerPhoneNumber) {
|
||||
result.append((contact.stableId, peer))
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
func matchingCloudContact(postbox: Postbox, peerId: PeerId) -> Signal<TelegramUser?, NoError> {
|
||||
return postbox.transaction { transaction -> TelegramUser? in
|
||||
if let user = transaction.getPeer(peerId) as? TelegramUser {
|
||||
return user
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 10.0, *)
|
||||
func personWithUser(stableId: String, user: TelegramUser) -> INPerson {
|
||||
var nameComponents = PersonNameComponents()
|
||||
nameComponents.givenName = user.firstName
|
||||
nameComponents.familyName = user.lastName
|
||||
let personHandle: INPersonHandle
|
||||
if let phone = user.phone {
|
||||
personHandle = INPersonHandle(value: formatPhoneNumber(phone), type: .phoneNumber)
|
||||
} else if let username = user.username {
|
||||
personHandle = INPersonHandle(value: "@\(username)", type: .unknown)
|
||||
} else {
|
||||
personHandle = INPersonHandle(value: user.nameOrPhone, type: .unknown)
|
||||
}
|
||||
|
||||
return INPerson(personHandle: personHandle, nameComponents: nameComponents, displayName: user.debugDisplayTitle, image: nil, contactIdentifier: stableId, customIdentifier: "tg\(user.id.toInt64())")
|
||||
}
|
||||
606
Telegram/SiriIntents/IntentHandler.swift
Normal file
|
|
@ -0,0 +1,606 @@
|
|||
import Foundation
|
||||
import Intents
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import BuildConfig
|
||||
import Contacts
|
||||
import OpenSSLEncryptionProvider
|
||||
|
||||
private var accountCache: Account?
|
||||
|
||||
private var installedSharedLogger = false
|
||||
|
||||
private func setupSharedLogger(_ path: String) {
|
||||
if !installedSharedLogger {
|
||||
installedSharedLogger = true
|
||||
Logger.setSharedLogger(Logger(basePath: path))
|
||||
}
|
||||
}
|
||||
|
||||
private let accountAuxiliaryMethods = AccountAuxiliaryMethods(updatePeerChatInputState: { interfaceState, inputState -> PeerChatInterfaceState? in
|
||||
return interfaceState
|
||||
}, fetchResource: { account, resource, ranges, _ in
|
||||
return nil
|
||||
}, fetchResourceMediaReferenceHash: { resource in
|
||||
return .single(nil)
|
||||
}, prepareSecretThumbnailData: { _ in
|
||||
return nil
|
||||
})
|
||||
|
||||
private struct ApplicationSettings {
|
||||
let logging: LoggingSettings
|
||||
}
|
||||
|
||||
private func applicationSettings(accountManager: AccountManager) -> Signal<ApplicationSettings, NoError> {
|
||||
return accountManager.transaction { transaction -> ApplicationSettings in
|
||||
let loggingSettings: LoggingSettings
|
||||
if let value = transaction.getSharedData(SharedDataKeys.loggingSettings) as? LoggingSettings {
|
||||
loggingSettings = value
|
||||
} else {
|
||||
loggingSettings = LoggingSettings.defaultSettings
|
||||
}
|
||||
return ApplicationSettings(logging: loggingSettings)
|
||||
}
|
||||
}
|
||||
|
||||
enum IntentHandlingError {
|
||||
case generic
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 10.0, *)
|
||||
@objc(IntentHandler)
|
||||
public class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessagesIntentHandling, INSetMessageAttributeIntentHandling, INStartAudioCallIntentHandling, INSearchCallHistoryIntentHandling {
|
||||
private let accountPromise = Promise<Account?>()
|
||||
|
||||
private let resolvePersonsDisposable = MetaDisposable()
|
||||
private let actionDisposable = MetaDisposable()
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
guard let appBundleIdentifier = Bundle.main.bundleIdentifier, let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
|
||||
return
|
||||
}
|
||||
|
||||
let baseAppBundleId = String(appBundleIdentifier[..<lastDotRange.lowerBound])
|
||||
|
||||
let buildConfig = BuildConfig(baseAppBundleId: baseAppBundleId)
|
||||
|
||||
let apiId: Int32 = buildConfig.apiId
|
||||
let apiHash: String = buildConfig.apiHash
|
||||
let languagesCategory = "ios"
|
||||
|
||||
let appGroupName = "group.\(baseAppBundleId)"
|
||||
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
||||
|
||||
guard let appGroupUrl = maybeAppGroupUrl else {
|
||||
return
|
||||
}
|
||||
|
||||
let rootPath = rootPathForBasePath(appGroupUrl.path)
|
||||
performAppGroupUpgrades(appGroupPath: appGroupUrl.path, rootPath: rootPath)
|
||||
|
||||
TempBox.initializeShared(basePath: rootPath, processType: "siri", launchSpecificId: arc4random64())
|
||||
|
||||
let logsPath = rootPath + "/siri-logs"
|
||||
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
setupSharedLogger(logsPath)
|
||||
|
||||
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
|
||||
|
||||
let account: Signal<Account?, NoError>
|
||||
if let accountCache = accountCache {
|
||||
account = .single(accountCache)
|
||||
} else {
|
||||
initializeAccountManagement()
|
||||
let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata")
|
||||
|
||||
let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId)
|
||||
let encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!)
|
||||
|
||||
account = currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), supplementary: true, manager: accountManager, rootPath: rootPath, auxiliaryMethods: accountAuxiliaryMethods, encryptionParameters: encryptionParameters)
|
||||
|> mapToSignal { account -> Signal<Account?, NoError> in
|
||||
if let account = account {
|
||||
switch account {
|
||||
case .upgrading:
|
||||
return .complete()
|
||||
case let .authorized(account):
|
||||
return applicationSettings(accountManager: accountManager)
|
||||
|> deliverOnMainQueue
|
||||
|> map { settings -> Account in
|
||||
accountCache = account
|
||||
Logger.shared.logToFile = settings.logging.logToFile
|
||||
Logger.shared.logToConsole = settings.logging.logToConsole
|
||||
|
||||
Logger.shared.redactSensitiveData = settings.logging.redactSensitiveData
|
||||
return account
|
||||
}
|
||||
case .unauthorized:
|
||||
return .complete()
|
||||
}
|
||||
} else {
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
|> take(1)
|
||||
}
|
||||
self.accountPromise.set(account)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.resolvePersonsDisposable.dispose()
|
||||
self.actionDisposable.dispose()
|
||||
}
|
||||
|
||||
override public func handler(for intent: INIntent) -> Any {
|
||||
return self
|
||||
}
|
||||
|
||||
enum ResolveResult {
|
||||
case success(INPerson)
|
||||
case disambiguation([INPerson])
|
||||
case needsValue
|
||||
case noResult
|
||||
case skip
|
||||
|
||||
@available(iOSApplicationExtension 11.0, *)
|
||||
var sendMessageRecipientResulutionResult: INSendMessageRecipientResolutionResult {
|
||||
switch self {
|
||||
case let .success(person):
|
||||
return .success(with: person)
|
||||
case let .disambiguation(persons):
|
||||
return .disambiguation(with: persons)
|
||||
case .needsValue:
|
||||
return .needsValue()
|
||||
case .noResult:
|
||||
return .unsupported()
|
||||
case .skip:
|
||||
return .notRequired()
|
||||
}
|
||||
}
|
||||
|
||||
var personResolutionResult: INPersonResolutionResult {
|
||||
switch self {
|
||||
case let .success(person):
|
||||
return .success(with: person)
|
||||
case let .disambiguation(persons):
|
||||
return .disambiguation(with: persons)
|
||||
case .needsValue:
|
||||
return .needsValue()
|
||||
case .noResult:
|
||||
return .unsupported()
|
||||
case .skip:
|
||||
return .notRequired()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func resolve(persons: [INPerson]?, with completion: @escaping ([ResolveResult]) -> Void) {
|
||||
let account = self.accountPromise.get()
|
||||
guard let initialPersons = persons, !initialPersons.isEmpty else {
|
||||
completion([.needsValue])
|
||||
return
|
||||
}
|
||||
|
||||
var filteredPersons: [INPerson] = []
|
||||
for person in initialPersons {
|
||||
if let contactIdentifier = person.contactIdentifier, !contactIdentifier.isEmpty {
|
||||
filteredPersons.append(person)
|
||||
}
|
||||
|
||||
if #available(iOSApplicationExtension 10.3, *) {
|
||||
if let siriMatches = person.siriMatches {
|
||||
for match in siriMatches {
|
||||
if let contactIdentifier = match.contactIdentifier, !contactIdentifier.isEmpty {
|
||||
filteredPersons.append(match)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if filteredPersons.isEmpty {
|
||||
completion([.noResult])
|
||||
return
|
||||
}
|
||||
|
||||
var allPersonsAlreadyMatched = true
|
||||
for person in filteredPersons {
|
||||
if !(person.customIdentifier ?? "").hasPrefix("tg") {
|
||||
allPersonsAlreadyMatched = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allPersonsAlreadyMatched && filteredPersons.count == 1 {
|
||||
completion([.success(filteredPersons[0])])
|
||||
return
|
||||
}
|
||||
|
||||
let stableIds = filteredPersons.compactMap({ person -> String? in
|
||||
if let contactIdentifier = person.contactIdentifier {
|
||||
return contactIdentifier
|
||||
}
|
||||
if #available(iOSApplicationExtension 10.3, *) {
|
||||
if let siriMatches = person.siriMatches {
|
||||
for match in siriMatches {
|
||||
if let contactIdentifier = match.contactIdentifier, !contactIdentifier.isEmpty {
|
||||
return contactIdentifier
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
let signal = matchingDeviceContacts(stableIds: stableIds)
|
||||
|> take(1)
|
||||
|> mapToSignal { matchedContacts in
|
||||
return account
|
||||
|> castError(IntentContactsError.self)
|
||||
|> mapToSignal { account -> Signal<[(String, TelegramUser)], IntentContactsError> in
|
||||
if let account = account {
|
||||
return matchingCloudContacts(postbox: account.postbox, contacts: matchedContacts)
|
||||
|> castError(IntentContactsError.self)
|
||||
} else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.resolvePersonsDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(next: { peers in
|
||||
if peers.isEmpty {
|
||||
completion([.noResult])
|
||||
} else if peers.count == 1 {
|
||||
completion(peers.map { .success(personWithUser(stableId: $0, user: $1)) })
|
||||
} else {
|
||||
completion([.disambiguation(peers.map { (personWithUser(stableId: $0, user: $1)) })])
|
||||
}
|
||||
}, error: { error in
|
||||
completion([.skip])
|
||||
}))
|
||||
}
|
||||
|
||||
// MARK: - INSendMessageIntentHandling
|
||||
|
||||
public func resolveRecipients(for intent: INSendMessageIntent, with completion: @escaping ([INPersonResolutionResult]) -> Void) {
|
||||
guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
|
||||
completion([INPersonResolutionResult.notRequired()])
|
||||
return
|
||||
}
|
||||
self.resolve(persons: intent.recipients, with: { result in
|
||||
completion(result.map { $0.personResolutionResult })
|
||||
})
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 11.0, *)
|
||||
public func resolveRecipients(for intent: INSendMessageIntent, with completion: @escaping ([INSendMessageRecipientResolutionResult]) -> Void) {
|
||||
if let peerId = intent.conversationIdentifier.flatMap(Int64.init) {
|
||||
let account = self.accountPromise.get()
|
||||
|
||||
let signal = account
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> mapToSignal { account -> Signal<INPerson?, IntentHandlingError> in
|
||||
if let account = account {
|
||||
return matchingCloudContact(postbox: account.postbox, peerId: PeerId(peerId))
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> map { user -> INPerson? in
|
||||
if let user = user {
|
||||
return personWithUser(stableId: "tg\(peerId)", user: user)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
}
|
||||
|
||||
self.resolvePersonsDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(next: { person in
|
||||
if let person = person {
|
||||
completion([INSendMessageRecipientResolutionResult.success(with: person)])
|
||||
} else {
|
||||
completion([INSendMessageRecipientResolutionResult.needsValue()])
|
||||
}
|
||||
}, error: { error in
|
||||
completion([INSendMessageRecipientResolutionResult.unsupported(forReason: .noAccount)])
|
||||
}))
|
||||
} else {
|
||||
guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
|
||||
completion([INSendMessageRecipientResolutionResult.notRequired()])
|
||||
return
|
||||
}
|
||||
self.resolve(persons: intent.recipients, with: { result in
|
||||
completion(result.map { $0.sendMessageRecipientResulutionResult })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func resolveContent(for intent: INSendMessageIntent, with completion: @escaping (INStringResolutionResult) -> Void) {
|
||||
guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
|
||||
completion(INStringResolutionResult.notRequired())
|
||||
return
|
||||
}
|
||||
if let text = intent.content, !text.isEmpty {
|
||||
completion(INStringResolutionResult.success(with: text))
|
||||
} else {
|
||||
completion(INStringResolutionResult.needsValue())
|
||||
}
|
||||
}
|
||||
|
||||
public func confirm(intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) {
|
||||
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self))
|
||||
guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
|
||||
let response = INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity)
|
||||
completion(response)
|
||||
return
|
||||
}
|
||||
let response = INSendMessageIntentResponse(code: .ready, userActivity: userActivity)
|
||||
completion(response)
|
||||
}
|
||||
|
||||
public func handle(intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) {
|
||||
self.actionDisposable.set((self.accountPromise.get()
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> take(1)
|
||||
|> mapToSignal { account -> Signal<Void, IntentHandlingError> in
|
||||
guard let account = account else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
guard let recipient = intent.recipients?.first, let customIdentifier = recipient.customIdentifier, customIdentifier.hasPrefix("tg") else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
||||
guard let peerIdValue = Int64(String(customIdentifier[customIdentifier.index(customIdentifier.startIndex, offsetBy: 2)...])) else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
||||
let peerId = PeerId(peerIdValue)
|
||||
if peerId.namespace != Namespaces.Peer.CloudUser {
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
||||
account.shouldBeServiceTaskMaster.set(.single(.now))
|
||||
return standaloneSendMessage(account: account, peerId: peerId, text: intent.content ?? "", attributes: [], media: nil, replyToMessageId: nil)
|
||||
|> mapError { _ -> IntentHandlingError in
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { _ -> Signal<Void, IntentHandlingError> in
|
||||
return .complete()
|
||||
}
|
||||
|> afterDisposed {
|
||||
account.shouldBeServiceTaskMaster.set(.single(.never))
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(error: { _ in
|
||||
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self))
|
||||
let response = INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity)
|
||||
completion(response)
|
||||
}, completed: {
|
||||
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self))
|
||||
let response = INSendMessageIntentResponse(code: .success, userActivity: userActivity)
|
||||
completion(response)
|
||||
}))
|
||||
}
|
||||
|
||||
// MARK: - INSearchForMessagesIntentHandling
|
||||
|
||||
public func resolveAttributes(for intent: INSearchForMessagesIntent, with completion: @escaping (INMessageAttributeOptionsResolutionResult) -> Void) {
|
||||
completion(.success(with: .unread))
|
||||
}
|
||||
|
||||
public func handle(intent: INSearchForMessagesIntent, completion: @escaping (INSearchForMessagesIntentResponse) -> Void) {
|
||||
self.actionDisposable.set((self.accountPromise.get()
|
||||
|> take(1)
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> mapToSignal { account -> Signal<[INMessage], IntentHandlingError> in
|
||||
guard let account = account else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
||||
account.shouldBeServiceTaskMaster.set(.single(.now))
|
||||
account.resetStateManagement()
|
||||
|
||||
let completion: Signal<Void, NoError> = account.stateManager.pollStateUpdateCompletion()
|
||||
|> map { _ in
|
||||
return Void()
|
||||
}
|
||||
|
||||
return (completion |> timeout(4.0, queue: Queue.mainQueue(), alternate: .single(Void())))
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> take(1)
|
||||
|> mapToSignal { _ -> Signal<[INMessage], IntentHandlingError> in
|
||||
let messages: Signal<[INMessage], NoError>
|
||||
if let identifiers = intent.identifiers, !identifiers.isEmpty {
|
||||
messages = getMessages(account: account, ids: identifiers.compactMap(MessageId.init(string:)))
|
||||
} else {
|
||||
messages = unreadMessages(account: account)
|
||||
}
|
||||
return messages
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> afterDisposed {
|
||||
account.shouldBeServiceTaskMaster.set(.single(.never))
|
||||
}
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { messages in
|
||||
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchForMessagesIntent.self))
|
||||
let response = INSearchForMessagesIntentResponse(code: .success, userActivity: userActivity)
|
||||
response.messages = messages
|
||||
completion(response)
|
||||
}, error: { _ in
|
||||
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchForMessagesIntent.self))
|
||||
let response = INSearchForMessagesIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity)
|
||||
completion(response)
|
||||
}))
|
||||
}
|
||||
|
||||
// MARK: - INSetMessageAttributeIntentHandling
|
||||
|
||||
public func resolveAttribute(for intent: INSetMessageAttributeIntent, with completion: @escaping (INMessageAttributeResolutionResult) -> Void) {
|
||||
let supportedAttributes: [INMessageAttribute] = [.read, .unread]
|
||||
var attribute = intent.attribute
|
||||
if attribute == .flagged {
|
||||
attribute = .unread
|
||||
}
|
||||
if supportedAttributes.contains(attribute) {
|
||||
completion(.success(with: attribute))
|
||||
} else {
|
||||
completion(.confirmationRequired(with: intent.attribute))
|
||||
}
|
||||
}
|
||||
|
||||
public func handle(intent: INSetMessageAttributeIntent, completion: @escaping (INSetMessageAttributeIntentResponse) -> Void) {
|
||||
self.actionDisposable.set((self.accountPromise.get()
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> take(1)
|
||||
|> mapToSignal { account -> Signal<Void, IntentHandlingError> in
|
||||
guard let account = account else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
||||
var signals: [Signal<Void, IntentHandlingError>] = []
|
||||
var maxMessageIdsToApply: [PeerId: MessageId] = [:]
|
||||
if let identifiers = intent.identifiers {
|
||||
for identifier in identifiers {
|
||||
let components = identifier.components(separatedBy: "_")
|
||||
if let first = components.first, let peerId = Int64(first), let namespace = Int32(components[1]), let id = Int32(components[2]) {
|
||||
let peerId = PeerId(peerId)
|
||||
let messageId = MessageId(peerId: peerId, namespace: namespace, id: id)
|
||||
if let currentMessageId = maxMessageIdsToApply[peerId] {
|
||||
if currentMessageId < messageId {
|
||||
maxMessageIdsToApply[peerId] = messageId
|
||||
}
|
||||
} else {
|
||||
maxMessageIdsToApply[peerId] = messageId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (_, messageId) in maxMessageIdsToApply {
|
||||
signals.append(applyMaxReadIndexInteractively(postbox: account.postbox, stateManager: account.stateManager, index: MessageIndex(id: messageId, timestamp: 0))
|
||||
|> castError(IntentHandlingError.self))
|
||||
}
|
||||
|
||||
if signals.isEmpty {
|
||||
return .complete()
|
||||
} else {
|
||||
account.shouldBeServiceTaskMaster.set(.single(.now))
|
||||
return combineLatest(signals)
|
||||
|> mapToSignal { _ -> Signal<Void, IntentHandlingError> in
|
||||
return .complete()
|
||||
}
|
||||
|> afterDisposed {
|
||||
account.shouldBeServiceTaskMaster.set(.single(.never))
|
||||
}
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(error: { _ in
|
||||
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSetMessageAttributeIntent.self))
|
||||
let response = INSetMessageAttributeIntentResponse(code: .failure, userActivity: userActivity)
|
||||
completion(response)
|
||||
}, completed: {
|
||||
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSetMessageAttributeIntent.self))
|
||||
let response = INSetMessageAttributeIntentResponse(code: .success, userActivity: userActivity)
|
||||
completion(response)
|
||||
}))
|
||||
}
|
||||
|
||||
// MARK: - INStartAudioCallIntentHandling
|
||||
|
||||
public func resolveContacts(for intent: INStartAudioCallIntent, with completion: @escaping ([INPersonResolutionResult]) -> Void) {
|
||||
guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
|
||||
completion([INPersonResolutionResult.notRequired()])
|
||||
return
|
||||
}
|
||||
self.resolve(persons: intent.contacts, with: { result in
|
||||
completion(result.map { $0.personResolutionResult })
|
||||
})
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 11.0, *)
|
||||
public func resolveDestinationType(for intent: INStartAudioCallIntent, with completion: @escaping (INCallDestinationTypeResolutionResult) -> Void) {
|
||||
completion(.success(with: .normal))
|
||||
}
|
||||
|
||||
public func handle(intent: INStartAudioCallIntent, completion: @escaping (INStartAudioCallIntentResponse) -> Void) {
|
||||
self.actionDisposable.set((self.accountPromise.get()
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> take(1)
|
||||
|> mapToSignal { account -> Signal<PeerId, IntentHandlingError> in
|
||||
guard let contact = intent.contacts?.first, let customIdentifier = contact.customIdentifier, customIdentifier.hasPrefix("tg") else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
||||
guard let peerIdValue = Int64(String(customIdentifier[customIdentifier.index(customIdentifier.startIndex, offsetBy: 2)...])) else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
||||
let peerId = PeerId(peerIdValue)
|
||||
if peerId.namespace != Namespaces.Peer.CloudUser {
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
||||
return .single(peerId)
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { peerId in
|
||||
let userActivity = NSUserActivity(activityType: NSStringFromClass(INStartAudioCallIntent.self))
|
||||
userActivity.userInfo = ["handle": "TGCA\(peerId.toInt64())"]
|
||||
let response = INStartAudioCallIntentResponse(code: .continueInApp, userActivity: userActivity)
|
||||
completion(response)
|
||||
}, error: { _ in
|
||||
let userActivity = NSUserActivity(activityType: NSStringFromClass(INStartAudioCallIntent.self))
|
||||
let response = INStartAudioCallIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity)
|
||||
completion(response)
|
||||
}))
|
||||
}
|
||||
|
||||
// MARK: - INSearchCallHistoryIntentHandling
|
||||
|
||||
@available(iOSApplicationExtension 11.0, *)
|
||||
public func resolveCallTypes(for intent: INSearchCallHistoryIntent, with completion: @escaping (INCallRecordTypeOptionsResolutionResult) -> Void) {
|
||||
completion(.success(with: .missed))
|
||||
}
|
||||
|
||||
/*public func resolveCallType(for intent: INSearchCallHistoryIntent, with completion: @escaping (INCallRecordTypeResolutionResult) -> Void) {
|
||||
completion(.success(with: .missed))
|
||||
}*/
|
||||
|
||||
public func handle(intent: INSearchCallHistoryIntent, completion: @escaping (INSearchCallHistoryIntentResponse) -> Void) {
|
||||
self.actionDisposable.set((self.accountPromise.get()
|
||||
|> take(1)
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> mapToSignal { account -> Signal<[CallRecord], IntentHandlingError> in
|
||||
guard let account = account else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
||||
account.shouldBeServiceTaskMaster.set(.single(.now))
|
||||
return missedCalls(account: account)
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> afterDisposed {
|
||||
account.shouldBeServiceTaskMaster.set(.single(.never))
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { calls in
|
||||
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchCallHistoryIntent.self))
|
||||
let response: INSearchCallHistoryIntentResponse
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
response = INSearchCallHistoryIntentResponse(code: .success, userActivity: userActivity)
|
||||
response.callRecords = calls.map { $0.intentCall }
|
||||
} else {
|
||||
response = INSearchCallHistoryIntentResponse(code: .continueInApp, userActivity: userActivity)
|
||||
}
|
||||
completion(response)
|
||||
}, error: { _ in
|
||||
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchCallHistoryIntent.self))
|
||||
let response = INSearchCallHistoryIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity)
|
||||
completion(response)
|
||||
}))
|
||||
}
|
||||
}
|
||||
247
Telegram/SiriIntents/IntentMessages.swift
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
import Foundation
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SyncCore
|
||||
import Contacts
|
||||
import Intents
|
||||
|
||||
extension MessageId {
|
||||
init?(string: String) {
|
||||
let components = string.components(separatedBy: "_")
|
||||
if components.count == 3, let peerIdValue = Int64(components[0]), let namespaceValue = Int32(components[1]), let idValue = Int32(components[2]) {
|
||||
self.init(peerId: PeerId(peerIdValue), namespace: namespaceValue, id: idValue)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 10.0, *)
|
||||
func getMessages(account: Account, ids: [MessageId]) -> Signal<[INMessage], NoError> {
|
||||
return account.postbox.transaction { transaction -> [INMessage] in
|
||||
var messages: [INMessage] = []
|
||||
for id in ids {
|
||||
if let message = transaction.getMessage(id).flatMap(messageWithTelegramMessage) {
|
||||
messages.append(message)
|
||||
}
|
||||
}
|
||||
return messages.sorted { $0.dateSent!.compare($1.dateSent!) == .orderedDescending }
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 10.0, *)
|
||||
func unreadMessages(account: Account) -> Signal<[INMessage], NoError> {
|
||||
return account.postbox.tailChatListView(groupId: .root, count: 20, summaryComponents: ChatListEntrySummaryComponents())
|
||||
|> take(1)
|
||||
|> mapToSignal { view -> Signal<[INMessage], NoError> in
|
||||
var signals: [Signal<[INMessage], NoError>] = []
|
||||
for entry in view.0.entries {
|
||||
if case let .MessageEntry(index, _, readState, notificationSettings, _, _, _, _, _) = entry {
|
||||
if index.messageIndex.id.peerId.namespace != Namespaces.Peer.CloudUser {
|
||||
continue
|
||||
}
|
||||
|
||||
var hasUnread = false
|
||||
var fixedCombinedReadStates: MessageHistoryViewReadState?
|
||||
if let readState = readState {
|
||||
hasUnread = readState.count != 0
|
||||
fixedCombinedReadStates = .peer([index.messageIndex.id.peerId: readState])
|
||||
}
|
||||
var isMuted = false
|
||||
if let notificationSettings = notificationSettings as? TelegramPeerNotificationSettings {
|
||||
if case let .muted(until) = notificationSettings.muteState, until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
|
||||
isMuted = true
|
||||
}
|
||||
}
|
||||
|
||||
if !isMuted && hasUnread {
|
||||
signals.append(account.postbox.aroundMessageHistoryViewForLocation(.peer(index.messageIndex.id.peerId), anchor: .upperBound, count: 10, fixedCombinedReadStates: fixedCombinedReadStates, topTaggedMessageIdNamespaces: Set(), tagMask: nil, namespaces: .not(Namespaces.Message.allScheduled), orderStatistics: .combinedLocation)
|
||||
|> take(1)
|
||||
|> map { view -> [INMessage] in
|
||||
var messages: [INMessage] = []
|
||||
for entry in view.0.entries {
|
||||
var isRead = true
|
||||
if let readState = readState {
|
||||
isRead = readState.isIncomingMessageIndexRead(entry.message.index)
|
||||
}
|
||||
|
||||
if !isRead {
|
||||
if let message = messageWithTelegramMessage(entry.message) {
|
||||
messages.append(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
return messages
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if signals.isEmpty {
|
||||
return .single([])
|
||||
} else {
|
||||
return combineLatest(signals)
|
||||
|> map { results -> [INMessage] in
|
||||
return results.flatMap { $0 }.sorted { $0.dateSent!.compare($1.dateSent!) == .orderedDescending }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 10.0, *)
|
||||
struct CallRecord {
|
||||
let identifier: String
|
||||
let date: Date
|
||||
let caller: INPerson
|
||||
let duration: Int32?
|
||||
let unseen: Bool
|
||||
|
||||
@available(iOSApplicationExtension 11.0, *)
|
||||
var intentCall: INCallRecord {
|
||||
return INCallRecord(identifier: self.identifier, dateCreated: self.date, caller: self.caller, callRecordType: .missed, callCapability: .audioCall, callDuration: self.duration.flatMap(Double.init), unseen: self.unseen)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 10.0, *)
|
||||
func missedCalls(account: Account) -> Signal<[CallRecord], NoError> {
|
||||
return account.viewTracker.callListView(type: .missed, index: MessageIndex.absoluteUpperBound(), count: 30)
|
||||
|> take(1)
|
||||
|> map { view -> [CallRecord] in
|
||||
var calls: [CallRecord] = []
|
||||
for entry in view.entries {
|
||||
switch entry {
|
||||
case let .message(_, messages):
|
||||
for message in messages {
|
||||
if let call = callWithTelegramMessage(message, account: account) {
|
||||
calls.append(call)
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return calls.sorted { $0.date.compare($1.date) == .orderedDescending }
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 10.0, *)
|
||||
private func callWithTelegramMessage(_ telegramMessage: Message, account: Account) -> CallRecord? {
|
||||
guard let author = telegramMessage.author, let user = telegramMessage.peers[author.id] as? TelegramUser else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let identifier = "\(telegramMessage.id.peerId.toInt64())_\(telegramMessage.id.namespace)_\(telegramMessage.id.id)"
|
||||
let personHandle: INPersonHandle
|
||||
if #available(iOSApplicationExtension 10.2, *) {
|
||||
var type: INPersonHandleType
|
||||
var label: INPersonHandleLabel?
|
||||
if let username = user.username {
|
||||
label = INPersonHandleLabel(rawValue: "@\(username)")
|
||||
type = .unknown
|
||||
} else if let phone = user.phone {
|
||||
label = INPersonHandleLabel(rawValue: formatPhoneNumber(phone))
|
||||
type = .phoneNumber
|
||||
} else {
|
||||
label = nil
|
||||
type = .unknown
|
||||
}
|
||||
personHandle = INPersonHandle(value: user.phone ?? "", type: type, label: label)
|
||||
} else {
|
||||
personHandle = INPersonHandle(value: user.phone ?? "", type: .phoneNumber)
|
||||
}
|
||||
|
||||
let caller = INPerson(personHandle: personHandle, nameComponents: nil, displayName: user.nameOrPhone, image: nil, contactIdentifier: nil, customIdentifier: "tg\(user.id.toInt64())")
|
||||
let date = Date(timeIntervalSince1970: TimeInterval(telegramMessage.timestamp))
|
||||
|
||||
var duration: Int32?
|
||||
for media in telegramMessage.media {
|
||||
if let action = media as? TelegramMediaAction, case let .phoneCall(_, _, callDuration) = action.action {
|
||||
duration = callDuration
|
||||
}
|
||||
}
|
||||
|
||||
return CallRecord(identifier: identifier, date: date, caller: caller, duration: duration, unseen: true)
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 10.0, *)
|
||||
private func messageWithTelegramMessage(_ telegramMessage: Message) -> INMessage? {
|
||||
guard let author = telegramMessage.author, let user = telegramMessage.peers[author.id] as? TelegramUser, user.id.id != 777000 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let identifier = "\(telegramMessage.id.peerId.toInt64())_\(telegramMessage.id.namespace)_\(telegramMessage.id.id)"
|
||||
let personHandle: INPersonHandle
|
||||
if #available(iOSApplicationExtension 10.2, *) {
|
||||
var type: INPersonHandleType
|
||||
var label: INPersonHandleLabel?
|
||||
if let username = user.username {
|
||||
label = INPersonHandleLabel(rawValue: "@\(username)")
|
||||
type = .unknown
|
||||
} else if let phone = user.phone {
|
||||
label = INPersonHandleLabel(rawValue: formatPhoneNumber(phone))
|
||||
type = .phoneNumber
|
||||
} else {
|
||||
label = nil
|
||||
type = .unknown
|
||||
}
|
||||
personHandle = INPersonHandle(value: user.phone ?? "", type: type, label: label)
|
||||
} else {
|
||||
personHandle = INPersonHandle(value: user.phone ?? "", type: .phoneNumber)
|
||||
}
|
||||
|
||||
let personIdentifier = "tg\(user.id.toInt64())"
|
||||
let sender = INPerson(personHandle: personHandle, nameComponents: nil, displayName: user.nameOrPhone, image: nil, contactIdentifier: personIdentifier, customIdentifier: personIdentifier)
|
||||
let date = Date(timeIntervalSince1970: TimeInterval(telegramMessage.timestamp))
|
||||
|
||||
let message: INMessage
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
var messageType: INMessageType = .text
|
||||
loop: for media in telegramMessage.media {
|
||||
if media is TelegramMediaImage {
|
||||
messageType = .mediaImage
|
||||
break loop
|
||||
}
|
||||
else if let file = media as? TelegramMediaFile {
|
||||
if file.isVideo {
|
||||
messageType = .mediaVideo
|
||||
break loop
|
||||
} else if file.isMusic {
|
||||
messageType = .mediaAudio
|
||||
break loop
|
||||
} else if file.isVoice {
|
||||
messageType = .mediaAudio
|
||||
break loop
|
||||
} else if file.isSticker || file.isAnimatedSticker {
|
||||
messageType = .sticker
|
||||
break loop
|
||||
} else if file.isAnimated {
|
||||
messageType = .mediaVideo
|
||||
break loop
|
||||
} else if #available(iOSApplicationExtension 12.0, *) {
|
||||
messageType = .file
|
||||
break loop
|
||||
}
|
||||
} else if media is TelegramMediaMap {
|
||||
messageType = .mediaLocation
|
||||
break loop
|
||||
} else if media is TelegramMediaContact {
|
||||
messageType = .mediaAddressCard
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
if telegramMessage.text.isEmpty && messageType == .text {
|
||||
return nil
|
||||
}
|
||||
|
||||
message = INMessage(identifier: identifier, conversationIdentifier: "\(telegramMessage.id.peerId.toInt64())", content: telegramMessage.text, dateSent: date, sender: sender, recipients: [], groupName: nil, messageType: messageType)
|
||||
} else {
|
||||
if telegramMessage.text.isEmpty {
|
||||
return nil
|
||||
}
|
||||
message = INMessage(identifier: identifier, content: telegramMessage.text, dateSent: date, sender: sender, recipients: [])
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
10
Telegram/SiriIntents/SiriIntents-AppStore.entitlements
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.telegram.TelegramHD</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
10
Telegram/SiriIntents/SiriIntents-AppStoreLLC.entitlements
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.ph.telegra.Telegraph</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
4
Telegram/SiriIntents/SiriIntents-Bridging-Header.h
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#ifndef SiriIntents_Bridging_Header_h
|
||||
#define SiriIntents_Bridging_Header_h
|
||||
|
||||
#endif
|
||||
10
Telegram/SiriIntents/SiriIntents-Fork.entitlements
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.fork.telegram.Telegram-iOS</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
10
Telegram/SiriIntents/SiriIntents-Hockeyapp.entitlements
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.org.telegram.Telegram-iOS</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
1
Telegram/SupportFiles/Empty.swift
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon4@40x40-2.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon4@60x60.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon4@58x58-2.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon4@87x87.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon4@80x80-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon4@120x120-1.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon4@120x120.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon4@180x180.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon4@20x20.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon4@40x40.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon4@29x29.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon4@58x58.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon4@40x40-1.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon4@80x80.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon4@76x76.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon4@152x152.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon4@167x167.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"size" : "1024x1024",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 6 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 555 B |
|
After Width: | Height: | Size: 831 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3 KiB |
|
|
@ -0,0 +1,115 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon2@40x40.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon2@60x60.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon2@58x58.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon2@87x87.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon2@80x80.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon2@120x120.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon2@120x120-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon2@180x180.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon2@20x20.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon2@40x40-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon2@29x29.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon2@58x58-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon2@40x40-2.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon2@80x80-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon2@76x76.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon2@152x152.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon2@167x167.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"size" : "1024x1024",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 592 B |
|
After Width: | Height: | Size: 929 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2 KiB |
|
After Width: | Height: | Size: 2 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3 KiB |
|
After Width: | Height: | Size: 3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
|
@ -0,0 +1,115 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon3@40x40.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon3@60x60.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon3@58x58.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon3@87x87.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon3@80x80.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon3@120x120.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon3@120x120-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon3@180x180.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon3@20x20.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon3@40x40-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon3@29x29.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon3@58x58-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon3@40x40-2.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon3@80x80-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon3@76x76.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon3@152x152.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon3@167x167.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"size" : "1024x1024",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 549 B |
|
After Width: | Height: | Size: 802 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |