diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 70d94be9ba..9ee3fe6e40 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9861,3 +9861,5 @@ Sorry for the inconvenience."; "Story.ViewList.ViewerCount_1" = "1 Viewer"; "Story.ViewList.ViewerCount_any" = "%d Viewers"; + +"AuthSessions.MessageApp" = "You allowed this bot to message you when you opened %@."; diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index 5a1b471fbe..30a67f2a8a 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -1806,6 +1806,21 @@ public extension Api.functions.auth { }) } } +public extension Api.functions.bots { + static func allowSendMessage(bot: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(933102155) + bot.serialize(buffer, true) + return (FunctionDescription(name: "bots.allowSendMessage", parameters: [("bot", String(describing: bot))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.bots { static func answerWebhookJSONQuery(queryId: Int64, data: Api.DataJSON) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -1822,6 +1837,21 @@ public extension Api.functions.bots { }) } } +public extension Api.functions.bots { + static func canSendMessage(bot: Api.InputUser) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(324662502) + bot.serialize(buffer, true) + return (FunctionDescription(name: "bots.canSendMessage", parameters: [("bot", String(describing: bot))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.bots { static func getBotCommands(scope: Api.BotCommandScope, langCode: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<[Api.BotCommand]>) { let buffer = Buffer() @@ -1870,6 +1900,23 @@ public extension Api.functions.bots { }) } } +public extension Api.functions.bots { + static func invokeWebViewCustomMethod(bot: Api.InputUser, customMethod: String, params: Api.DataJSON) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(142591463) + bot.serialize(buffer, true) + serializeString(customMethod, buffer: buffer, boxed: false) + params.serialize(buffer, true) + return (FunctionDescription(name: "bots.invokeWebViewCustomMethod", parameters: [("bot", String(describing: bot)), ("customMethod", String(describing: customMethod)), ("params", String(describing: params))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.DataJSON? in + let reader = BufferReader(buffer) + var result: Api.DataJSON? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.DataJSON + } + return result + }) + } +} public extension Api.functions.bots { static func reorderUsernames(bot: Api.InputUser, order: [String]) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift index fef8fb0c46..eeaff7396f 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -50,8 +50,20 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe return TelegramMediaAction(action: .historyScreenshot) case let .messageActionCustomAction(message): return TelegramMediaAction(action: .customText(text: message, entities: [])) - case let .messageActionBotAllowed(_, domain, _): - return TelegramMediaAction(action: .botDomainAccessGranted(domain: domain ?? "")) + case let .messageActionBotAllowed(flags, domain, app): + if let domain = domain { + return TelegramMediaAction(action: .botDomainAccessGranted(domain: domain)) + } else if case let .botApp(_, _, _, _, appName, _, _, _, _) = app { + var type: BotSendMessageAccessGrantedType? + if (flags & (1 << 3)) != 0 { + type = .request + } else if (flags & (1 << 1)) != 0 { + type = .attachMenu + } + return TelegramMediaAction(action: .botAppAccessGranted(appName: appName, type: type)) + } else { + return nil + } case .messageActionSecureValuesSentMe: return nil case let .messageActionSecureValuesSent(types): diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 252e52681e..829908f62b 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift @@ -23,6 +23,11 @@ public enum SentSecureValueType: Int32 { case temporaryRegistration = 12 } +public enum BotSendMessageAccessGrantedType: Int32 { + case attachMenu = 0 + case request = 1 +} + public enum TelegramMediaActionType: PostboxCoding, Equatable { public enum ForumTopicEditComponent: PostboxCoding, Equatable { case title(String) @@ -86,6 +91,7 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case paymentSent(currency: String, totalAmount: Int64, invoiceSlug: String?, isRecurringInit: Bool, isRecurringUsed: Bool) case customText(text: String, entities: [MessageTextEntity]) case botDomainAccessGranted(domain: String) + case botAppAccessGranted(appName: String, type: BotSendMessageAccessGrantedType?) case botSentSecureValues(types: [SentSecureValueType]) case peerJoined case phoneNumberRequest @@ -195,6 +201,8 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { } else { self = .unknown } + case 35: + self = .botAppAccessGranted(appName: decoder.decodeStringForKey("app", orElse: ""), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) }) default: self = .unknown } @@ -362,6 +370,14 @@ public enum TelegramMediaActionType: PostboxCoding, Equatable { case let .setSameChatWallpaper(wallpaper): encoder.encodeInt32(34, forKey: "_rawValue") encoder.encode(TelegramWallpaperNativeCodable(wallpaper), forKey: "wallpaper") + case let .botAppAccessGranted(appName, type): + encoder.encodeInt32(35, forKey: "_rawValue") + encoder.encodeString(appName, forKey: "app") + if let type = type { + encoder.encodeInt32(type.rawValue, forKey: "atp") + } else { + encoder.encodeNil(forKey: "atp") + } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index 48103237cc..c369a6784c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -236,3 +236,71 @@ func _internal_requestAppWebView(postbox: Postbox, network: Network, stateManage |> castError(RequestAppWebViewError.self) |> switchToLatest } + +func _internal_canBotSendMessages(postbox: Postbox, network: Network, botId: PeerId) -> Signal { + return postbox.transaction { transaction -> Signal in + guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else { + return .single(false) + } + + return network.request(Api.functions.bots.canSendMessage(bot: inputUser)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> map { result -> Bool in + if case .boolTrue = result { + return true + } else { + return false + } + } + } + |> switchToLatest +} + +func _internal_allowBotSendMessages(postbox: Postbox, network: Network, botId: PeerId) -> Signal { + return postbox.transaction { transaction -> Signal in + guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else { + return .single(false) + } + + return network.request(Api.functions.bots.allowSendMessage(bot: inputUser)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> map { result -> Bool in + if case .boolTrue = result { + return true + } else { + return false + } + } + } + |> switchToLatest +} + +public enum InvokeBotCustomMethodError { + case generic +} + +func _internal_invokeBotCustomMethod(postbox: Postbox, network: Network, botId: PeerId, method: String, params: String) -> Signal { + let params = Api.DataJSON.dataJSON(data: params) + return postbox.transaction { transaction -> Signal in + guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else { + return .fail(.generic) + } + return network.request(Api.functions.bots.invokeWebViewCustomMethod(bot: inputUser, customMethod: method, params: params)) + |> mapError { _ -> InvokeBotCustomMethodError in + return .generic + } + |> map { result -> String in + if case let .dataJSON(data) = result { + return data + } else { + return "" + } + } + } + |> castError(InvokeBotCustomMethodError.self) + |> switchToLatest +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 640594f509..272dc1c39d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -507,6 +507,18 @@ public extension TelegramEngine { public func sendWebViewData(botId: PeerId, buttonText: String, data: String) -> Signal { return _internal_sendWebViewData(postbox: self.account.postbox, network: self.account.network, stateManager: self.account.stateManager, botId: botId, buttonText: buttonText, data: data) } + + public func canBotSendMessages(botId: PeerId) -> Signal { + return _internal_canBotSendMessages(postbox: self.account.postbox, network: self.account.network, botId: botId) + } + + public func allowBotSendMessages(botId: PeerId) -> Signal { + return _internal_allowBotSendMessages(postbox: self.account.postbox, network: self.account.network, botId: botId) + } + + public func invokeBotCustomMethod(botId: PeerId, method: String, params: String) -> Signal { + return _internal_invokeBotCustomMethod(postbox: self.account.postbox, network: self.account.network, botId: botId, method: method, params: params) + } public func addBotToAttachMenu(botId: PeerId, allowWrite: Bool) -> Signal { return _internal_addBotToAttachMenu(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, botId: botId, allowWrite: allowWrite) diff --git a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift index 45e7bbe12f..1159187c22 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -629,6 +629,8 @@ public func universalServiceMessageString(presentationData: (PresentationTheme, attributedString = stringWithAppliedEntities(text, entities: entities, baseColor: primaryTextColor, linkColor: primaryTextColor, baseFont: titleFont, linkFont: titleBoldFont, boldFont: titleBoldFont, italicFont: titleFont, boldItalicFont: titleBoldFont, fixedFont: titleFont, blockQuoteFont: titleFont, underlineLinks: false, message: message._asMessage()) case let .botDomainAccessGranted(domain): attributedString = NSAttributedString(string: strings.AuthSessions_Message(domain).string, font: titleFont, textColor: primaryTextColor) + case let .botAppAccessGranted(appName, _): + attributedString = NSAttributedString(string: strings.AuthSessions_MessageApp(appName).string, font: titleFont, textColor: primaryTextColor) case let .botSentSecureValues(types): var typesString = "" var hasIdentity = false diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index dce54e4139..c51b75eb09 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -917,6 +917,20 @@ public final class WebAppController: ViewController, AttachmentContainable { } self.sendClipboardTextEvent(requestId: requestId, fillData: fillData) } + case "web_app_request_write_access": + self.requestWriteAccess() + case "web_app_request_phone": + self.shareAccountContact() + case "web_app_invoke_custom_method": + if let json, let requestId = json["req_id"] as? String, let method = json["method"] as? String, let params = json["params"] { + var paramsString: String? + if let string = params as? String { + paramsString = string + } else if let data1 = try? JSONSerialization.data(withJSONObject: params, options: []), let convertedString = String(data: data1, encoding: String.Encoding.utf8) { + paramsString = convertedString + } + self.invokeCustomMethod(requestId: requestId, method: method, params: paramsString ?? "{}") + } default: break } @@ -959,7 +973,7 @@ public final class WebAppController: ViewController, AttachmentContainable { var resultString: String? if let string = data as? String { resultString = string - } else if let data1 = try? JSONSerialization.data(withJSONObject: data, options: JSONSerialization.WritingOptions.prettyPrinted), let convertedString = String(data: data1, encoding: String.Encoding.utf8) { + } else if let data1 = try? JSONSerialization.data(withJSONObject: data, options: []), let convertedString = String(data: data1, encoding: String.Encoding.utf8) { resultString = convertedString } if let resultString = resultString { @@ -1066,6 +1080,92 @@ public final class WebAppController: ViewController, AttachmentContainable { } self.webView?.sendEvent(name: "clipboard_text_received", data: paramsString) } + + fileprivate func requestWriteAccess() { + guard let controller = self.controller, !self.dismissed else { + return + } + + let sendEvent: (Bool) -> Void = { success in + var paramsString: String + if success { + paramsString = "{status: \"allowed\"}" + } else { + paramsString = "{status: \"cancelled\"}" + } + self.webView?.sendEvent(name: "write_access_requested", data: paramsString) + } + + let _ = (self.context.engine.messages.canBotSendMessages(botId: controller.botId) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self, let controller = self.controller else { + return + } + if result { + sendEvent(true) + } else { + controller.present(textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: "Allow Sending Messages", text: "Allow \(controller.botName) to send messages?", actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { + sendEvent(false) + }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in + guard let self else { + return + } + + let _ = (self.context.engine.messages.allowBotSendMessages(botId: controller.botId) + |> deliverOnMainQueue).start(next: { result in + sendEvent(result) + }) + })]), in: .window(.root)) + } + }) + } + + fileprivate func shareAccountContact() { + guard let controller = self.controller else { + return + } + + let sendEvent: (Bool) -> Void = { success in + var paramsString: String + if success { + paramsString = "{status: \"sent\"}" + } else { + paramsString = "{status: \"cancelled\"}" + } + self.webView?.sendEvent(name: "phone_requested", data: paramsString) + } + + controller.present(textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.Conversation_ShareBotContactConfirmationTitle, text: self.presentationData.strings.Conversation_ShareBotContactConfirmation, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { + sendEvent(false) + }), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in + guard let self else { + return + } + let _ = (self.context.account.postbox.loadedPeerWithId(self.context.account.peerId) + |> deliverOnMainQueue).start(next: { [weak self] peer in + if let self, let botId = self.controller?.botId, let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { + let _ = enqueueMessages(account: self.context.account, peerId: botId, messages: [ + .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: phone, peerId: peer.id, vCardData: nil)), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + ]).start() + sendEvent(true) + } + }) + })]), in: .window(.root)) + } + + fileprivate func invokeCustomMethod(requestId: String, method: String, params: String) { + guard let controller = self.controller, !self.dismissed else { + return + } + let _ = (self.context.engine.messages.invokeBotCustomMethod(botId: controller.botId, method: method, params: params) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + let paramsString = "{req_id: \"\(requestId)\", result: \(result)}" + self.webView?.sendEvent(name: "custom_method_invoked", data: paramsString) + }) + } } fileprivate var controllerNode: Node {