Files
Swiftgram/submodules/TelegramUI/Sources/OpenUrl.swift
2026-02-09 22:35:14 +02:00

1095 lines
51 KiB
Swift

import SGLogging
import SGAPIWebSettings
import SGConfig
import SGSettingsUI
import SGDebugUI
import SFSafariViewControllerPlus
import UndoUI
//
import ContactListUI
import Foundation
import Display
import SafariServices
import TelegramCore
import Postbox
import SwiftSignalKit
import MtProtoKit
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import UrlEscaping
import PassportUI
import UrlHandling
import OpenInExternalAppUI
import BrowserUI
import OverlayStatusController
import PresentationDataUtils
public struct ParsedSecureIdUrl {
public let peerId: PeerId
public let scope: String
public let publicKey: String
public let callbackUrl: String
public let opaquePayload: Data
public let opaqueNonce: Data
}
public func parseProxyUrl(sharedContext: SharedAccountContext, url: URL) -> ProxyServerSettings? {
guard let proxy = parseProxyUrl(sharedContext: sharedContext, url: url.absoluteString) else {
return nil
}
if let secret = proxy.secret, let _ = MTProxySecret.parseData(secret) {
return ProxyServerSettings(host: proxy.host, port: proxy.port, connection: .mtp(secret: secret))
} else {
return ProxyServerSettings(host: proxy.host, port: proxy.port, connection: .socks5(username: proxy.username, password: proxy.password))
}
}
public func isOAuthUrl(_ url: URL) -> Bool {
guard let query = url.query, let params = QueryParameters(query), ["oauth", "resolve"].contains(url.host) else {
return false
}
let domain = params["domain"]
let startApp = params["startapp"]
let token = params["token"]
var valid = false
if url.host == "resolve" {
if domain == "oauth", let _ = startApp {
valid = true
}
} else {
if let _ = token {
valid = true
}
}
return valid
}
public func parseSecureIdUrl(_ url: URL) -> ParsedSecureIdUrl? {
guard let query = url.query, let params = QueryParameters(query), ["passport", "resolve"].contains(url.host) else {
return nil
}
let domain = params["domain"]
let botId = params["bot_id"].flatMap(Int64.init)
let scope = params["scope"]
let publicKey = params["public_key"]
let callbackUrl = params["callback_url"]
var opaquePayload = Data()
var opaqueNonce = Data()
if let payloadValue = params["payload"], let data = payloadValue.data(using: .utf8) {
opaquePayload = data
}
if let nonceValue = params["nonce"], let data = nonceValue.data(using: .utf8) {
opaqueNonce = data
}
let valid: Bool
if url.host == "resolve" {
if domain == "telegrampassport" {
valid = true
} else {
valid = false
}
} else {
valid = true
}
if valid {
if let botId = botId, let scope = scope, let publicKey = publicKey, let callbackUrl = callbackUrl {
if scope.hasPrefix("{") && scope.hasSuffix("}") {
opaquePayload = Data()
if opaqueNonce.isEmpty {
return nil
}
} else if opaquePayload.isEmpty {
return nil
}
return ParsedSecureIdUrl(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(botId)), scope: scope, publicKey: publicKey, callbackUrl: callbackUrl, opaquePayload: opaquePayload, opaqueNonce: opaqueNonce)
}
}
return nil
}
public func parseConfirmationCodeUrl(sharedContext: SharedAccountContext, url: URL) -> Int? {
if url.pathComponents.count == 3 && url.pathComponents[1].lowercased() == "login" {
if let code = Int(url.pathComponents[2]) {
return code
}
}
if url.scheme == "tg" {
if let host = url.host, let query = url.query, let parsedUrl = parseInternalUrl(sharedContext: sharedContext, context: nil, query: host + "?" + query) {
switch parsedUrl {
case let .confirmationCode(code):
return code
default:
break
}
}
}
return nil
}
func formattedConfirmationCode(_ code: Int) -> String {
let source = "\(code)"
let segmentLength = 3
var result = ""
for c in source {
if !result.isEmpty && result.count % segmentLength == 0 {
result.append("-")
}
result.append(c)
}
return result
}
private func canonicalExternalUrl(from url: String) -> URL? {
var urlWithScheme = url
if !url.contains("://") && !url.hasPrefix("mailto:") {
urlWithScheme = "http://" + url
}
if let parsed = URL(string: urlWithScheme) {
return parsed
} else if let encoded = (urlWithScheme as NSString).addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed) {
return URL(string: encoded)
}
return nil
}
private func makeResolvedUrlHandler(
context: AccountContext,
presentationData: PresentationData,
navigationController: NavigationController?,
dismissInput: @escaping () -> Void
) -> (ResolvedUrl) -> Void {
return { resolved in
if case let .externalUrl(value) = resolved {
context.sharedContext.applicationBindings.openUrl(value)
} else {
context.sharedContext.openResolvedUrl(
resolved,
context: context,
urlContext: .generic,
navigationController: navigationController,
forceExternal: false,
forceUpdate: false,
openPeer: { peer, navigation in
switch navigation {
case .info:
if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
context.sharedContext.applicationBindings.dismissNativeController()
navigationController?.pushViewController(infoController)
}
case let .chat(textInputState, subject, peekData):
context.sharedContext.applicationBindings.dismissNativeController()
if let navigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: subject, updateTextInputState: !peer.id.isGroupOrChannel ? textInputState : nil, peekData: peekData))
}
case let .withBotStartPayload(payload):
context.sharedContext.applicationBindings.dismissNativeController()
if let navigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), botStart: payload))
}
case let .withAttachBot(attachBotStart):
context.sharedContext.applicationBindings.dismissNativeController()
if let navigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), attachBotStart: attachBotStart))
}
case let .withBotApp(botAppStart):
context.sharedContext.applicationBindings.dismissNativeController()
if let navigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), botAppStart: botAppStart))
}
default:
break
}
},
sendFile: nil,
sendSticker: nil,
sendEmoji: nil,
requestMessageActionUrlAuth: nil,
joinVoiceChat: { _, _, _ in },
present: { c, a in
context.sharedContext.applicationBindings.dismissNativeController()
c.presentationArguments = a
context.sharedContext.applicationBindings.getWindowHost()?.present(c, on: .root, blockInteraction: false, completion: {})
},
dismissInput: {
dismissInput()
},
contentContext: nil,
progress: nil,
completion: nil
)
}
}
}
private func makeInternalUrlHandler(
context: AccountContext,
resolvedHandler: @escaping (ResolvedUrl) -> Void
) -> (String) -> Void {
return { url in
let _ = (context.sharedContext.resolveUrl(context: context, peerId: nil, url: url, skipUrlAuth: true)
|> deliverOnMainQueue).startStandalone(next: resolvedHandler)
}
}
private let internetSchemes: [String] = ["http", "https"]
private let telegramMeHosts: [String] = ["t.me", "telegram.me", "telegram.dog"]
private func handleInternetUrl(
parsedUrl: URL,
originalUrl: String,
context: AccountContext,
presentationData: PresentationData,
navigationController: NavigationController?,
handleInternalUrl: @escaping (String) -> Void
) {
let urlScheme = (parsedUrl.scheme ?? "").lowercased()
var isInternetUrl = false
if internetSchemes.contains(urlScheme) {
isInternetUrl = true
}
if urlScheme == "tonsite" {
isInternetUrl = true
}
if isInternetUrl {
if let host = parsedUrl.host, telegramMeHosts.contains(host) {
handleInternalUrl(parsedUrl.absoluteString)
} else {
let settings = combineLatest(context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings, ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]), context.sharedContext.accountManager.accessChallengeData())
|> take(1)
|> map { sharedData, accessChallengeData -> WebBrowserSettings in
let passcodeSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]?.get(PresentationPasscodeSettings.self) ?? PresentationPasscodeSettings.defaultSettings
var settings: WebBrowserSettings
if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.webBrowserSettings]?.get(WebBrowserSettings.self) {
settings = current
} else {
settings = .defaultSettings
}
if accessChallengeData.data.isLockable {
if passcodeSettings.autolockTimeout != nil && settings.defaultWebBrowser == "inApp" {
settings = WebBrowserSettings(defaultWebBrowser: "safari", exceptions: [])
}
}
return settings
}
let _ = (settings
|> deliverOnMainQueue).startStandalone(next: { settings in
var isTonSite = false
if let host = parsedUrl.host, host.lowercased().hasSuffix(".ton") {
isTonSite = true
} else if let scheme = parsedUrl.scheme, scheme.lowercased().hasPrefix("tonsite") {
isTonSite = true
}
if let defaultWebBrowser = settings.defaultWebBrowser, defaultWebBrowser != "inApp" && !isTonSite {
let openInOptions = availableOpenInOptions(context: context, item: .url(url: originalUrl))
if let option = openInOptions.first(where: { $0.identifier == settings.defaultWebBrowser }) {
if case let .openUrl(openInUrl) = option.action() {
context.sharedContext.applicationBindings.openUrl(openInUrl)
} else {
context.sharedContext.applicationBindings.openUrl(originalUrl)
}
} else {
context.sharedContext.applicationBindings.openUrl(originalUrl)
}
} else {
var isExceptedDomain = false
let host = ".\((parsedUrl.host ?? "").lowercased())"
for exception in settings.exceptions {
if host.hasSuffix(".\(exception.domain)") {
isExceptedDomain = true
break
}
}
if settings.defaultWebBrowser == "inApp" { isExceptedDomain = false } // MARK: Swiftgram
if (settings.defaultWebBrowser == nil && !isExceptedDomain) || isTonSite {
let controller = BrowserScreen(context: context, subject: .webPage(url: parsedUrl.absoluteString))
navigationController?.pushViewController(controller)
} else {
if let window = navigationController?.view.window, !isExceptedDomain {
let controller = SFSafariViewControllerPlusDidFinish(url: parsedUrl) // MARK: Swiftgram
controller.preferredBarTintColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor
controller.preferredControlTintColor = presentationData.theme.rootController.navigationBar.accentTextColor
// MARK: Swiftgram
if parsedUrl.host?.lowercased() == SG_API_WEBAPP_URL_PARSED.host?.lowercased() {
controller.onDidFinish = {
SGLogger.shared.log("SafariController", "Closed webapp")
updateSGWebSettingsInteractivelly(context: context)
}
}
//
window.rootViewController?.present(controller, animated: true)
} else {
context.sharedContext.applicationBindings.openUrl(parsedUrl.absoluteString)
}
}
}
})
}
} else {
context.sharedContext.applicationBindings.openUrl(originalUrl)
}
}
private struct QueryParameters {
private let map: [String: [String?]]
let items: [URLQueryItem]
init?(_ query: String) {
guard let components = URLComponents(string: "/?" + query) else {
return nil
}
let queryItems = components.queryItems ?? []
self.items = queryItems
var map: [String: [String?]] = [:]
for item in queryItems {
map[item.name, default: []].append(item.value)
}
self.map = map
}
subscript(_ name: String) -> String? {
return self.map[name]?.first ?? nil
}
}
private func appendQueryItems(to base: String, items: [URLQueryItem]) -> String {
guard !items.isEmpty else {
return base
}
var components = URLComponents()
components.queryItems = items
guard let query = components.percentEncodedQuery, !query.isEmpty else {
return base
}
let separator = base.contains("?") ? "&" : "?"
return base + separator + query
}
private func makeTelegramUrl(_ path: String, queryItems: [URLQueryItem] = []) -> String {
return appendQueryItems(to: "https://t.me\(path)", items: queryItems)
}
func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void) {
if forceExternal || url.lowercased().hasPrefix("tel:") || url.lowercased().hasPrefix("calshow:") {
if url.lowercased().hasPrefix("tel:+888") {
context.sharedContext.presentGlobalController(textAlertController(context: context, title: nil, text: presentationData.strings.Conversation_CantPhoneCallAnonymousNumberError, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {
}),
], parseMarkdown: true), nil)
return
}
context.sharedContext.applicationBindings.openUrl(url)
return
}
guard let canonicalUrl = canonicalExternalUrl(from: url) else {
return
}
if canonicalUrl.scheme == "mailto" {
context.sharedContext.applicationBindings.openUrl(url)
return
}
var parsedUrl = canonicalUrl
if let host = parsedUrl.host?.lowercased() {
if host == "itunes.apple.com" {
if context.sharedContext.applicationBindings.canOpenUrl(parsedUrl.absoluteString) {
context.sharedContext.applicationBindings.openUrl(url)
return
}
}
if host == "twitter.com" || host == "mobile.twitter.com" {
if context.sharedContext.applicationBindings.canOpenUrl("twitter://status") {
context.sharedContext.applicationBindings.openUrl(url)
return
}
} else if host == "instagram.com" {
if context.sharedContext.applicationBindings.canOpenUrl("instagram://photo") {
context.sharedContext.applicationBindings.openUrl(url)
return
}
}
}
let handleResolvedUrl = makeResolvedUrlHandler(
context: context,
presentationData: presentationData,
navigationController: navigationController,
dismissInput: dismissInput
)
let handleInternalUrl = makeInternalUrlHandler(
context: context,
resolvedHandler: handleResolvedUrl
)
let continueHandling: () -> Void = {
if let scheme = parsedUrl.scheme, (scheme == "tg" || scheme == context.sharedContext.applicationBindings.appSpecificScheme) {
if parsedUrl.host == "tonsite" {
if let value = URL(string: "tonsite:/" + parsedUrl.path) {
parsedUrl = value
}
}
}
if let scheme = parsedUrl.scheme, (scheme == "tg" || scheme == context.sharedContext.applicationBindings.appSpecificScheme) {
var convertedUrl: String?
let host = parsedUrl.host?.lowercased() ?? ""
if let query = parsedUrl.query, let params = QueryParameters(query) {
switch host {
case "localpeer":
if let peerIdValue = params["id"].flatMap(Int64.init), let accountId = params["accountId"].flatMap(Int64.init) {
let peerId = PeerId(peerIdValue)
context.sharedContext.applicationBindings.dismissNativeController()
context.sharedContext.navigateToChat(accountId: AccountRecordId(rawValue: accountId), peerId: peerId, messageId: nil)
}
case "join":
if let invite = params["invite"] {
convertedUrl = makeTelegramUrl("/joinchat/\(invite)")
}
case "addstickers":
if let set = params["set"] {
convertedUrl = makeTelegramUrl("/addstickers/\(set)")
}
case "addemoji":
if let set = params["set"] {
convertedUrl = makeTelegramUrl("/addemoji/\(set)")
}
case "invoice":
if let slug = params["slug"] {
convertedUrl = makeTelegramUrl("/invoice/\(slug)")
}
case "setlanguage":
if let lang = params["lang"] {
convertedUrl = makeTelegramUrl("/setlanguage/\(lang)")
}
case "msg":
let sharePhoneNumber = params["to"]
let shareText = params["text"]
if sharePhoneNumber != nil || shareText != nil {
handleResolvedUrl(.share(url: nil, text: shareText, to: sharePhoneNumber))
return
}
case "msg_url":
if let shareUrl = params["url"] {
var queryItems: [URLQueryItem] = [URLQueryItem(name: "url", value: shareUrl)]
if let shareText = params["text"] {
queryItems.append(URLQueryItem(name: "text", value: shareText))
}
convertedUrl = makeTelegramUrl("/share/url", queryItems: queryItems)
}
case "socks", "proxy":
let server = params["server"] ?? params["proxy"]
let port = params["port"]
let user = params["user"]
let pass = params["pass"]
let secret = params["secret"]
let secretHost = params["host"]
if let server, !server.isEmpty, let port, let _ = Int32(port) {
var queryItems: [URLQueryItem] = [
URLQueryItem(name: "proxy", value: server),
URLQueryItem(name: "port", value: port)
]
if let user {
queryItems.append(URLQueryItem(name: "user", value: user))
if let pass {
queryItems.append(URLQueryItem(name: "pass", value: pass))
}
}
if let secret {
queryItems.append(URLQueryItem(name: "secret", value: secret))
}
if let secretHost {
queryItems.append(URLQueryItem(name: "host", value: secretHost))
}
convertedUrl = makeTelegramUrl("/proxy", queryItems: queryItems)
}
case "passport", "oauth", "resolve":
if isOAuthUrl(parsedUrl) {
handleResolvedUrl(.oauth(url: url))
return
} else if let secureId = parseSecureIdUrl(parsedUrl) {
if case .chat = urlContext {
return
}
let controller = SecureIdAuthController(context: context, mode: .form(peerId: secureId.peerId, scope: secureId.scope, publicKey: secureId.publicKey, callbackUrl: secureId.callbackUrl, opaquePayload: secureId.opaquePayload, opaqueNonce: secureId.opaqueNonce))
if let navigationController = navigationController {
context.sharedContext.applicationBindings.dismissNativeController()
navigationController.view.window?.endEditing(true)
context.sharedContext.applicationBindings.getWindowHost()?.present(controller, on: .root, blockInteraction: false, completion: {})
}
return
}
case "user":
if let idValue = params["id"].flatMap(Int64.init), idValue > 0 {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(idValue))))
|> deliverOnMainQueue).startStandalone(next: { peer in
if let peer = peer, let controller = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
mode: .generic,
avatarInitiallyExpanded: false,
fromChat: false,
requestsContext: nil
) {
navigationController?.pushViewController(controller)
}
})
return
}
case "login":
if let _ = params["token"] {
let alertController = textAlertController(
context: context,
title: nil,
text: presentationData.strings.AuthSessions_AddDevice_UrlLoginHint,
actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {})],
parseMarkdown: true
)
context.sharedContext.presentGlobalController(alertController, nil)
return
}
if let code = params["code"] {
convertedUrl = makeTelegramUrl("/login/\(code)")
}
case "contact":
if let token = params["token"] {
convertedUrl = makeTelegramUrl("/contact/\(token)")
}
case "confirmphone":
if let phone = params["phone"], let hash = params["hash"] {
let queryItems = [
URLQueryItem(name: "phone", value: phone),
URLQueryItem(name: "hash", value: hash)
]
convertedUrl = makeTelegramUrl("/confirmphone", queryItems: queryItems)
}
case "bg":
var parameter: String?
var queryItems: [URLQueryItem] = []
for item in params.items {
guard let value = item.value else {
continue
}
switch item.name {
case "slug", "color", "gradient":
parameter = value
case "mode", "bg_color", "intensity", "rotation":
queryItems.append(URLQueryItem(name: item.name, value: value))
default:
break
}
}
if let parameter = parameter {
convertedUrl = makeTelegramUrl("/bg/\(parameter)", queryItems: queryItems)
}
case "addtheme":
if let parameter = params["slug"] {
convertedUrl = makeTelegramUrl("/addtheme/\(parameter)")
}
case "nft":
if let slug = params["slug"] {
convertedUrl = makeTelegramUrl("/nft/\(slug)")
}
case "stargift_auction":
if let slug = params["slug"] {
convertedUrl = makeTelegramUrl("/auction/\(slug)")
}
case "privatepost":
let channelId = params["channel"].flatMap(Int64.init)
let postId = params["post"].flatMap(Int32.init)
let threadId = params["thread"].flatMap(Int64.init)
if let channelId {
if let postId {
if let threadId {
convertedUrl = makeTelegramUrl("/c/\(channelId)/\(threadId)/\(postId)")
} else {
convertedUrl = makeTelegramUrl("/c/\(channelId)/\(postId)")
}
} else if let threadId {
convertedUrl = makeTelegramUrl("/c/\(channelId)/\(threadId)")
}
}
case "giftcode":
if let slug = params["slug"] {
convertedUrl = makeTelegramUrl("/giftcode/\(slug)")
}
case "message":
if let parameter = params["slug"] {
convertedUrl = makeTelegramUrl("/m/\(parameter)")
}
case "hostoverride":
if let override = params["host"] {
let _ = updateNetworkSettingsInteractively(postbox: context.account.postbox, network: context.account.network, { settings in
var settings = settings
settings.backupHostOverride = override
return settings
}).startStandalone()
return
}
case "premium_offer":
let reference = params["ref"]
handleResolvedUrl(.premiumOffer(reference: reference))
case "premium_multigift":
let reference = params["ref"]
handleResolvedUrl(.premiumMultiGift(reference: reference))
case "stars_topup":
let amount = params["balance"].flatMap(Int64.init)
let purpose = params["purpose"]
if let amount, amount > 0 && amount < Int64(Int32.max) {
handleResolvedUrl(.starsTopup(amount: amount, purpose: purpose))
} else {
handleResolvedUrl(.starsTopup(amount: nil, purpose: purpose))
}
case "addlist":
if let slug = params["slug"] {
convertedUrl = makeTelegramUrl("/addlist/\(slug)")
}
case "boost":
if let domain = params["domain"] {
convertedUrl = makeTelegramUrl("/\(domain)", queryItems: [URLQueryItem(name: "boost", value: nil)])
} else if let channel = params["channel"].flatMap(Int64.init) {
convertedUrl = makeTelegramUrl("/c/\(channel)", queryItems: [URLQueryItem(name: "boost", value: nil)])
}
case "call":
if let slug = params["slug"] {
convertedUrl = makeTelegramUrl("/call/\(slug)")
}
case "sharestory":
if let session = params["session"].flatMap(Int64.init) {
handleResolvedUrl(.shareStory(session))
return
}
case "send_gift":
if let recipient = params["to"] {
if let id = Int64(recipient) {
handleResolvedUrl(.sendGift(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))))
} else {
let _ = (context.engine.peers.resolvePeerByName(name: recipient, referrer: nil)
|> deliverOnMainQueue).start(next: { result in
guard case let .result(peer) = result, let peer else {
return
}
handleResolvedUrl(.sendGift(peerId: peer.id))
})
}
} else {
handleResolvedUrl(.sendGift(peerId: nil))
}
default:
break
}
if host == "resolve" {
var phone: String?
var domain: String?
var start: String?
var startGroup: String?
var startChannel: String?
var admin: String?
var game: String?
var post: String?
var voiceChat: String?
var attach: String?
var startAttach: String?
var choose: String?
var threadId: Int64?
var appName: String?
var startApp: String?
var text: String?
var profile = false
var direct = false
var referrer: String?
var albumId: Int64?
var collectionId: Int64?
for queryItem in params.items {
if let value = queryItem.value {
switch queryItem.name {
case "phone":
phone = value
case "domain":
domain = value
case "start":
start = value
case "startgroup":
startGroup = value
case "admin":
admin = value
case "game":
game = value
case "post":
post = value
case "voicechat", "videochat", "livestream":
voiceChat = value
case "attach":
attach = value
case "startattach":
startAttach = value
case "choose":
choose = value
case "thread":
threadId = Int64(value)
case "appname":
appName = value
case "startapp":
startApp = value
case "text":
text = value
case "ref":
referrer = value
case "album":
albumId = Int64(value)
case "collection":
collectionId = Int64(value)
default:
break
}
} else {
switch queryItem.name {
case "voicechat", "videochat", "livestream":
voiceChat = ""
case "startattach":
startAttach = ""
case "startgroup":
startGroup = ""
case "startchannel":
startChannel = ""
case "profile":
profile = true
case "direct":
direct = true
case "startapp":
startApp = ""
default:
break
}
}
}
if let phone = phone {
var queryItems: [URLQueryItem] = []
if let text {
queryItems.append(URLQueryItem(name: "text", value: text))
}
if let referrer {
queryItems.append(URLQueryItem(name: "ref", value: referrer))
}
if profile {
queryItems.append(URLQueryItem(name: "profile", value: nil))
}
if direct {
queryItems.append(URLQueryItem(name: "direct", value: nil))
}
convertedUrl = makeTelegramUrl("/+\(phone)", queryItems: queryItems)
} else if let domain = domain {
var path = "/\(domain)"
if let appName {
path += "/\(appName)"
}
if let threadId {
path += "/\(threadId)"
if let post, let postValue = Int(post) {
path += "/\(postValue)"
}
} else if let post, let postValue = Int(post) {
path += "/\(postValue)"
}
if let albumId {
path += "/a/\(albumId)"
} else if let collectionId {
path += "/c/\(collectionId)"
}
var queryItems: [URLQueryItem] = []
if let startApp {
queryItems.append(URLQueryItem(name: "startapp", value: startApp.isEmpty ? "" : startApp))
}
if let start {
queryItems.append(URLQueryItem(name: "start", value: start))
} else if let startGroup {
queryItems.append(URLQueryItem(name: "startgroup", value: startGroup.isEmpty ? nil : startGroup))
if let admin {
queryItems.append(URLQueryItem(name: "admin", value: admin))
}
} else if let startChannel {
queryItems.append(URLQueryItem(name: "startchannel", value: startChannel.isEmpty ? nil : startChannel))
if let admin = admin {
queryItems.append(URLQueryItem(name: "admin", value: admin))
}
} else if let game {
queryItems.append(URLQueryItem(name: "game", value: game))
} else if let voiceChat {
queryItems.append(URLQueryItem(name: "voicechat", value: voiceChat.isEmpty ? "" : voiceChat))
} else if let attach {
queryItems.append(URLQueryItem(name: "attach", value: attach))
}
if let startAttach {
queryItems.append(URLQueryItem(name: "startattach", value: startAttach.isEmpty ? nil : startAttach))
if let choose {
queryItems.append(URLQueryItem(name: "choose", value: choose))
}
}
if let text {
queryItems.append(URLQueryItem(name: "text", value: text))
}
if let referrer {
queryItems.append(URLQueryItem(name: "ref", value: referrer))
}
if profile {
queryItems.append(URLQueryItem(name: "profile", value: nil))
}
if direct {
queryItems.append(URLQueryItem(name: "direct", value: nil))
}
convertedUrl = makeTelegramUrl(path, queryItems: queryItems)
}
}
} else {
switch host {
case "stars":
handleResolvedUrl(.stars)
case "sg":
if let path = parsedUrl.pathComponents.last {
switch path {
case "debug":
if let debugController = context.sharedContext.makeDebugSettingsController(context: context) {
navigationController?.pushViewController(debugController)
return
}
case "sgdebug", "sg_debug":
navigationController?.pushViewController(sgDebugController(context: context))
return
case "settings":
navigationController?.pushViewController(sgSettingsController(context: context))
return
case "ios_settings":
context.sharedContext.applicationBindings.openSettings()
return
case "contacts":
if let lastViewController = navigationController?.viewControllers.last as? ViewController {
lastViewController.present(ContactsController(context: context), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
return
case "pro", "premium", "buy":
if context.sharedContext.immediateSGStatus.status > 1 {
navigationController?.pushViewController(context.sharedContext.makeSGProController(context: context))
} else {
if let lastViewController = navigationController?.viewControllers.last as? ViewController {
if let payWallController = context.sharedContext.makeSGPayWallController(context: context) {
lastViewController.present(payWallController, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} else {
lastViewController.present(context.sharedContext.makeSGUpdateIOSController(), animated: true)
}
}
}
case "restart":
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let lang = presentationData.strings.baseLanguageCode
context.sharedContext.presentGlobalController(
UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil,
text: "Common.RestartRequired".i18n(lang),
timeout: nil,
customUndoText: "Common.RestartNow".i18n(lang)
),
elevatedLayout: false,
action: { action in if action == .undo { exit(0) }; return true }
),
nil
)
case "restore_purchases", "pro_restore", "validate", "restore":
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let lang = presentationData.strings.baseLanguageCode
context.sharedContext.presentGlobalController(UndoOverlayController(
presentationData: presentationData,
content: .info(title: nil, text: "PayWall.Button.Restoring".i18n(lang), timeout: nil, customUndoText: nil),
elevatedLayout: false,
action: { _ in return false }
),
nil)
context.sharedContext.SGIAP?.restorePurchases {}
default:
break
}
}
case "ton":
handleResolvedUrl(.ton)
case "importstickers":
handleResolvedUrl(.importStickers)
case "premium_offer":
handleResolvedUrl(.premiumOffer(reference: nil))
case "restore_purchases":
let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
context.sharedContext.presentGlobalController(statusController, nil)
context.inAppPurchaseManager?.restorePurchases(completion: { [weak statusController] result in
statusController?.dismiss()
let text: String?
switch result {
case let .succeed(serverProvided):
text = serverProvided ? nil : presentationData.strings.Premium_Restore_Success
case .failed:
text = presentationData.strings.Premium_Restore_ErrorUnknown
}
if let text {
let alertController = textAlertController(context: context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
context.sharedContext.presentGlobalController(alertController, nil)
}
})
case "send_gift":
handleResolvedUrl(.sendGift(peerId: nil))
case "contacts":
var section: ResolvedUrl.ContactsSection?
if let path = parsedUrl.pathComponents.last {
switch path {
case "search":
section = .search
case "sort":
section = .sort
case "new":
section = .new
case "invite":
section = .invite
case "manage":
section = .manage
default:
break
}
}
handleResolvedUrl(.contacts(section))
case "chats":
var section: ResolvedUrl.ChatsSection?
if let path = parsedUrl.pathComponents.last {
switch path {
case "search":
section = .search
case "edit":
section = .edit
case "emoji-status":
section = .emojiStatus
default:
break
}
}
handleResolvedUrl(.chats(section))
case "new":
var section: ResolvedUrl.ComposeSection?
if let path = parsedUrl.pathComponents.last {
switch path {
case "group":
section = .group
case "channel":
section = .channel
case "contact":
section = .contact
default:
break
}
}
handleResolvedUrl(.compose(section))
case "post":
var section: ResolvedUrl.PostStorySection?
if let path = parsedUrl.pathComponents.last {
switch path {
case "photo":
section = .photo
case "video":
section = .video
case "live":
section = .live
default:
break
}
}
handleResolvedUrl(.postStory(section))
case "settings":
if let lastComponent = parsedUrl.pathComponents.last {
var section: ResolvedUrl.SettingsSection?
switch lastComponent {
case "themes":
section = .legacy(.theme)
case "devices":
section = .legacy(.devices)
case "enable_log":
section = .legacy(.enableLog)
case "phone_privacy":
section = .legacy(.phonePrivacy)
case "login_email":
section = .legacy(.loginEmail)
default:
let fullPath = parsedUrl.pathComponents.joined(separator: "/").replacingOccurrences(of: "//", with: "")
section = .path(fullPath)
}
if let section {
handleResolvedUrl(.settings(section))
}
} else {
handleResolvedUrl(.settings(.path("")))
}
default:
break
}
}
if let convertedUrl {
handleInternalUrl(convertedUrl)
} else if let path = parsedUrl.host {
handleResolvedUrl(.unknownDeepLink(path: path))
}
return
}
handleInternetUrl(
parsedUrl: parsedUrl,
originalUrl: url,
context: context,
presentationData: presentationData,
navigationController: navigationController,
handleInternalUrl: handleInternalUrl
)
}
if let scheme = parsedUrl.scheme, internetSchemes.contains(scheme) {
if let host = parsedUrl.host, telegramMeHosts.contains(host) {
continueHandling()
} else {
if isTelegraPhLink(parsedUrl.absoluteString) {
continueHandling()
} else {
context.sharedContext.applicationBindings.openUniversalUrl(url, TelegramApplicationOpenUrlCompletion(completion: { success in
if !success {
continueHandling()
}
}))
}
}
} else {
continueHandling()
}
}