diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 8cecadca30..254539b666 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9854,3 +9854,14 @@ Sorry for the inconvenience."; "Location.TypeCity" = "City"; "Location.TypeStreet" = "Street"; "Location.TypeLocation" = "Location"; + +"WebApp.AllowWriteTitle" = "Allow Sending Messages?"; +"WebApp.AllowWriteConfirmation" = "This will allow the bot **%@** to message you on Telegram."; + +"AuthSessions.MessageApp" = "You allowed this bot to message you when you opened %@."; +"Notification.BotWriteAllowedMenu" = "You allowed this bot to message you when you added it to your attachment menu."; +"Notification.BotWriteAllowedRequest" = "You allowed this bot to message you in the app."; + +"WebApp.SharePhoneTitle" = "Share Phone Number?"; +"WebApp.SharePhoneConfirmation" = "**%@** will know your phone number. This can be useful for integration with other services."; +"WebApp.SharePhoneConfirmationUnblock" = "**%@** will know your phone number. This can be useful for integration with other services.\n\nThis will also unblock the bot."; diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index 5a1b471fbe..ac7815157c 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(-248323089) + bot.serialize(buffer, true) + return (FunctionDescription(name: "bots.allowSendMessage", parameters: [("bot", String(describing: bot))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + 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..997719f0cd 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaAction.swift @@ -50,8 +50,23 @@ 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 { + var appName: String? + if case let .botApp(_, _, _, _, appNameValue, _, _, _, _) = app { + appName = appNameValue + } + var type: BotSendMessageAccessGrantedType? + if (flags & (1 << 1)) != 0 { + type = .attachMenu + } + if (flags & (1 << 3)) != 0 { + type = .request + } + return TelegramMediaAction(action: .botAppAccessGranted(appName: appName, type: type)) + } case .messageActionSecureValuesSentMe: return nil case let .messageActionSecureValuesSent(types): diff --git a/submodules/TelegramCore/Sources/State/Serialization.swift b/submodules/TelegramCore/Sources/State/Serialization.swift index f4b2044975..b4542e8af7 100644 --- a/submodules/TelegramCore/Sources/State/Serialization.swift +++ b/submodules/TelegramCore/Sources/State/Serialization.swift @@ -210,7 +210,7 @@ public class BoxedMessage: NSObject { public class Serialization: NSObject, MTSerialization { public func currentLayer() -> UInt { - return 161 + return 162 } public func parseMessage(_ data: Data!) -> Any! { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaAction.swift index 252e52681e..9e1cf940a1 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.decodeOptionalStringForKey("app"), type: decoder.decodeOptionalInt32ForKey("atp").flatMap { BotSendMessageAccessGrantedType(rawValue: $0) }) default: self = .unknown } @@ -362,6 +370,18 @@ 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") + if let appName = appName { + encoder.encodeString(appName, forKey: "app") + } else { + encoder.encodeNil(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..13f9228fbe 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -236,3 +236,72 @@ 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, stateManager: AccountStateManager, botId: PeerId) -> Signal { + return postbox.transaction { transaction -> Signal in + guard let bot = transaction.getPeer(botId), let inputUser = apiInputUser(bot) else { + return .never() + } + + return network.request(Api.functions.bots.allowSendMessage(bot: inputUser)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { updates -> Api.Updates? in + if let updates = updates { + stateManager.addUpdates(updates) + } + return updates + } + |> ignoreValues + } + |> 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 637b49fa0e..87a97317f5 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, stateManager: self.account.stateManager, sbotId: 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..0998610d15 100644 --- a/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift +++ b/submodules/TelegramStringFormatting/Sources/ServiceMessageStrings.swift @@ -629,6 +629,16 @@ 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, type): + let text: String + if type == .attachMenu { + text = strings.Notification_BotWriteAllowedMenu + } else if type == .request { + text = strings.Notification_BotWriteAllowedRequest + } else { + text = strings.AuthSessions_MessageApp(appName ?? "").string + } + attributedString = NSAttributedString(string: text, font: titleFont, textColor: primaryTextColor) case let .botSentSecureValues(types): var typesString = "" var hasIdentity = false diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 9ec5046275..351c71e2b8 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -13188,7 +13188,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let buttons: Signal<([AttachmentButtonType], [AttachmentButtonType], AttachmentButtonType?), NoError> - if !isScheduledMessages { + if !isScheduledMessages && !peer.isDeleted { buttons = self.context.engine.messages.attachMenuBots() |> map { attachMenuBots in var buttons = availableButtons diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 8c866d11e1..125977e450 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,153 @@ 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 { + let alertController = textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.WebApp_AllowWriteTitle, text: self.presentationData.strings.WebApp_AllowWriteConfirmation(controller.botName).string, 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) + }) + })], parseMarkdown: true) + alertController.dismissed = { byOutsideTap in + if byOutsideTap { + sendEvent(false) + } + } + controller.present(alertController, in: .window(.root)) + } + }) + } + + fileprivate func shareAccountContact() { + guard let controller = self.controller, let botId = self.controller?.botId, let botName = self.controller?.botName 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) + } + + let context = self.context + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId), + TelegramEngine.EngineData.Item.Peer.IsBlocked(id: botId) + ) + |> deliverOnMainQueue).start(next: { [weak self, weak controller] accountPeer, isBlocked in + guard let self, let controller, let accountPeer else { + return + } + var requiresUnblock = false + if case let .known(value) = isBlocked, value { + requiresUnblock = true + } + + let text: String + if requiresUnblock { + text = self.presentationData.strings.WebApp_SharePhoneConfirmationUnblock(botName).string + } else { + text = self.presentationData.strings.WebApp_SharePhoneConfirmation(botName).string + } + + let alertController = textAlertController(context: self.context, updatedPresentationData: controller.updatedPresentationData, title: self.presentationData.strings.WebApp_SharePhoneTitle, text: text, 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, case let .user(user) = accountPeer, let phone = user.phone, !phone.isEmpty else { + return + } + + let sendMessageSignal = enqueueMessages(account: self.context.account, peerId: botId, messages: [ + .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaContact(firstName: user.firstName ?? "", lastName: user.lastName ?? "", phoneNumber: phone, peerId: user.id, vCardData: nil)), replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: []) + ]) + |> mapToSignal { messageIds in + if let maybeMessageId = messageIds.first, let messageId = maybeMessageId { + return context.account.pendingMessageManager.pendingMessageStatus(messageId) + |> mapToSignal { status, _ -> Signal in + if status != nil { + return .never() + } else { + return .single(true) + } + } + |> take(1) + } else { + return .complete() + } + } + + let sendMessage = { + let _ = (sendMessageSignal + |> deliverOnMainQueue).start(completed: { + sendEvent(true) + }) + } + + if requiresUnblock { + let _ = (context.engine.privacy.requestUpdatePeerIsBlocked(peerId: botId, isBlocked: false) + |> deliverOnMainQueue).start(completed: { + sendMessage() + }) + } else { + sendMessage() + } + })], parseMarkdown: true) + alertController.dismissed = { byOutsideTap in + if byOutsideTap { + sendEvent(false) + } + } + controller.present(alertController, 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 { diff --git a/versions.json b/versions.json index 2c71d357af..7dfc3cd2e1 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "10.0.1", + "app": "10.0.2", "bazel": "6.1.1", "xcode": "14.2" }