├── Sources ├── CCurl │ ├── shim.c │ └── include │ │ ├── module.modulemap │ │ └── shim.h └── TelegramBotSDK │ ├── Types │ ├── PollType.swift │ ├── ChatType.swift │ ├── ParseMode.swift │ ├── ChatMember+Status.swift │ ├── Message+Command.swift │ ├── InputFile.swift │ ├── Response.swift │ ├── MessageEntity+Utils.swift │ ├── ChatId.swift │ ├── MessageOrBool.swift │ ├── InputFileOrString.swift │ ├── InputMessageContent.swift │ ├── InputMedia.swift │ ├── ReplyMarkup.swift │ └── InlineQueryResult.swift │ ├── WriteCallbackData.swift │ ├── Extensions │ ├── String+Trim.swift │ ├── NSRunLoop+Utils.swift │ ├── String+Utils.swift │ ├── Optional+Unwrap.swift │ ├── String+ExtractBotCommand.swift │ ├── Scanner+Compatibility.swift │ ├── String+HTTP.swift │ └── Scanner+Utils.swift │ ├── TaskAssociatedData.swift │ ├── DictionaryUtils.swift │ ├── BotName.swift │ ├── TelegramBot+Utils.swift │ ├── Router │ ├── ContentType.swift │ ├── Arguments.swift │ ├── Router+Helpers.swift │ ├── Command.swift │ ├── Context.swift │ └── Router.swift │ ├── Utils.swift │ ├── DataTaskError.swift │ ├── Methods │ ├── TelegramBot+getUpdates+Utils.swift │ └── TelegramBot+sendChatAction+Utils.swift │ ├── MimeTypes.swift │ ├── TelegramBot+Requests.swift │ └── HTTPUtils.swift ├── API ├── .vimrc ├── README └── Makefile ├── Rapier ├── README.md ├── Tests │ ├── LinuxMain.swift │ └── rapierTests │ │ ├── XCTestManifests.swift │ │ └── rapierTests.swift ├── Sources │ ├── Rapier │ │ ├── TypeInfo.swift │ │ ├── MethodInfo.swift │ │ ├── CodeGenerator.swift │ │ ├── FieldInfo.swift │ │ ├── RapierError.swift │ │ └── Rapier.swift │ └── RapierCLI │ │ ├── main.swift │ │ └── Generators │ │ ├── OverviewGenerator.swift │ │ └── TelegramBotSDKCodableGenerator.swift ├── Package.resolved └── Package.swift ├── Examples ├── shopster-bot │ ├── .gitignore │ ├── Makefile │ ├── Sources │ │ └── shopster-bot │ │ │ ├── Categories │ │ │ └── Context+Session.swift │ │ │ ├── Data │ │ │ └── Commands.swift │ │ │ ├── Models │ │ │ └── DB.swift │ │ │ ├── Records │ │ │ ├── Session.swift │ │ │ └── Item.swift │ │ │ ├── Controllers │ │ │ ├── MigrationController.swift │ │ │ ├── AddController.swift │ │ │ ├── DeleteController.swift │ │ │ └── MainController.swift │ │ │ └── main.swift │ └── Package.swift ├── hello-bot │ ├── Makefile │ ├── Package.swift │ ├── Sources │ │ └── hello-bot │ │ │ └── main.swift │ └── .gitignore └── word-reverse-bot │ ├── Makefile │ ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── contents.xcworkspacedata │ ├── Package.swift │ ├── .gitignore │ └── Sources │ └── word-reverse-bot │ └── main.swift ├── AUTHORS.txt ├── Makefile ├── CONTRIBUTING.txt ├── Tests ├── LinuxMain.swift └── TelegramBotSDKTests │ ├── BlockingServerTests.swift │ ├── TelegramBotTests.swift │ ├── RequestTests.swift │ ├── UrlencodeTests.swift │ └── RouterTests.swift ├── .travis.yml ├── .github └── workflows │ └── swift.yml ├── .gitignore ├── LICENSE.SwiftyJSON.txt ├── Package.swift └── CHANGELOG.md /Sources/CCurl/shim.c: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /API/.vimrc: -------------------------------------------------------------------------------- 1 | set autoread 2 | 3 | -------------------------------------------------------------------------------- /Rapier/README.md: -------------------------------------------------------------------------------- 1 | # Currently on active development 2 | -------------------------------------------------------------------------------- /Sources/CCurl/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module CCurl [system] { 2 | header "shim.h" 3 | link "curl" 4 | export * 5 | } 6 | -------------------------------------------------------------------------------- /Examples/shopster-bot/.gitignore: -------------------------------------------------------------------------------- 1 | SHOPSTER_BOT_TOKEN 2 | db.sqlite 3 | .DS_Store 4 | /.build 5 | /Packages 6 | /*.xcodeproj 7 | Package.resolved 8 | -------------------------------------------------------------------------------- /Rapier/Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import rapierTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += rapierTests.allTests() 7 | XCTMain(tests) -------------------------------------------------------------------------------- /Examples/hello-bot/Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | test: 4 | swift test 5 | 6 | build: 7 | swift build 8 | 9 | clean: 10 | swift package clean 11 | 12 | .PHONY: all test build clean 13 | -------------------------------------------------------------------------------- /Examples/shopster-bot/Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | test: 4 | swift test 5 | 6 | build: 7 | swift build 8 | 9 | clean: 10 | swift package clean 11 | 12 | .PHONY: all test build clean 13 | -------------------------------------------------------------------------------- /Examples/word-reverse-bot/Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | test: 4 | swift test 5 | 6 | build: 7 | swift build 8 | 9 | clean: 10 | swift package clean 11 | 12 | .PHONY: all test build clean 13 | -------------------------------------------------------------------------------- /Rapier/Tests/rapierTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !os(macOS) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(rapierTests.allTests), 7 | ] 8 | } 9 | #endif -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | --------------- 2 | Project authors 3 | --------------- 4 | 5 | - Andrey Fidrya 6 | - Andrea de Marco <24erre@gmail.com> 7 | - Artur Lich 8 | - Matteo Piccina 9 | -------------------------------------------------------------------------------- /Examples/word-reverse-bot/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | test: 4 | swift test 5 | 6 | build: 7 | swift build 8 | 9 | clean: 10 | swift package clean 11 | 12 | xcodeproj: 13 | swift package generate-xcodeproj 14 | 15 | .PHONY: all test build clean xcodeproj 16 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Types/PollType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Matteo Piccina on 30/01/2020. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum PollType: String, Codable { 11 | case regular 12 | case quiz 13 | } 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.txt: -------------------------------------------------------------------------------- 1 | By submitting a pull request, you represent that you have the right to license your contribution to the project authors listed in AUTHORS.txt and the community, and agree by submitting the patch that your contributions are licensed under the project's license: LICENSE.txt 2 | -------------------------------------------------------------------------------- /API/README: -------------------------------------------------------------------------------- 1 | Install Xcode Commandline Tools (for compiling nokogiri): 2 | 3 | xcode-select --install 4 | 5 | Install dependencies: 6 | 7 | gem install bundler 8 | bundle install 9 | 10 | Regenerate Telegram Bot classes from documentation: 11 | 12 | ./download_api_docs.sh 13 | ruby generate_wrappers.rb 14 | 15 | -------------------------------------------------------------------------------- /Rapier/Sources/Rapier/TypeInfo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct TypeInfo { 4 | public init(name: String, fields: [FieldInfo] = []) { 5 | self.name = name 6 | self.fields = fields 7 | } 8 | 9 | public var name: String 10 | public var fields: [FieldInfo] 11 | } 12 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import TelegramBotTests 3 | 4 | XCTMain([ 5 | testCase(BlockingServerTests.allTests), 6 | testCase(RequestTests.allTests), 7 | testCase(RouterTests.allTests), 8 | testCase(TelegramBotTests.allTests), 9 | testCase(UrlencodeTests.allTests), 10 | ]) 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | language: generic 5 | sudo: required 6 | dist: xenial 7 | osx_image: xcode10.2 8 | addons: 9 | apt: 10 | packages: 11 | - libcurl4-openssl-dev 12 | env: 13 | - SWIFT_VERSION=5.0.1 14 | install: 15 | - eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" 16 | script: 17 | - swift build 18 | -------------------------------------------------------------------------------- /Rapier/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Yaml", 6 | "repositoryURL": "https://github.com/behrang/YamlSwift.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "287f5cab7da0d92eb947b5fd8151b203ae04a9a3", 10 | "version": "3.4.4" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Rapier/Sources/Rapier/MethodInfo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct MethodInfo { 4 | public init(name: String, parameters: [FieldInfo] = [], result: FieldInfo) { 5 | self.name = name 6 | self.parameters = parameters 7 | self.result = result 8 | } 9 | public var name: String 10 | public var parameters: [FieldInfo] 11 | public var result: FieldInfo 12 | } 13 | -------------------------------------------------------------------------------- /API/Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | build: 4 | mkdir -p out 5 | swift run --package-path ../Rapier rapier TelegramAPIDefinition.yml out 6 | 7 | install: 8 | cp ./out/Methods.swift ../Sources/TelegramBotSDK/Generated 9 | cp ./out/Types.swift ../Sources/TelegramBotSDK/Generated 10 | 11 | rebuild: clean build 12 | 13 | clean: 14 | rm -f out/Methods.swift 15 | rm -rf out/Types.swift 16 | 17 | .PHONY: all build install rebuild clean 18 | -------------------------------------------------------------------------------- /Examples/shopster-bot/Sources/shopster-bot/Categories/Context+Session.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Telegram Bot SDK for Swift (unofficial). 3 | // 4 | // This file containing the example code is in public domain. 5 | // Feel free to copy-paste it and edit it in any way you like. 6 | // 7 | 8 | import Foundation 9 | import TelegramBotSDK 10 | 11 | extension Context { 12 | var session: Session { return properties["session"] as! Session } 13 | } 14 | -------------------------------------------------------------------------------- /Rapier/Sources/Rapier/CodeGenerator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol CodeGenerator { 4 | init(directory: String) 5 | 6 | func start() throws 7 | 8 | func beforeGeneratingTypes() throws 9 | func generateType(name: String, info: TypeInfo) throws 10 | func afterGeneratingTypes() throws 11 | 12 | func beforeGeneratingMethods() throws 13 | func generateMethod(name: String, info: MethodInfo) throws 14 | func afterGeneratingMethods() throws 15 | 16 | func finish() throws 17 | } 18 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/WriteCallbackData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WriteCallbackData.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | class WriteCallbackData { 16 | var data = Data() 17 | } 18 | -------------------------------------------------------------------------------- /Rapier/Sources/Rapier/FieldInfo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct FieldInfo { 4 | public init(name: String = "", type: String = "", isArray: Bool = false, isArrayOfArray: Bool = false, isOptional: Bool = false) { 5 | self.name = name 6 | self.type = type 7 | self.isArray = isArray 8 | self.isArrayOfArray = isArrayOfArray 9 | self.isOptional = isOptional 10 | } 11 | 12 | public var name: String 13 | public var type: String 14 | public var isArray: Bool 15 | public var isArrayOfArray: Bool 16 | public var isOptional: Bool 17 | } 18 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Types/ChatType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatType.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | public enum ChatType: String, Codable { 16 | case privateChat = "private" 17 | case group 18 | case supergroup 19 | case channel 20 | } 21 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Types/ParseMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TelegramBot+Enums.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | import Foundation 13 | 14 | public enum ParseMode: String, Codable { 15 | case html = "HTML" 16 | case markdown = "MarkDown" 17 | case markdownv2 = "MarkdownV2" 18 | } 19 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Extensions/String+Trim.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Trim.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | extension String { 16 | public func trimmed(set: CharacterSet = CharacterSet.whitespacesAndNewlines) -> String { 17 | return trimmingCharacters(in: set) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Types/ChatMember+Status.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatMember+Utils.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | extension ChatMember { 16 | public enum Status: String, Codable { 17 | case creator 18 | case administrator 19 | case member 20 | case restricted 21 | case left 22 | case kicked 23 | } 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /Rapier/Sources/RapierCLI/main.swift: -------------------------------------------------------------------------------- 1 | import Darwin 2 | import Foundation 3 | import Yaml 4 | import Rapier 5 | 6 | private func main() throws -> Int32 { 7 | guard CommandLine.arguments.count == 3 else { 8 | print("Usage: rapier ") 9 | return 1 10 | } 11 | let rapier = Rapier(ymlFile: CommandLine.arguments[1]) 12 | 13 | try rapier.parseYml() 14 | 15 | let generator = TelegramBotSDKCodableGenerator(directory: CommandLine.arguments[2]) 16 | try rapier.generate(generator: generator) 17 | 18 | return 0 19 | } 20 | 21 | do { 22 | exit(try main()) 23 | } catch { 24 | print(error.localizedDescription) 25 | exit(1) 26 | } 27 | -------------------------------------------------------------------------------- /Examples/shopster-bot/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "shopster-bot", 7 | products: [ 8 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 9 | .executable( 10 | name: "shopster-bot", 11 | targets: ["shopster-bot"]), 12 | ], 13 | dependencies: [ 14 | .package(url: "../..", from: "1.2.3"), 15 | .package(url: "https://github.com/groue/GRDB.swift.git", from: "4.5.0") 16 | ], 17 | targets: [ 18 | .target( 19 | name: "shopster-bot", 20 | dependencies: ["TelegramBotSDK", "GRDB"]), 21 | ] 22 | ) 23 | 24 | -------------------------------------------------------------------------------- /Examples/shopster-bot/Sources/shopster-bot/Data/Commands.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Telegram Bot SDK for Swift (unofficial). 3 | // 4 | // This file containing the example code is in public domain. 5 | // Feel free to copy-paste it and edit it in any way you like. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Commands { 11 | static let start = "start" 12 | static let stop = "stop" 13 | static let help = ["ℹ️ Help", "help"] 14 | static let add = ["➕ Add", "add"] 15 | static let delete = ["⛔️ Delete", "delete"] 16 | static let list = ["🎁 List", "list"] 17 | static let support = ["✉️ Support", "support"] 18 | static let cancel = ["↩️ Cancel", "cancel"] 19 | static let confirmDeletion = ["⛔️ Confirm Deletion", "confirm_deletion"] 20 | } 21 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Extensions/NSRunLoop+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSRunLoop+Utils.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | import Dispatch 15 | 16 | public extension RunLoop { 17 | func waitForSemaphore(_ sem: DispatchSemaphore) { 18 | repeat { 19 | run(mode: RunLoop.Mode.default, before: Date(timeIntervalSinceNow: 0.01)) 20 | } while .success != sem.wait(timeout: DispatchTime.now()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Extensions/String+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Utils.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | extension String { 16 | public func hasPrefix(_ prefix: String, caseInsensitive: Bool) -> Bool { 17 | if caseInsensitive { 18 | return nil != self.range(of: prefix, options: [.caseInsensitive, .anchored]) 19 | } 20 | return hasPrefix(prefix) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Types/Message+Command.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Message+Command.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | extension Message { 16 | public func extractCommand(for bot: TelegramBot) -> String? { 17 | return text?.without(botName: bot.name) ?? nil 18 | } 19 | 20 | public func addressed(to bot: TelegramBot) -> Bool { 21 | guard let text = text else { return true } 22 | return text.without(botName: bot.name) != nil 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Examples/shopster-bot/Sources/shopster-bot/Models/DB.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Telegram Bot SDK for Swift (unofficial). 3 | // 4 | // This file containing the example code is in public domain. 5 | // Feel free to copy-paste it and edit it in any way you like. 6 | // 7 | 8 | import Foundation 9 | import TelegramBotSDK 10 | import GRDB 11 | 12 | class DB { 13 | static let queue: DatabaseQueue = { 14 | var config = Configuration() 15 | config.busyMode = .timeout(10) // Wait 10 seconds before throwing SQLITE_BUSY error 16 | config.defaultTransactionKind = .deferred 17 | config.trace = { print($0) } // Prints all SQL statements 18 | 19 | do { 20 | return try DatabaseQueue(path: "db.sqlite", configuration: config) 21 | } catch { 22 | fatalError("Unable to open database: \(error)") 23 | } 24 | }() 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Build Project 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build-macos: 11 | runs-on: macos-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup Swift 15 | uses: fwal/setup-swift@d43a564349d1341cd990cfbd70d94d63b8899475 16 | with: 17 | swift-version: "5.4" 18 | - name: Build 19 | run: swift build 20 | build-linux: 21 | runs-on: [ubuntu-18.04] 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Install dependencies 25 | run: sudo apt-get update && sudo apt-get install libcurl4-openssl-dev 26 | - name: Setup Swift 27 | uses: fwal/setup-swift@d43a564349d1341cd990cfbd70d94d63b8899475 28 | with: 29 | swift-version: "5.4" 30 | - name: Build 31 | run: swift build 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | Packages/ 3 | *_TOKEN 4 | *.xcodeproj 5 | test_bot_token.txt 6 | API/api.html 7 | API/api.txt 8 | API/out 9 | 10 | # Xcode 11 | # 12 | build/ 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata 22 | *.xccheckout 23 | *.moved-aside 24 | DerivedData 25 | *.hmap 26 | *.ipa 27 | *.xcuserstate 28 | TelegramBot.swift 29 | 30 | # CocoaPods 31 | # 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 35 | # 36 | # Pods/ 37 | 38 | # Carthage 39 | # 40 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 41 | # Carthage/Checkouts 42 | 43 | Carthage/Build 44 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/TaskAssociatedData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskAssociatedData.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | public class TaskAssociatedData { 16 | /// If no networking errors occur and the data returned by the server 17 | /// is parsed successfully, this handler will be called 18 | internal var completion: TelegramBot.DataTaskCompletion? 19 | 20 | /// Current number of reconnect attempts 21 | public var retryCount: Int = 0 22 | 23 | init(_ completion: @escaping TelegramBot.DataTaskCompletion = { _, _ in }) { 24 | self.completion = completion 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/DictionaryUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictionaryUtils.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | /* 16 | func + (left: Dictionary, right: Dictionary?) -> Dictionary { 17 | guard let right = right else { return left } 18 | return left.reduce(right) { 19 | var new = $0 as [K:V] 20 | new.updateValue($1.1, forKey: $1.0) 21 | return new 22 | } 23 | } 24 | */ 25 | 26 | /* 27 | func += (left: inout Dictionary, right: Dictionary?) { 28 | guard let right = right else { return } 29 | right.forEach { key, value in 30 | left.updateValue(value, forKey: key) 31 | } 32 | } 33 | */ 34 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Types/InputFile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputFile.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | /// Represents the contents of a file to be uploaded. Must be posted using multipart/form-data in the usual way that files are uploaded via the browser.. 16 | /// - SeeAlso: 17 | 18 | public class InputFile: Codable { 19 | var filename: String 20 | var data: Data 21 | var mimeType: String? 22 | 23 | public init(filename: String, data: Data, mimeType: String? = nil) { 24 | self.filename = filename 25 | self.data = data 26 | self.mimeType = mimeType 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Extensions/Optional+Unwrap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Optional+Unwrap.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | extension Optional { 16 | /// Removes `Optional()` when printing optionals. 17 | /// ```swift 18 | /// var x: String? = "text" 19 | /// var y: String? 20 | /// print("\(x), \(y)") 21 | /// print("\(x.unwrapOptional), \(y.unwrapOptional") 22 | /// ``` 23 | /// Results in: 24 | /// ``` 25 | /// Optional("text"), nil 26 | /// text, nil 27 | /// ``` 28 | public var unwrapOptional: String { 29 | if let v = self { 30 | return "\(v)" 31 | } 32 | return "nil" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Types/Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | 16 | /// Response to Bot API request. 17 | public struct Response: Decodable { 18 | 19 | /// If `ok` equals true, the request was successful and the result of the query can be found in the `result` field. In case of an unsuccessful request, ‘ok’ equals false and the error is explained in the ‘errorDescription’. 20 | public var ok: Bool 21 | /// *Optional.* Error description. 22 | public var description: String? 23 | /// *Optional.* Error code. Its contents are subject to change in the future. 24 | public var errorCode: Int? 25 | /// *Optional.* Result. 26 | internal var result: T? 27 | } 28 | -------------------------------------------------------------------------------- /Examples/hello-bot/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "hello-bot", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .executable( 11 | name: "hello-bot", 12 | targets: ["hello-bot"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | .package(url: "../..", from: "1.2.2"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 22 | .target( 23 | name: "hello-bot", 24 | dependencies: ["TelegramBotSDK"]), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Examples/word-reverse-bot/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "word-reverse-bot", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .executable( 11 | name: "word-reverse-bot", 12 | targets: ["word-reverse-bot"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | .package(url: "../..", from: "1.2.2"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 22 | .target( 23 | name: "word-reverse-bot", 24 | dependencies: ["TelegramBotSDK"]), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /LICENSE.SwiftyJSON.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ruoyu Fu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Types/MessageEntity+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageEntity+Utils.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | public enum MessageEntityType: String, Codable { 16 | case mention 17 | case hashtag 18 | case cashtag 19 | case botCommand = "bot_command" 20 | case url 21 | case email 22 | case phoneNumber = "phone_number" 23 | case bold 24 | case italic 25 | case underline 26 | case strikethrough 27 | case code 28 | case pre 29 | case textLink = "text_link" 30 | case textMention = "text_mention" 31 | case blockquote 32 | 33 | case unknown 34 | 35 | public init(from decoder: any Decoder) throws { 36 | let container = try decoder.singleValueContainer() 37 | let raw = try container.decode(String.self) 38 | self = MessageEntityType(rawValue: raw) ?? .unknown 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Rapier/Sources/Rapier/RapierError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum RapierError: Error { 4 | case expectedDictionary 5 | case unknownSectionType 6 | case expectedField(name: String, parent: String?) 7 | case missingReturn(parent: String) 8 | case fieldNameIsNotString(parent: String) 9 | case fieldTypeIsNotString(parent: String) 10 | } 11 | 12 | extension RapierError: LocalizedError { 13 | public var errorDescription: String? { 14 | switch self { 15 | case .expectedDictionary: return "Expected dictionary as top level element" 16 | case .unknownSectionType: return "Unknown section type" 17 | case let .expectedField(name, parent): 18 | if let parent = parent { 19 | return "'\(parent)': expected field named '\(name)'" 20 | } else { 21 | return "Expected field named '\(name)'" 22 | } 23 | case let .missingReturn(parent): 24 | return "'\(parent)': Missing result property" 25 | case let .fieldNameIsNotString(parent): 26 | return "'\(parent)': field name is not a string" 27 | case let .fieldTypeIsNotString(parent): 28 | return "'\(parent)': field type is not a string" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Rapier/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Rapier", 8 | products: [ 9 | .library( 10 | name: "Rapier", 11 | targets: ["Rapier"]), 12 | .executable( 13 | name: "rapier", 14 | targets: ["RapierCLI"]) 15 | 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | .package(url: "https://github.com/behrang/YamlSwift.git", from: "3.4.4") 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 25 | .target( 26 | name: "Rapier", 27 | dependencies: ["Yaml"]), 28 | .target( 29 | name: "RapierCLI", 30 | dependencies: ["Yaml", "Rapier"]), 31 | .testTarget( 32 | name: "rapierTests", 33 | dependencies: ["RapierCLI"]), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/BotName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BotName.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | public class BotName { 16 | let underscoreBotSuffix = "_bot" 17 | let botSuffix = "bot" 18 | public var withoutSuffix: String 19 | 20 | public init(username: String) { 21 | let lowercase = username.lowercased() 22 | if lowercase.hasSuffix(underscoreBotSuffix) { 23 | withoutSuffix = String(username.dropLast(underscoreBotSuffix.count)) 24 | } else if lowercase.hasSuffix(botSuffix) { 25 | withoutSuffix = String(username.dropLast(botSuffix.count)) 26 | } else { 27 | withoutSuffix = username 28 | } 29 | } 30 | 31 | 32 | } 33 | 34 | extension BotName: Equatable { 35 | } 36 | 37 | public func ==(lhs: BotName, rhs: BotName) -> Bool { 38 | return lhs.withoutSuffix == rhs.withoutSuffix 39 | } 40 | 41 | extension BotName: Comparable { 42 | } 43 | 44 | public func <(lhs: BotName, rhs: BotName) -> Bool { 45 | return lhs.withoutSuffix < rhs.withoutSuffix 46 | } 47 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "TelegramBotSDK", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "CCurl", 12 | targets: ["CCurl"]), 13 | .library( 14 | name: "TelegramBotSDK", 15 | targets: ["TelegramBotSDK"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 24 | .target( 25 | name: "CCurl", 26 | dependencies: []), 27 | .target( 28 | name: "TelegramBotSDK", 29 | dependencies: ["CCurl"]), 30 | .testTarget( 31 | name: "TelegramBotSDKTests", 32 | dependencies: ["TelegramBotSDK"]), 33 | ], 34 | swiftLanguageVersions: [.v4_2, .v5] 35 | ) 36 | -------------------------------------------------------------------------------- /Examples/shopster-bot/Sources/shopster-bot/Records/Session.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Telegram Bot SDK for Swift (unofficial). 3 | // 4 | // This file containing the example code is in public domain. 5 | // Feel free to copy-paste it and edit it in any way you like. 6 | // 7 | 8 | import Foundation 9 | import TelegramBotSDK 10 | import GRDB 11 | 12 | class Session: Record { 13 | var chatId: Int64 14 | var routerName: String 15 | 16 | override class var databaseTableName: String { 17 | return "sessions" 18 | } 19 | 20 | required init(row: Row) { 21 | chatId = row["chat_id"] 22 | routerName = row["router_name"] 23 | super.init(row: row) 24 | } 25 | 26 | init(chatId: Int64) { 27 | self.chatId = chatId 28 | self.routerName = "main" 29 | super.init() 30 | } 31 | 32 | static func session(for chatId: Int64) throws -> Session { 33 | let session: Session = try DB.queue.inDatabase { db in 34 | var session = try Session.fetchOne(db, key: chatId) 35 | if session == nil { 36 | session = Session(chatId: chatId) 37 | try session?.insert(db) 38 | } 39 | return session! 40 | } 41 | return session 42 | } 43 | 44 | func save() throws { 45 | try DB.queue.inDatabase { db in 46 | try self.save(db) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Types/ChatId.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChatId.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | public enum ChatId: Codable { 16 | case channel(String) 17 | case chat(Int64) 18 | case unknown 19 | 20 | public init(from decoder: Decoder) throws { 21 | if let string = try? decoder.singleValueContainer().decode(String.self) { 22 | self = .channel(string) 23 | return 24 | } 25 | 26 | if let int64 = try? decoder.singleValueContainer().decode(Int64.self) { 27 | self = .chat(int64) 28 | return 29 | } 30 | 31 | self = .unknown 32 | } 33 | 34 | public func encode(to encoder: Encoder) throws { 35 | var container = encoder.singleValueContainer() 36 | switch self { 37 | case let .channel(string): 38 | try container.encode(string) 39 | case let .chat(int64): 40 | try container.encode(int64) 41 | default: 42 | fatalError("Unknown should not be used") 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Types/MessageOrBool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageOrBool.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | 16 | public enum MessageOrBool: Codable { 17 | case message(Message) 18 | case bool(Bool) 19 | case unknown 20 | 21 | public init(from decoder: Decoder) throws { 22 | if let message = try? decoder.singleValueContainer().decode(Message.self) { 23 | self = .message(message) 24 | return 25 | } 26 | 27 | if let bool = try? decoder.singleValueContainer().decode(Bool.self) { 28 | self = .bool(bool) 29 | return 30 | } 31 | 32 | self = .unknown 33 | } 34 | 35 | public func encode(to encoder: Encoder) throws { 36 | var container = encoder.singleValueContainer() 37 | switch self { 38 | case let .message(message): 39 | try container.encode(message) 40 | case let .bool(bool): 41 | try container.encode(bool) 42 | default: 43 | fatalError("Unknown should not be used") 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Types/InputFileOrString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputFileOrString.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | public enum InputFileOrString: Codable { 16 | case inputFile(InputFile) 17 | case string(String) 18 | 19 | case unknown 20 | 21 | public init(from decoder: Decoder) throws { 22 | if let inputFile = try? decoder.singleValueContainer().decode(InputFile.self) { 23 | self = .inputFile(inputFile) 24 | return 25 | } 26 | 27 | if let string = try? decoder.singleValueContainer().decode(String.self) { 28 | self = .string(string) 29 | return 30 | } 31 | 32 | self = .unknown 33 | } 34 | 35 | public func encode(to encoder: Encoder) throws { 36 | var container = encoder.singleValueContainer() 37 | switch self { 38 | case let .inputFile(inputFile): 39 | try container.encode(inputFile) 40 | case let .string(string): 41 | try container.encode(string) 42 | default: 43 | fatalError("Unknown should not be used") 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Examples/hello-bot/Sources/hello-bot/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // 4 | // This file containing the example code is in public domain. 5 | // Feel free to copy-paste it and edit it in any way you like. 6 | // 7 | 8 | import Foundation 9 | import TelegramBotSDK 10 | 11 | let token = readToken(from: "HELLO_BOT_TOKEN") 12 | 13 | let bot = TelegramBot(token: token) 14 | 15 | let router = Router(bot: bot) 16 | 17 | router["help"] = { context in 18 | guard let from = context.message?.from else { return false } 19 | 20 | let helpText = "Usage: /greet" 21 | context.respondPrivatelyAsync(helpText, 22 | groupText: "\(from.firstName), please find usage instructions in a personal message.") 23 | return true 24 | } 25 | 26 | router["greet"] = { context in 27 | guard let from = context.message?.from else { return false } 28 | context.respondAsync("Hello, \(from.firstName)!") 29 | return true 30 | } 31 | 32 | router[.newChatMembers] = { context in 33 | guard let users = context.message?.newChatMembers else { return false } 34 | for user in users { 35 | guard user.id != bot.user.id else { return false } 36 | context.respondAsync("Welcome, \(user.firstName)!") 37 | } 38 | return true 39 | } 40 | 41 | print("Ready to accept commands") 42 | while let update = bot.nextUpdateSync() { 43 | print("--- update: \(update.debugDescription)") 44 | 45 | try router.process(update: update) 46 | } 47 | 48 | fatalError("Server stopped due to error: \(bot.lastError.unwrapOptional)") 49 | -------------------------------------------------------------------------------- /Sources/CCurl/include/shim.h: -------------------------------------------------------------------------------- 1 | #ifndef CCURL_SHIM_H 2 | #define CCURL_SHIM_H 3 | 4 | #import 5 | #include 6 | 7 | typedef size_t (*write_callback_t)(char *ptr, size_t size, size_t nmemb, void *userdata); 8 | 9 | 10 | static inline CURLcode curl_easy_setopt_string(CURL *handle, CURLoption option, const char *parameter) 11 | { 12 | return curl_easy_setopt(handle, option, parameter); 13 | } 14 | 15 | static inline CURLcode curl_easy_setopt_int(CURL *handle, CURLoption option, int parameter) 16 | { 17 | return curl_easy_setopt(handle, option, parameter); 18 | } 19 | 20 | static inline CURLcode curl_easy_setopt_binary(CURL *handle, CURLoption option, const uint8_t *parameter) 21 | { 22 | return curl_easy_setopt(handle, option, parameter); 23 | } 24 | 25 | static inline CURLcode curl_easy_setopt_write_function(CURL *handle, CURLoption option, write_callback_t parameter) 26 | { 27 | return curl_easy_setopt(handle, option, parameter); 28 | } 29 | 30 | static inline CURLcode curl_easy_setopt_pointer(CURL *handle, CURLoption option, void *parameter) 31 | { 32 | return curl_easy_setopt(handle, option, parameter); 33 | } 34 | 35 | static inline CURLcode curl_easy_setopt_slist(CURL *handle, CURLoption option, struct curl_slist *parameter) 36 | { 37 | return curl_easy_setopt(handle, option, parameter); 38 | } 39 | 40 | static inline CURLcode curl_easy_getinfo_long(CURL *curl, CURLINFO info, long *result) 41 | { 42 | return curl_easy_getinfo(curl, info, result); 43 | } 44 | 45 | #endif 46 | 47 | -------------------------------------------------------------------------------- /Rapier/Tests/rapierTests/rapierTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import class Foundation.Bundle 3 | 4 | final class rapierTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | 10 | // Some of the APIs that we use below are available in macOS 10.13 and above. 11 | guard #available(macOS 10.13, *) else { 12 | return 13 | } 14 | 15 | let fooBinary = productsDirectory.appendingPathComponent("rapier") 16 | 17 | let process = Process() 18 | process.executableURL = fooBinary 19 | 20 | let pipe = Pipe() 21 | process.standardOutput = pipe 22 | 23 | try process.run() 24 | process.waitUntilExit() 25 | 26 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 27 | let output = String(data: data, encoding: .utf8) 28 | 29 | XCTAssertEqual(output, "Hello, world!\n") 30 | } 31 | 32 | /// Returns path to the built products directory. 33 | var productsDirectory: URL { 34 | #if os(macOS) 35 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { 36 | return bundle.bundleURL.deletingLastPathComponent() 37 | } 38 | fatalError("couldn't find the products directory") 39 | #else 40 | return Bundle.main.bundleURL 41 | #endif 42 | } 43 | 44 | static var allTests = [ 45 | ("testExample", testExample), 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /Examples/shopster-bot/Sources/shopster-bot/Controllers/MigrationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Telegram Bot SDK for Swift (unofficial). 3 | // 4 | // This file containing the example code is in public domain. 5 | // Feel free to copy-paste it and edit it in any way you like. 6 | // 7 | 8 | import Foundation 9 | import GRDB 10 | 11 | class MigrationController { 12 | static var migrator = DatabaseMigrator() 13 | 14 | static func migrate() throws { 15 | // Migrations run in order, once and only once. When a user upgrades the application, only non-applied migrations are run. 16 | 17 | // v1.0 database 18 | migrator.registerMigration("createTables") { db in 19 | try db.execute(sql: 20 | """ 21 | CREATE TABLE sessions ( 22 | chat_id INTEGER PRIMARY KEY, 23 | router_name TEXT NOT NULL 24 | ); 25 | CREATE TABLE items ( 26 | item_id INTEGER PRIMARY KEY AUTOINCREMENT, 27 | chat_id INTEGER, 28 | name TEXT NOT NULL, 29 | purchased BOOLEAN NOT NULL, 30 | FOREIGN KEY(chat_id) REFERENCES sessions(chat_id) ON DELETE CASCADE 31 | ); 32 | CREATE INDEX items_by_chat_idx ON items (chat_id, item_id); 33 | """ 34 | ) 35 | } 36 | 37 | // Migrations for future versions will be inserted here: 38 | // 39 | // // v2.0 database 40 | 41 | try migrator.migrate(DB.queue) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/TelegramBotSDKTests/BlockingServerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockingServerTests.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2018 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import XCTest 14 | @testable import TelegramBotSDK 15 | 16 | class BlockingServerTests: XCTestCase { 17 | 18 | var token: String! 19 | 20 | override func setUp() { 21 | token = readToken(from: "TEST_BOT_TOKEN") 22 | super.setUp() 23 | } 24 | 25 | override func tearDown() { 26 | super.tearDown() 27 | } 28 | 29 | func _testServer() { 30 | let bot = TelegramBot(token: token) 31 | 32 | while let update = bot.nextUpdateSync() { 33 | print("--- update: \(update)") 34 | if let message = update.message, let text = message.text, let chatId = message.from?.id { 35 | if text == "Hello" { 36 | bot.sendMessageAsync(chatId: .chat(chatId), text: "How are you?") 37 | } 38 | } 39 | } 40 | if let lastError = bot.lastError { 41 | print("Server stopped due to error: \(lastError)") 42 | } 43 | } 44 | 45 | static var allTests : [(String, (BlockingServerTests) -> () throws -> Void)] { 46 | return [ 47 | //("testExample", testExample), 48 | ] 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/TelegramBot+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TelegramBot+Utils.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | extension TelegramBot { 16 | public static var unhandledErrorText = "❗ Error while performing the operation." 17 | 18 | @discardableResult 19 | public func reportErrorSync(chatId: Int64, text: String, errorDescription: String) -> Message? { 20 | logger("ERROR: \(errorDescription)") 21 | return sendMessageSync(chatId: .chat(chatId), text: text) 22 | } 23 | 24 | @discardableResult 25 | public func reportErrorSync(chatId: Int64, errorDescription: String) -> Message? { 26 | logger("ERROR: \(errorDescription)") 27 | return sendMessageSync(chatId: .chat(chatId), text: TelegramBot.unhandledErrorText) 28 | } 29 | 30 | public func reportErrorAsync(chatId: Int64, text: String, errorDescription: String, completion: SendMessageCompletion? = nil) { 31 | logger("ERROR: \(errorDescription)") 32 | sendMessageAsync(chatId: .chat(chatId), text: text, completion: completion) 33 | } 34 | 35 | public func reportErrorAsync(chatId: Int64, errorDescription: String, completion: SendMessageCompletion? = nil) { 36 | logger("ERROR: \(errorDescription)") 37 | sendMessageAsync(chatId: .chat(chatId), text: TelegramBot.unhandledErrorText, completion: completion) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Router/ContentType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentType.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | public enum ContentType { 16 | case command(Command) 17 | case commands([Command]) 18 | case from 19 | case forwardFrom 20 | case forwardFromChat 21 | case forwardDate 22 | case replyToMessage 23 | case editDate 24 | case text 25 | case entities 26 | case audio 27 | case document 28 | case photo 29 | case sticker 30 | case video 31 | case voice 32 | case caption 33 | case contact 34 | case location 35 | case venue 36 | case newChatMembers 37 | case leftChatMember 38 | case newChatTitle 39 | case newChatPhoto 40 | case deleteChatPhoto 41 | case groupChatCreated 42 | case supergroupChatCreated 43 | case channelChatCreated 44 | case migrateToChatId 45 | case migrateFromChatId 46 | case pinnedMessage 47 | case callback_query(data: String?) 48 | 49 | case editedFrom 50 | case editedForwardFrom 51 | case editedForwardFromChat 52 | case editedForwardDate 53 | case editedReplyToMessage 54 | case editedEditDate 55 | case editedText 56 | case editedEntities 57 | case editedAudio 58 | case editedDocument 59 | case editedPhoto 60 | case editedSticker 61 | case editedVideo 62 | case editedVoice 63 | case editedCaption 64 | case editedContact 65 | case editedLocation 66 | case editedVenue 67 | } 68 | -------------------------------------------------------------------------------- /Examples/hello-bot/.gitignore: -------------------------------------------------------------------------------- 1 | HELLO_BOT_TOKEN 2 | hello-bot.xcodeproj 3 | .DS_Store 4 | *.swp 5 | 6 | # Xcode 7 | # 8 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 9 | 10 | ## Build generated 11 | build/ 12 | DerivedData 13 | 14 | ## Various settings 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | 25 | ## Other 26 | *.xccheckout 27 | *.moved-aside 28 | *.xcuserstate 29 | *.xcscmblueprint 30 | 31 | ## Obj-C/Swift specific 32 | *.hmap 33 | *.ipa 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | Packages/ 43 | .build/ 44 | Package.resolved 45 | 46 | # CocoaPods 47 | # 48 | # We recommend against adding the Pods directory to your .gitignore. However 49 | # you should judge for yourself, the pros and cons are mentioned at: 50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 51 | # 52 | # Pods/ 53 | 54 | # Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | # Carthage/Checkouts 58 | 59 | Carthage/Build 60 | 61 | # fastlane 62 | # 63 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 64 | # screenshots whenever they are needed. 65 | # For more information about the recommended setup visit: 66 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 67 | 68 | fastlane/report.xml 69 | fastlane/screenshots 70 | -------------------------------------------------------------------------------- /Examples/word-reverse-bot/.gitignore: -------------------------------------------------------------------------------- 1 | WORD_REVERSE_BOT_TOKEN 2 | word-reverse-bot.xcodeproj 3 | .DS_Store 4 | *.swp 5 | 6 | # Xcode 7 | # 8 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 9 | 10 | ## Build generated 11 | build/ 12 | DerivedData 13 | 14 | ## Various settings 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | 25 | ## Other 26 | *.xccheckout 27 | *.moved-aside 28 | *.xcuserstate 29 | *.xcscmblueprint 30 | 31 | ## Obj-C/Swift specific 32 | *.hmap 33 | *.ipa 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | Packages/ 43 | .build/ 44 | Package.resolved 45 | 46 | # CocoaPods 47 | # 48 | # We recommend against adding the Pods directory to your .gitignore. However 49 | # you should judge for yourself, the pros and cons are mentioned at: 50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 51 | # 52 | # Pods/ 53 | 54 | # Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | # Carthage/Checkouts 58 | 59 | Carthage/Build 60 | 61 | # fastlane 62 | # 63 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 64 | # screenshots whenever they are needed. 65 | # For more information about the recommended setup visit: 66 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 67 | 68 | fastlane/report.xml 69 | fastlane/screenshots 70 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | /// Reads token from environment variable or from a file. 16 | /// 17 | /// - Returns: token. 18 | public func readToken(from name: String) -> String { 19 | guard let token: String = readConfigurationValue(name) else { 20 | print("\n" + 21 | "-----\n" + 22 | "ERROR\n" + 23 | "-----\n" + 24 | "Please create either:\n" + 25 | " - an environment variable named \(name)\n" + 26 | " - a file named \(name)\n" + 27 | "containing your bot's token.\n\n") 28 | exit(1) 29 | } 30 | return token 31 | } 32 | 33 | /// Reads value from environment variable or from a file. 34 | /// 35 | /// - Returns: `String`. 36 | public func readConfigurationValue(_ name: String) -> String? { 37 | let environment = ProcessInfo.processInfo.environment 38 | var value = environment[name] 39 | if value == nil { 40 | do { 41 | value = try String(contentsOfFile: name, encoding: String.Encoding.utf8) 42 | } catch { 43 | } 44 | } 45 | if let value = value { 46 | return value.trimmed() 47 | } 48 | return nil 49 | } 50 | 51 | /// Reads value from environment variable or from a file. 52 | /// 53 | /// - Returns: `Int64`. 54 | public func readConfigurationValue(_ name: String) -> Int64? { 55 | if let v: String = readConfigurationValue(name) { 56 | return Int64(v) 57 | } 58 | return nil 59 | } 60 | 61 | /// Reads value from environment variable or from a file. 62 | /// 63 | /// - Returns: `Int`. 64 | public func readConfigurationValue(_ name: String) -> Int? { 65 | if let v: String = readConfigurationValue(name) { 66 | return Int(v) 67 | } 68 | return nil 69 | } 70 | 71 | -------------------------------------------------------------------------------- /Examples/shopster-bot/Sources/shopster-bot/Controllers/AddController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Telegram Bot SDK for Swift (unofficial). 3 | // 4 | // This file containing the example code is in public domain. 5 | // Feel free to copy-paste it and edit it in any way you like. 6 | // 7 | 8 | import Foundation 9 | import TelegramBotSDK 10 | 11 | class AddController { 12 | typealias T = AddController 13 | 14 | init() { 15 | routers["add"] = Router(bot: bot) { router in 16 | router[Commands.help, .slashRequired] = onHelp 17 | router[Commands.cancel, .slashRequired] = onCancel 18 | router.unmatched = addItem 19 | } 20 | } 21 | 22 | func onHelp(context: Context) -> Bool { 23 | showHelp(context: context) 24 | return true 25 | } 26 | 27 | func onCancel(context: Context) throws -> Bool { 28 | try mainController.showMainMenu(context: context, text: "Cancelled") 29 | context.session.routerName = "main" 30 | try context.session.save() 31 | return true 32 | } 33 | 34 | func addItem(context: Context) throws -> Bool { 35 | guard let chatId = context.chatId else { return false } 36 | let name = context.args.scanRestOfString() 37 | guard name != Commands.add[0] else { return false } // Button pressed twice in a row 38 | try Item.add(name: name, chatId: chatId) 39 | try mainController.showMainMenu(context: context, text: "Added: \(name)") 40 | context.session.routerName = "main" 41 | try context.session.save() 42 | return true 43 | } 44 | 45 | func showHelp(context: Context) { 46 | let text = "Type a name to add or /cancel to cancel." 47 | 48 | if context.privateChat { 49 | context.respondAsync(text, replyMarkup: ReplyKeyboardRemove()) 50 | } else { 51 | let replyTo = context.message?.messageId 52 | var markup = ForceReply() 53 | markup.selective = replyTo != nil 54 | context.respondAsync(text, 55 | replyToMessageId: replyTo, 56 | replyMarkup: markup) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Examples/shopster-bot/Sources/shopster-bot/Records/Item.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Telegram Bot SDK for Swift (unofficial). 3 | // 4 | // This file containing the example code is in public domain. 5 | // Feel free to copy-paste it and edit it in any way you like. 6 | // 7 | 8 | import Foundation 9 | import GRDB 10 | 11 | class Item: Record { 12 | var itemId: Int64? 13 | var chatId: Int64 14 | var name: String 15 | var purchased: Bool 16 | 17 | override class var databaseTableName: String { 18 | return "items" 19 | } 20 | 21 | required init(row: Row) { 22 | itemId = row["item_id"] 23 | chatId = row["chat_id"] 24 | name = row["name"] 25 | purchased = row["purchased"] 26 | super.init(row: row) 27 | } 28 | 29 | init(name: String, chatId: Int64) { 30 | self.chatId = chatId 31 | self.name = name 32 | self.purchased = false 33 | super.init() 34 | } 35 | 36 | override func didInsert(with rowID: Int64, for column: String?) { 37 | itemId = rowID 38 | } 39 | 40 | static func add(name: String, chatId: Int64) throws { 41 | let item = Item(name: name, chatId: chatId) 42 | try DB.queue.inDatabase { db in 43 | try item.insert(db) 44 | } 45 | } 46 | 47 | static func allItems(in chatId: Int64) -> [Item] { 48 | return try! DB.queue.inDatabase { db in 49 | try Item.fetchAll(db, sql: "SELECT * FROM items WHERE chat_id = ?", arguments: [chatId]) 50 | } 51 | } 52 | 53 | static func item(itemId: Int64, from chatId: Int64) throws -> Item? { 54 | let item = try DB.queue.inDatabase { db in 55 | try Item.fetchOne(db, sql: "SELECT * FROM items WHERE chat_id = ? AND item_id = ?", arguments: [chatId, itemId]) 56 | } 57 | return item 58 | } 59 | 60 | static func deletePurchased(in chatId: Int64) throws { 61 | try DB.queue.inDatabase { db in 62 | try db.execute(sql: "DELETE FROM items WHERE chat_id = ? AND purchased = 1", arguments: [chatId]) 63 | } 64 | } 65 | 66 | func save() throws { 67 | try DB.queue.inDatabase { db in 68 | try self.save(db) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Types/InputMessageContent.swift: -------------------------------------------------------------------------------- 1 | // Telegram Bot SDK for Swift (unofficial). 2 | // This file is autogenerated by API/generate_wrappers.rb script. 3 | 4 | import Foundation 5 | 6 | 7 | /// This object represents the content of a message to be sent as a result of an inline query. Telegram clients currently support the following 4 types: 8 | /// InputTextMessageContent 9 | /// InputLocationMessageContent 10 | /// InputVenueMessageContent 11 | /// InputContactMessageContent 12 | /// 13 | /// - SeeAlso: 14 | 15 | public enum InputMessageContent: Codable { 16 | case text(InputTextMessageContent) 17 | case location(InputLocationMessageContent) 18 | case venue(InputVenueMessageContent) 19 | case contact(InputContactMessageContent) 20 | case unknown 21 | 22 | public init(from decoder: Decoder) throws { 23 | if let text = try? decoder.singleValueContainer().decode(InputTextMessageContent.self) { 24 | self = .text(text) 25 | return 26 | } 27 | 28 | if let location = try? decoder.singleValueContainer().decode(InputLocationMessageContent.self) { 29 | self = .location(location) 30 | return 31 | } 32 | 33 | if let venue = try? decoder.singleValueContainer().decode(InputVenueMessageContent.self) { 34 | self = .venue(venue) 35 | return 36 | } 37 | 38 | if let contact = try? decoder.singleValueContainer().decode(InputContactMessageContent.self) { 39 | self = .contact(contact) 40 | return 41 | } 42 | 43 | self = .unknown 44 | } 45 | 46 | public func encode(to encoder: Encoder) throws { 47 | var container = encoder.singleValueContainer() 48 | switch self { 49 | case let .text(text): 50 | try container.encode(text) 51 | case let .location(location): 52 | try container.encode(location) 53 | case let .venue(venue): 54 | try container.encode(venue) 55 | case let .contact(contact): 56 | try container.encode(contact) 57 | default: 58 | fatalError("Unknown should not be used") 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Examples/shopster-bot/Sources/shopster-bot/Controllers/DeleteController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Telegram Bot SDK for Swift (unofficial). 3 | // 4 | // This file containing the example code is in public domain. 5 | // Feel free to copy-paste it and edit it in any way you like. 6 | // 7 | 8 | import Foundation 9 | import TelegramBotSDK 10 | 11 | class DeleteController { 12 | typealias T = DeleteController 13 | 14 | init() { 15 | routers["delete"] = Router(bot: bot) { router in 16 | router[Commands.help] = onHelp 17 | router[Commands.cancel] = onCancel 18 | router[Commands.confirmDeletion] = onConfirmDeletion 19 | router.unmatched = onCancel // safe default 20 | } 21 | } 22 | 23 | func onHelp(context: Context) -> Bool { 24 | let text = "/confirm_deletion Confirm deletion\n" + 25 | "/cancel Cancel" 26 | showConfirmationKeyboard(context: context, text: text) 27 | return true 28 | } 29 | 30 | func onCancel(context: Context) throws -> Bool { 31 | try mainController.showMainMenu(context: context, text: "Cancelled") 32 | context.session.routerName = "main" 33 | try context.session.save() 34 | return true 35 | } 36 | 37 | func onConfirmDeletion(context: Context) throws -> Bool { 38 | guard let chatId = context.chatId else { return false } 39 | try Item.deletePurchased(in: chatId) 40 | try mainController.showMainMenu(context: context, text: "Purchased items were deleted.") 41 | context.session.routerName = "main" 42 | try context.session.save() 43 | return true 44 | } 45 | 46 | func showConfirmationKeyboard(context: Context, text: String) { 47 | let replyTo = context.privateChat ? nil : context.message?.messageId 48 | 49 | var markup = ReplyKeyboardMarkup() 50 | //markup.one_time_keyboard = true 51 | markup.resizeKeyboard = true 52 | markup.selective = replyTo != nil 53 | markup.keyboardStrings = [ 54 | [ Commands.cancel[0], Commands.confirmDeletion[0] ] 55 | ] 56 | context.respondAsync(text, 57 | replyToMessageId: replyTo, 58 | replyMarkup: markup) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/DataTaskError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataTaskError.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | import CCurl 16 | 17 | /// Telegram DataTask errors 18 | public enum DataTaskError { 19 | /// Invalid request 20 | case invalidRequest 21 | 22 | /// Libcurl initialization error 23 | case libcurlInitError 24 | 25 | /// Libcurl error 26 | case libcurlError(code: CURLcode, description: String) 27 | 28 | /// Aborted by callback 29 | case libcurlAbortedByCallback 30 | 31 | /// Status Code is not 200 (OK) 32 | case invalidStatusCode(statusCode: Int, telegramDescription: String, telegramErrorCode: Int, data: Data?) 33 | 34 | /// Telegram server returned no data 35 | case noDataReceived 36 | 37 | /// Server error (server returned "ok: false") 38 | case serverError(data: Data) 39 | 40 | /// Codable failed to decode to type 41 | case decodeError(data: Data) 42 | } 43 | 44 | extension DataTaskError: CustomDebugStringConvertible { 45 | // MARK: CustomDebugStringConvertible 46 | public var debugDescription: String { 47 | switch self { 48 | case .invalidRequest: 49 | return "Invalid HTTP request" 50 | case .libcurlInitError: 51 | return "Libcurl initialization error" 52 | case let .libcurlError(code, description): 53 | return "Libcurl error \(code.rawValue): \(description)" 54 | case .libcurlAbortedByCallback: 55 | return "Libcurl aborted by callback" 56 | case let .invalidStatusCode(statusCode, telegramDescription, _, _): 57 | return "Expected status code 200, got \(statusCode): \(telegramDescription)" 58 | case .noDataReceived: 59 | return "No data received" 60 | case .serverError: 61 | return "Telegram server returned an error" 62 | case .decodeError: 63 | return "Codable failed to decode result" 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Extensions/String+ExtractBotCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+ExtractBotCommand.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | extension String { 16 | /// - Parameter botName: bot name to remove. 17 | /// - Returns: "/command@botName arguments" -> "/command arguments". Nil if bot name does not match `botName` parameter. 18 | public func without(botName: BotName) -> String? { 19 | let scanner = Scanner(string: self) 20 | scanner.caseSensitive = false 21 | scanner.charactersToBeSkipped = nil 22 | 23 | let whitespaceAndNewline = CharacterSet.whitespacesAndNewlines 24 | scanner.skipCharacters(from: whitespaceAndNewline) 25 | 26 | guard scanner.skipString("/") else { 27 | return self 28 | } 29 | 30 | let alphanumericCharacters = CharacterSet.alphanumerics 31 | guard scanner.skipCharacters(from: alphanumericCharacters) else { 32 | return self 33 | } 34 | 35 | let usernameSeparatorIndex = scanner.scanLocation 36 | 37 | let usernameSeparator = "@" 38 | guard scanner.skipString(usernameSeparator) else { 39 | return self 40 | } 41 | 42 | // A set of characters allowed in bot names 43 | let usernameCharacters = CharacterSet(charactersIn: 44 | "abcdefghijklmnopqrstuvwxyz" + 45 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + 46 | "1234567890_") 47 | guard let username = scanner.scanCharacters(from: usernameCharacters) else { 48 | // Empty bot name. Treat as no bot name and process the comamnd. 49 | return self 50 | } 51 | 52 | guard BotName(username: username) == botName else { 53 | // Another bot's message, skip it. 54 | return nil 55 | } 56 | 57 | let t = NSString(string: self) 58 | return t.substring(to: usernameSeparatorIndex) + 59 | t.substring(from: scanner.scanLocation) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Types/InputMedia.swift: -------------------------------------------------------------------------------- 1 | // Telegram Bot SDK for Swift (unofficial). 2 | // This file is autogenerated by API/generate_wrappers.rb script. 3 | 4 | import Foundation 5 | 6 | 7 | /// This object represents the content of a media message to be sent. It should be one of 8 | /// InputMediaPhoto 9 | /// InputMediaVideo 10 | /// 11 | /// - SeeAlso: 12 | 13 | public enum InputMedia: Codable { 14 | case photo(InputMediaPhoto) 15 | case video(InputMediaVideo) 16 | case audio(InputMediaAudio) 17 | case document(InputMediaDocument) 18 | case animation(InputMediaAnimation) 19 | case unknown 20 | 21 | public init(from decoder: Decoder) throws { 22 | if let photo = try? decoder.singleValueContainer().decode(InputMediaPhoto.self) { 23 | self = .photo(photo) 24 | return 25 | } 26 | 27 | if let video = try? decoder.singleValueContainer().decode(InputMediaVideo.self) { 28 | self = .video(video) 29 | return 30 | } 31 | 32 | if let audio = try? decoder.singleValueContainer().decode(InputMediaAudio.self) { 33 | self = .audio(audio) 34 | return 35 | } 36 | 37 | if let document = try? decoder.singleValueContainer().decode(InputMediaDocument.self) { 38 | self = .document(document) 39 | return 40 | } 41 | 42 | if let animation = try? decoder.singleValueContainer().decode(InputMediaAnimation.self) { 43 | self = .animation(animation) 44 | return 45 | } 46 | 47 | self = .unknown 48 | } 49 | 50 | public func encode(to encoder: Encoder) throws { 51 | var container = encoder.singleValueContainer() 52 | switch self { 53 | case let .photo(photo): 54 | try container.encode(photo) 55 | case let .video(video): 56 | try container.encode(video) 57 | case let .audio(audio): 58 | try container.encode(audio) 59 | case let .document(document): 60 | try container.encode(document) 61 | case let .animation(animation): 62 | try container.encode(animation) 63 | default: 64 | fatalError("Unknown should not be used") 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Types/ReplyMarkup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReplyMarkup.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | public enum ReplyMarkup: Codable { 16 | case inlineKeyboardMarkup(InlineKeyboardMarkup) 17 | case replyKeyboardMarkup(ReplyKeyboardMarkup) 18 | case replyKeyboardRemove(ReplyKeyboardRemove) 19 | case forceReply(ForceReply) 20 | case unknown 21 | 22 | public init(from decoder: Decoder) throws { 23 | if let inlineKeyboardMarkup = try? decoder.singleValueContainer().decode(InlineKeyboardMarkup.self) { 24 | self = .inlineKeyboardMarkup(inlineKeyboardMarkup) 25 | return 26 | } 27 | 28 | if let replyKeyboardMarkup = try? decoder.singleValueContainer().decode(ReplyKeyboardMarkup.self) { 29 | self = .replyKeyboardMarkup(replyKeyboardMarkup) 30 | return 31 | } 32 | 33 | if let replyKeyboardRemove = try? decoder.singleValueContainer().decode(ReplyKeyboardRemove.self) { 34 | self = .replyKeyboardRemove(replyKeyboardRemove) 35 | return 36 | } 37 | 38 | if let forceReply = try? decoder.singleValueContainer().decode(ForceReply.self) { 39 | self = .forceReply(forceReply) 40 | return 41 | } 42 | 43 | self = .unknown 44 | } 45 | 46 | public func encode(to encoder: Encoder) throws { 47 | var container = encoder.singleValueContainer() 48 | switch self { 49 | case let .inlineKeyboardMarkup(inlineKeyboardMarkup): 50 | try container.encode(inlineKeyboardMarkup) 51 | case let .replyKeyboardMarkup(replyKeyboardMarkup): 52 | try container.encode(replyKeyboardMarkup) 53 | case let .replyKeyboardRemove(replyKeyboardRemove): 54 | try container.encode(replyKeyboardRemove) 55 | case let .forceReply(forceReply): 56 | try container.encode(forceReply) 57 | default: 58 | fatalError("Unknown should not be used") 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Router/Arguments.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Arguments.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | public class Arguments { 16 | typealias T = Arguments 17 | 18 | public let scanner: Scanner 19 | 20 | public var isAtEnd: Bool { 21 | return scanner.isAtEnd 22 | } 23 | 24 | static let whitespaceAndNewline = CharacterSet.whitespacesAndNewlines 25 | 26 | init(scanner: Scanner) { 27 | self.scanner = scanner 28 | } 29 | 30 | public func scanWord() -> String? { 31 | return scanner.scanUpToCharacters(from: T.whitespaceAndNewline) 32 | } 33 | 34 | public func scanWords() -> [String] { 35 | var words = [String]() 36 | while let word = scanWord() { 37 | words.append(word) 38 | } 39 | return words 40 | } 41 | 42 | public func scanInteger() -> Int? { 43 | guard let word = scanWord() else { 44 | return nil 45 | } 46 | let validator = Scanner(string: word) 47 | validator.charactersToBeSkipped = nil 48 | guard let value = validator.scanInt(), validator.isAtEnd else { 49 | return nil 50 | } 51 | return value 52 | } 53 | 54 | public func scanInt64() -> Int64? { 55 | guard let word = scanWord() else { 56 | return nil 57 | } 58 | let validator = Scanner(string: word) 59 | validator.charactersToBeSkipped = nil 60 | guard let value = validator.scanInt64(), validator.isAtEnd else { 61 | return nil 62 | } 63 | return value 64 | } 65 | 66 | public func scanDouble() -> Double? { 67 | guard let word = scanWord() else { 68 | return nil 69 | } 70 | let validator = Scanner(string: word) 71 | validator.charactersToBeSkipped = nil 72 | guard let value = validator.scanDouble(), validator.isAtEnd else { 73 | return nil 74 | } 75 | return value 76 | } 77 | 78 | public func scanRestOfString() -> String { 79 | guard let restOfString = scanner.scanUpTo("") else { 80 | return "" 81 | } 82 | return restOfString 83 | } 84 | 85 | public func skipRestOfString() { 86 | scanner.skipUpTo("") 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Examples/shopster-bot/Sources/shopster-bot/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Telegram Bot SDK for Swift (unofficial). 3 | // 4 | // This file containing the example code is in public domain. 5 | // Feel free to copy-paste it and edit it in any way you like. 6 | // 7 | 8 | import Foundation 9 | import TelegramBotSDK 10 | 11 | print("Checking if database is up to date") 12 | 13 | do { 14 | try MigrationController.migrate() 15 | } catch { 16 | print("Error while migrating the database: \(error)") 17 | exit(1) 18 | } 19 | 20 | // Add SHOPSTER_BOT_TOKEN to environment variables or create a file named 'SHOPSTER_BOT_TOKEN' 21 | // containing your bot's token in app's working dir. 22 | let token = readToken(from: "SHOPSTER_BOT_TOKEN") 23 | 24 | let bot = TelegramBot(token: token) 25 | var routers = [String: Router]() 26 | 27 | let mainController = MainController() 28 | let addController = AddController() 29 | let deleteController = DeleteController() 30 | 31 | // Disable sendMessage notifications by default 32 | bot.defaultParameters["sendMessage"] = ["disable_notification": true] 33 | 34 | print("Ready to accept commands") 35 | 36 | while let update = bot.nextUpdateSync() { 37 | update.prettyPrint() 38 | 39 | // Properties associated with request context 40 | var properties = [String: AnyObject]() 41 | 42 | // ChatId is needed for choosing a router associated with particular chat 43 | guard let chatId = update.message?.chat.id ?? 44 | update.callbackQuery?.message?.chat.id else { 45 | continue 46 | } 47 | 48 | do { 49 | // Fetch Session object from database. It will be created if missing. 50 | let session = try Session.session(for: chatId) 51 | 52 | // Fetching from database is expensive operation. Store the session 53 | // in properties to avoid fetching it again in handlers 54 | properties["session"] = session 55 | 56 | let router = routers[session.routerName] 57 | if let router = router { 58 | try router.process(update: update, properties: properties) 59 | } else { 60 | print("Warning: chat \(chatId) has invalid router: \(session.routerName)") 61 | } 62 | } catch { 63 | bot.reportErrorAsync(chatId: chatId, 64 | text: "❗ Error while performing the operation.", 65 | errorDescription: "Recovered from exception: \(error)") 66 | } 67 | } 68 | 69 | print("Server stopped due to error: \(bot.lastError.unwrapOptional)") 70 | exit(1) 71 | -------------------------------------------------------------------------------- /Rapier/Sources/RapierCLI/Generators/OverviewGenerator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Rapier 3 | 4 | private struct Context { 5 | let directory: String 6 | 7 | var outTypes: String = "" 8 | var outMethods: String = "" 9 | 10 | init(directory: String) { 11 | self.directory = directory 12 | } 13 | } 14 | 15 | class OverviewGenerator: CodeGenerator { 16 | private var context: Context 17 | 18 | required init(directory: String) { 19 | self.context = Context(directory: directory) 20 | } 21 | 22 | func start() throws { 23 | 24 | } 25 | 26 | func beforeGeneratingTypes() throws { 27 | context.outTypes.append("TYPES:\n\n") 28 | } 29 | 30 | func generateType(name: String, info: TypeInfo) throws { 31 | context.outTypes.append("\(name)\n") 32 | info.fields.forEach { fieldInfo in 33 | context.outTypes.append(" \(fieldInfo.name): \(fieldInfo.type)") 34 | if (fieldInfo.isOptional) { 35 | context.outTypes.append(" [optional]") 36 | } 37 | context.outTypes.append("\n") 38 | } 39 | } 40 | 41 | func afterGeneratingTypes() throws { 42 | context.outTypes.append("\nEND\n") 43 | } 44 | 45 | func beforeGeneratingMethods() throws { 46 | context.outMethods.append("METHODS:\n\n") 47 | } 48 | 49 | func generateMethod(name: String, info: MethodInfo) throws { 50 | context.outMethods.append("\(name)\n") 51 | info.parameters.forEach { fieldInfo in 52 | context.outMethods.append(" \(fieldInfo.name): \(fieldInfo.type)") 53 | if (fieldInfo.isOptional) { 54 | context.outMethods.append(" [optional]") 55 | } 56 | context.outMethods.append("\n") 57 | } 58 | } 59 | 60 | func afterGeneratingMethods() throws { 61 | context.outMethods.append("\nEND\n") 62 | } 63 | 64 | func finish() throws { 65 | try saveTypes() 66 | try saveMethods() 67 | } 68 | } 69 | 70 | extension OverviewGenerator { 71 | private func saveTypes() throws { 72 | let dir = URL(fileURLWithPath: context.directory, isDirectory: true) 73 | let file = dir.appendingPathComponent("types.swift", isDirectory: false) 74 | try context.outTypes.write(to: file, atomically: true, encoding: .utf8) 75 | } 76 | 77 | private func saveMethods() throws { 78 | let dir = URL(fileURLWithPath: context.directory, isDirectory: true) 79 | let file = dir.appendingPathComponent("methods.swift", isDirectory: false) 80 | try context.outMethods.write(to: file, atomically: true, encoding: .utf8) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Methods/TelegramBot+getUpdates+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TelegramBot+getUpdates+Utils.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | import CCurl 15 | 16 | extension TelegramBot { 17 | /// Returns next unprocessed update from Telegram. 18 | /// 19 | /// If no more updates are available in local queue, the method blocks while 20 | /// trying to fetch more from the server. 21 | /// 22 | /// - Returns: `Update` object. Nil on error, in which case details can be 23 | /// obtained using `lastError` property. 24 | public func nextUpdateSync() -> Update? { 25 | if unprocessedUpdates.isEmpty { 26 | var updates: [Update]? 27 | var retryCount = 0 28 | while true { 29 | updates = getUpdatesSync(offset: nextOffset, limit: defaultUpdatesLimit, timeout: defaultUpdatesTimeout) 30 | if updates == nil { 31 | // Retry on temporary problems 32 | if autoReconnect, 33 | let error = lastError, 34 | case .libcurlError(let code, _) = error 35 | { 36 | switch code { 37 | case CURLE_COULDNT_RESOLVE_PROXY, CURLE_COULDNT_RESOLVE_HOST, CURLE_COULDNT_CONNECT, CURLE_OPERATION_TIMEDOUT, CURLE_SSL_CONNECT_ERROR, CURLE_SEND_ERROR, CURLE_RECV_ERROR: 38 | let delay = reconnectDelay(retryCount) 39 | retryCount += 1 40 | if delay == 0.0 { 41 | logger("Reconnect attempt \(retryCount), will retry at once") 42 | } else { 43 | logger("Reconnect attempt \(retryCount), will retry after \(delay) sec") 44 | wait(seconds: delay) 45 | } 46 | continue 47 | default: 48 | break 49 | } 50 | } 51 | // Unrecoverable error, report to caller 52 | return nil 53 | } 54 | if let updates = updates, !updates.isEmpty { 55 | break 56 | } 57 | // else try again 58 | } 59 | unprocessedUpdates = updates! 60 | } 61 | 62 | guard let update = unprocessedUpdates.first else { 63 | return nil 64 | } 65 | 66 | let nextUpdateId = update.updateId + 1 67 | if nextOffset == nil || nextUpdateId > nextOffset! { 68 | nextOffset = nextUpdateId 69 | } 70 | unprocessedUpdates.remove(at: 0) 71 | return update 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Router/Router+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Router+Helpers.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | extension Router { 16 | // add() taking string 17 | 18 | public func add(_ commandString: String, _ options: Command.Options = [], _ handler: @escaping (Context) throws -> Bool) { 19 | add(Command(commandString, options: options), handler) 20 | } 21 | 22 | // Subscripts taking ContentType 23 | 24 | public subscript(_ contentType: ContentType) -> (Context) throws->Bool { 25 | get { fatalError("Not implemented") } 26 | set { add(contentType, newValue) } 27 | } 28 | 29 | // Subscripts taking Command 30 | 31 | public subscript(_ command: Command) -> (Context) throws->Bool { 32 | get { fatalError("Not implemented") } 33 | set { add(command, newValue) } 34 | } 35 | 36 | public subscript(_ commands: [Command]) -> (Context) throws->Bool { 37 | get { fatalError("Not implemented") } 38 | set { add(commands, newValue) } 39 | } 40 | 41 | public subscript(_ commands: Command...) -> (Context) throws->Bool { 42 | get { fatalError("Not implemented") } 43 | set { add(commands, newValue) } 44 | } 45 | 46 | // Subscripts taking String 47 | 48 | public subscript(_ commandString: String, _ options: Command.Options) -> (Context) throws -> Bool { 49 | get { fatalError("Not implemented") } 50 | set { add(Command(commandString, options: options), newValue) } 51 | } 52 | 53 | public subscript(_ commandString: String) -> (Context) throws -> Bool { 54 | get { fatalError("Not implemented") } 55 | set { add(Command(commandString), newValue) } 56 | } 57 | 58 | public subscript(_ commandStrings: [String], _ options: Command.Options) -> (Context) throws -> Bool { 59 | get { fatalError("Not implemented") } 60 | set { 61 | let commands = commandStrings.map { Command($0, options: options) } 62 | add(commands, newValue) 63 | } 64 | } 65 | 66 | public subscript(commandStrings: [String]) -> (Context) throws -> Bool { 67 | get { fatalError("Not implemented") } 68 | set { 69 | let commands = commandStrings.map { Command($0) } 70 | add(commands, newValue) 71 | } 72 | } 73 | 74 | // Segmentation fault 75 | // public subscript(commandStrings: String..., _ options: Command.Options) -> (Context) throws -> Bool { 76 | // get { fatalError("Not implemented") } 77 | // set { 78 | // let commands = commandStrings.map { Command($0, options: options) } 79 | // add(commands, newValue) 80 | // } 81 | // } 82 | 83 | public subscript(commandStrings: String...) -> (Context) throws -> Bool { 84 | get { fatalError("Not implemented") } 85 | set { 86 | let commands = commandStrings.map { Command($0) } 87 | add(commands, newValue) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Extensions/Scanner+Compatibility.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Scanner { 4 | #if os(OSX) 5 | func scanInt() -> Int? { 6 | var result: Int = 0 7 | return scanInt(&result) ? result : nil 8 | } 9 | #endif 10 | 11 | #if os(OSX) 12 | func scanInt32() -> Int32? { 13 | var result: Int32 = 0 14 | return scanInt32(&result) ? result : nil 15 | } 16 | #endif 17 | 18 | func scanInt64() -> Int64? { 19 | var result: Int64 = 0 20 | return scanInt64(&result) ? result : nil 21 | } 22 | 23 | func scanUInt64() -> UInt64? { 24 | var result: UInt64 = 0 25 | return scanUnsignedLongLong(&result) ? result : nil 26 | } 27 | 28 | #if os(OSX) 29 | func scanFloat() -> Float? { 30 | var result: Float = 0.0 31 | return scanFloat(&result) ? result : nil 32 | } 33 | #endif 34 | 35 | #if os(OSX) 36 | func scanDouble() -> Double? { 37 | var result: Double = 0.0 38 | return scanDouble(&result) ? result : nil 39 | } 40 | #endif 41 | 42 | func scanHexUInt32() -> UInt32? { 43 | var result: UInt32 = 0 44 | return scanHexInt32(&result) ? result : nil 45 | } 46 | 47 | func scanHexUInt64() -> UInt64? { 48 | var result: UInt64 = 0 49 | return scanHexInt64(&result) ? result : nil 50 | } 51 | 52 | func scanHexFloat() -> Float? { 53 | var result: Float = 0.0 54 | return scanHexFloat(&result) ? result : nil 55 | } 56 | 57 | func scanHexDouble() -> Double? { 58 | var result: Double = 0.0 59 | return scanHexDouble(&result) ? result : nil 60 | } 61 | 62 | #if os(OSX) 63 | func scanString(_ searchString: String) -> String? { 64 | var result: NSString? 65 | guard scanString(searchString, into: &result) else { return nil } 66 | return result as String? 67 | } 68 | #endif 69 | 70 | #if os(Linux) || os(Windows) 71 | func scanCharacters(from set: CharacterSet) -> String? { 72 | return scanCharactersFromSet(set) 73 | } 74 | #elseif os(OSX) 75 | func scanCharacters(from: CharacterSet) -> String? { 76 | var result: NSString? 77 | guard scanCharacters(from: from, into: &result) else { return nil } 78 | return result as String? 79 | } 80 | #endif 81 | 82 | #if os(Linux) || os(Windows) 83 | func scanUpTo(_ string: String) -> String? { 84 | if string.isEmpty { 85 | return scanUpToCharacters(from: CharacterSet()) 86 | } 87 | return scanUpToString(string) 88 | } 89 | #elseif os(OSX) 90 | func scanUpTo(_ string: String) -> String? { 91 | var result: NSString? 92 | guard scanUpTo(string, into: &result) else { return nil } 93 | return result as String? 94 | } 95 | #endif 96 | 97 | #if os(Linux) || os(Windows) 98 | func scanUpToCharacters(from set: CharacterSet) -> String? { 99 | return scanUpToCharactersFromSet(set) 100 | } 101 | #elseif os(OSX) 102 | func scanUpToCharacters(from set: CharacterSet) -> String? { 103 | var result: NSString? 104 | guard scanUpToCharacters(from: set, into: &result) else { return nil } 105 | return result as String? 106 | } 107 | #endif 108 | } 109 | 110 | 111 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Extensions/String+HTTP.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+HTTP.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | extension String { 16 | struct HTTPData { 17 | // "0123456789ABCDEF" 18 | static let hexDigits: [CChar] = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70] 19 | 20 | static let formUrlencodedAllowedCharacters: CharacterSet = { 21 | var cs = CharacterSet() 22 | cs.insert(charactersIn: 23 | "0123456789" + 24 | "abcdefghijklmnopqrstuvwxyz" + 25 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + 26 | "-._* ") 27 | return cs 28 | }() 29 | 30 | static let urlQueryAllowedCharacters: CharacterSet = { 31 | var cs = CharacterSet() 32 | cs.insert(charactersIn: 33 | "0123456789" + 34 | "abcdefghijklmnopqrstuvwxyz" + 35 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + 36 | "-._~") 37 | return cs 38 | }() 39 | } 40 | 41 | /// Replaces spaces with `'+'`, percent-encodes everything 42 | /// else except alphanumerics and `'-._*'` 43 | /// 44 | /// Should be used for encoding parameter values when 45 | /// using `application/x-www-form-urlencoded` Content-Type. 46 | /// 47 | /// - SeeAlso: `func urlQueryEncode() -> String` 48 | /// - Returns: Encoded string 49 | public func formUrlencode() -> String { 50 | #if os(Linux) 51 | // https://bugs.swift.org/browse/SR-3216 52 | let encoded = _addingPercentEncoding(withAllowedCharacters: HTTPData.formUrlencodedAllowedCharacters) 53 | #else 54 | let encoded = addingPercentEncoding(withAllowedCharacters: HTTPData.formUrlencodedAllowedCharacters) 55 | #endif 56 | return encoded?.replacingOccurrences(of: " ", with: "+") ?? "" 57 | } 58 | 59 | /// Percent-encodes everything except alphanumerics 60 | /// and `'-._~'`. 61 | /// 62 | /// Should be used for encoding URL query components. 63 | /// 64 | /// - Returns: Encoded string 65 | /// - SeeAlso: `func formUrlencode() -> String` 66 | public func urlQueryEncode() -> String { 67 | #if os(Linux) 68 | // https://bugs.swift.org/browse/SR-3216 69 | return _addingPercentEncoding(withAllowedCharacters: HTTPData.urlQueryAllowedCharacters) ?? "" 70 | #else 71 | return addingPercentEncoding(withAllowedCharacters: HTTPData.urlQueryAllowedCharacters) ?? "" 72 | #endif 73 | } 74 | 75 | private func _addingPercentEncoding(withAllowedCharacters allowedCharacters: CharacterSet) -> String? { 76 | // Workaround broken addingPercentEncoding() 77 | var result: [CChar] = [] 78 | for byte in utf8 { 79 | let scalar = UnicodeScalar(byte) 80 | if allowedCharacters.contains(scalar) { 81 | result.append(CChar(bitPattern: byte)) 82 | } else { 83 | result.append(37) // "%" 84 | result.append(HTTPData.hexDigits[Int((byte & 0xf0) >> 4)]) 85 | result.append(HTTPData.hexDigits[Int(byte & 0x0f)]) 86 | } 87 | } 88 | result.append(0) 89 | let encoded = String(cString: result, encoding: .utf8) 90 | return encoded 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Methods/TelegramBot+sendChatAction+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TelegramBot+sendChatAction+Utils.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | import Dispatch 15 | 16 | 17 | public extension TelegramBot { 18 | enum ChatAction: String, Codable { 19 | case typing = "typing" 20 | case uploadPhoto = "upload_photo" 21 | case recordVideo = "record_video" 22 | case uploadVideo = "upload_video" 23 | case recordAudio = "record_audio" 24 | case uploadAudio = "upload_audio" 25 | case uploadDocument = "upload_document" 26 | case findLocation = "find_location" 27 | } 28 | 29 | /// Use this method when you need to tell the user that something is happening on the bot's side. The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear its typing status). 30 | /// Example: The ImageBot needs some time to process a request and upload the image. Instead of sending a text message along the lines of “Retrieving image, please wait…”, the bot may use sendChatAction with action = upload_photo. The user will see a “sending photo” status for the bot. 31 | /// We only recommend using this method when a response from the bot will take a noticeable amount of time to arrive. 32 | /// - Parameters: 33 | /// - chatId: Unique identifier for the target chat or username of the target channel (in the format @channelusername) 34 | /// - action: Type of action to broadcast. Choose one, depending on what the user is about to receive: typing for text messages, upload_photo for photos, record_video or upload_video for videos, record_audio or upload_audio for audio files, upload_document for general files, find_location for location data. 35 | /// - Returns: Bool on success. Nil on error, in which case `TelegramBot.lastError` contains the details. 36 | /// - Note: Blocking version of the method. 37 | /// 38 | /// - SeeAlso: 39 | @discardableResult 40 | func sendChatActionSync(chatId: ChatId, action: ChatAction, 41 | _ parameters: [String: Encodable?] = [:]) -> Bool? { 42 | return requestSync("sendChatAction", defaultParameters["sendChatAction"], parameters, 43 | ["chat_id": chatId, "action": action]) 44 | } 45 | 46 | /// Use this method when you need to tell the user that something is happening on the bot's side. The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear its typing status). 47 | /// Example: The ImageBot needs some time to process a request and upload the image. Instead of sending a text message along the lines of “Retrieving image, please wait…”, the bot may use sendChatAction with action = upload_photo. The user will see a “sending photo” status for the bot. 48 | /// We only recommend using this method when a response from the bot will take a noticeable amount of time to arrive. 49 | /// - Parameters: 50 | /// - chatId: Unique identifier for the target chat or username of the target channel (in the format @channelusername) 51 | /// - action: Type of action to broadcast. Choose one, depending on what the user is about to receive: typing for text messages, upload_photo for photos, record_video or upload_video for videos, record_audio or upload_audio for audio files, upload_document for general files, find_location for location data. 52 | /// - Returns: Bool on success. Nil on error, in which case `error` contains the details. 53 | /// - Note: Asynchronous version of the method. 54 | /// 55 | /// - SeeAlso: 56 | func sendChatActionAsync(chatId: ChatId, action: ChatAction, 57 | _ parameters: [String: Encodable?] = [:], 58 | queue: DispatchQueue = DispatchQueue.main, 59 | completion: SendChatActionCompletion? = nil) { 60 | requestAsync("sendChatAction", defaultParameters["sendChatAction"], parameters, 61 | ["chat_id": chatId, "action": action], 62 | queue: queue, completion: completion) 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/MimeTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MimeTypea.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | let mimeTypes: [String: String] = [ 16 | "3gp": "video/3gpp", 17 | "3gpp": "video/3gpp", 18 | "7z": "application/x-7z-compressed", 19 | "ai": "application/postscript", 20 | "asf": "video/x-ms-asf", 21 | "asx": "video/x-ms-asf", 22 | "atom": "application/atom+xml", 23 | "avi": "video/x-msvideo", 24 | "bin": "application/octet-stream", 25 | "bmp": "image/x-ms-bmp", 26 | "cco": "application/x-cocoa", 27 | "crt": "application/x-x509-ca-cert", 28 | "css": "text/css", 29 | "deb": "application/octet-stream", 30 | "der": "application/x-x509-ca-cert", 31 | "dll": "application/octet-stream", 32 | "dmg": "application/octet-stream", 33 | "doc": "application/msword", 34 | "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 35 | "ear": "application/java-archive", 36 | "eot": "application/vnd.ms-fontobject", 37 | "eps": "application/postscript", 38 | "exe": "application/octet-stream", 39 | "flv": "video/x-flv", 40 | "gif": "image/gif", 41 | "hqx": "application/mac-binhex40", 42 | "htc": "text/x-component", 43 | "htm": "text/html", 44 | "html": "text/html", 45 | "ico": "image/x-icon", 46 | "img": "application/octet-stream", 47 | "iso": "application/octet-stream", 48 | "jad": "text/vnd.sun.j2me.app-descriptor", 49 | "jar": "application/java-archive", 50 | "jardiff": "application/x-java-archive-diff", 51 | "jng": "image/x-jng", 52 | "jnlp": "application/x-java-jnlp-file", 53 | "jpeg": "image/jpeg", 54 | "jpg": "image/jpeg", 55 | "js": "application/javascript", 56 | "json": "application/json", 57 | "kar": "audio/midi", 58 | "kml": "application/vnd.google-earth.kml+xml", 59 | "kmz": "application/vnd.google-earth.kmz", 60 | "m3u8": "application/vnd.apple.mpegurl", 61 | "m4a": "audio/x-m4a", 62 | "m4v": "video/x-m4v", 63 | "md": "text/markdown", 64 | "mpg": "video/mpeg", 65 | "mid": "audio/midi", 66 | "midi": "audio/midi", 67 | "mml": "text/mathml", 68 | "mng": "video/x-mng", 69 | "mov": "video/quicktime", 70 | "mp3": "audio/mpeg", 71 | "mp4": "video/mp4", 72 | "mpeg": "video/mpeg", 73 | "msi": "application/octet-stream", 74 | "msm": "application/octet-stream", 75 | "msp": "application/octet-stream", 76 | "ogg": "audio/ogg", 77 | "pdb": "application/x-pilot", 78 | "pdf": "application/pdf", 79 | "pem": "application/x-x509-ca-cert", 80 | "pl": "application/x-perl", 81 | "pm": "application/x-perl", 82 | "png": "image/png", 83 | "ppt": "application/vnd.ms-powerpoint", 84 | "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", 85 | "prc": "application/x-pilot", 86 | "ps": "application/postscript", 87 | "ra": "audio/x-realaudio", 88 | "rar": "application/x-rar-compressed", 89 | "rpm": "application/x-redhat-package-manager", 90 | "rss": "application/rss+xml", 91 | "rtf": "application/rtf", 92 | "run": "application/x-makeself", 93 | "sea": "application/x-sea", 94 | "shtml": "text/html", 95 | "sit": "application/x-stuffit", 96 | "svg": "image/svg+xml", 97 | "svgz": "image/svg+xml", 98 | "swf": "application/x-shockwave-flash", 99 | "tcl": "application/x-tcl", 100 | "tif": "image/tiff", 101 | "tiff": "image/tiff", 102 | "tk": "application/x-tcl", 103 | "ts": "video/mp2t", 104 | "txt": "text/plain", 105 | "war": "application/java-archive", 106 | "wbmp": "image/vnd.wap.wbmp", 107 | "webm": "video/webm", 108 | "webp": "image/webp", 109 | "wml": "text/vnd.wap.wml", 110 | "wmlc": "application/vnd.wap.wmlc", 111 | "wmv": "video/x-ms-wmv", 112 | "woff": "application/font-woff", 113 | "xhtml": "application/xhtml+xml", 114 | "xls": "application/vnd.ms-excel", 115 | "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 116 | "xml": "text/xml", 117 | "xpi": "application/x-xpinstall", 118 | "xspf": "application/xspf+xml", 119 | "zip": "application/zip" 120 | ] 121 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Router/Command.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Command.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | public class Command { 16 | typealias T = Command 17 | 18 | public struct Options: OptionSet { 19 | public var rawValue: Int 20 | public init(rawValue: Int) { self.rawValue = rawValue } 21 | 22 | /// Do not skip "/" prefix in command name, if present. 23 | /// Note that comparision is still case insensitive by default. 24 | public static let exactMatch = Options(rawValue: 1 << 0) 25 | 26 | /// Require command to be prefixed with "/". 27 | /// By default prefixing is optional. 28 | /// Ignored if `exactMatch` flag is set. 29 | public static let slashRequired = Options(rawValue: 1 << 1) 30 | 31 | /// Case sensitive comparision of commands. 32 | public static let caseSensitive = Options(rawValue: 1 << 2) 33 | } 34 | 35 | static let whitespaceAndNewline = CharacterSet.whitespacesAndNewlines 36 | 37 | let name: String 38 | let nameWords: [String] 39 | let options: Options 40 | 41 | public init(_ name: String, options: Options = []) { 42 | self.options = options 43 | let finalName: String 44 | if !options.contains(.exactMatch) && name.hasPrefix("/") { 45 | finalName = String(name[name.index(after: name.startIndex)...]) 46 | print("WARNING: Command name shouldn't start with '/', the slash is added automatically if needed") 47 | } else { 48 | finalName = name 49 | } 50 | self.name = finalName 51 | nameWords = finalName.components(separatedBy: T.whitespaceAndNewline) 52 | } 53 | 54 | public func fetchFrom(_ scanner: Scanner, caseSensitive: Bool = false) -> (command: String, startsWithSlash: Bool)? { 55 | if nameWords.isEmpty { 56 | // This is "match all" rule 57 | guard let word = scanner.scanUpToCharacters(from: T.whitespaceAndNewline) else { 58 | return nil 59 | } 60 | 61 | let startsWithSlash = word.hasPrefix("/") 62 | if options.contains(.slashRequired) && !startsWithSlash { 63 | return nil 64 | } 65 | return (word, startsWithSlash) 66 | } 67 | 68 | let caseSensitive = caseSensitive || options.contains(.caseSensitive) 69 | var userCommand = "" 70 | var isFirstWord = true 71 | var firstWordStartsWithSlash = false 72 | 73 | // Each word in nameWords should match a word (possibly abbreviated) from scanner 74 | for nameWord in nameWords { 75 | guard let word = scanner.scanUpToCharacters(from: T.whitespaceAndNewline) else { 76 | return nil 77 | } 78 | 79 | if isFirstWord { 80 | firstWordStartsWithSlash = word.hasPrefix("/") 81 | } 82 | 83 | if options.contains(.exactMatch) { 84 | 85 | guard nameWord.hasPrefix(word, caseInsensitive: !caseSensitive) else { 86 | return nil 87 | } 88 | 89 | userCommand += word 90 | 91 | } else { 92 | 93 | if isFirstWord && options.contains(.slashRequired) { 94 | guard firstWordStartsWithSlash else { return nil } 95 | } 96 | 97 | let processedWord: String 98 | if isFirstWord && firstWordStartsWithSlash { 99 | processedWord = String(word[word.index(after: word.startIndex)...]) 100 | } else { 101 | processedWord = word 102 | } 103 | 104 | guard nameWord.hasPrefix(processedWord, caseInsensitive: !caseSensitive) else { 105 | return nil 106 | } 107 | 108 | userCommand += processedWord 109 | } 110 | 111 | isFirstWord = false 112 | } 113 | return (userCommand, firstWordStartsWithSlash) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/TelegramBot+Requests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TelegramBot+Requests.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | import Dispatch 15 | 16 | extension TelegramBot { 17 | /// Perform synchronous request. 18 | /// - Returns: Decodable on success. Nil on error, in which case `lastError` contains the details. 19 | internal func requestSync(_ endpoint: String, _ parameters: [String: Encodable?] = [:]) -> TResult? where TResult: Decodable { 20 | 21 | var retval: TResult! 22 | let sem = DispatchSemaphore(value: 0) 23 | let queue = DispatchQueue.global() 24 | requestAsync(endpoint, parameters, queue: queue) { 25 | (result: TResult?, error: DataTaskError?) in 26 | retval = result 27 | self.lastError = error 28 | sem.signal() 29 | } 30 | RunLoop.current.waitForSemaphore(sem) 31 | return retval 32 | } 33 | 34 | /// Perform synchronous request. 35 | /// - Returns: Decodable on success. Nil on error, in which case `lastError` contains the details. 36 | internal func requestSync(_ endpoint: String, _ parameters: [String: Encodable?]?...) -> TResult? where TResult: Decodable { 37 | return requestSync(endpoint, mergeParameters(parameters)) 38 | } 39 | 40 | /// Perform asynchronous request. 41 | /// - Returns: Decodable on success. Nil on error, in which case `error` contains the details. 42 | internal func requestAsync(_ endpoint: String, _ parameters: [String: Encodable?] = [:], queue: DispatchQueue = DispatchQueue.main, completion: ((_ result: TResult?, _ error: DataTaskError?) -> ())?) where TResult: Decodable { 43 | 44 | startDataTaskForEndpoint(endpoint, parameters: parameters, resultType: TResult.self) { 45 | rawResult, error in 46 | var resultValid = false 47 | if (rawResult as? TResult?) != nil { 48 | resultValid = true 49 | } 50 | queue.async() { 51 | completion?(resultValid ? rawResult as! TResult? : nil, error) 52 | } 53 | } 54 | } 55 | 56 | /// Perform asynchronous request. 57 | /// - Returns: Decodable on success. Nil on error, in which case `error` contains the details. 58 | internal func requestAsync(_ endpoint: String, _ parameters: [String: Encodable?]?..., queue: DispatchQueue = DispatchQueue.main, completion: ((_ result: TResult?, _ error: DataTaskError?) -> ())?) where TResult: Decodable { 59 | requestAsync(endpoint, mergeParameters(parameters), queue: queue, completion: completion) 60 | } 61 | 62 | /// Perform asynchronous request. 63 | /// - Returns: array of Decodable on success. Nil on error, in which case `error` contains the details. 64 | internal func requestAsync(_ endpoint: String, _ parameters: [String: Encodable?] = [:], queue: DispatchQueue = DispatchQueue.main, completion: ((_ result: [TResult]?, _ error: DataTaskError?) -> ())?) where TResult: Decodable { 65 | 66 | startDataTaskForEndpoint(endpoint, parameters: parameters, resultType: [TResult].self) { 67 | rawResult, error in 68 | var resultValid = false 69 | if (rawResult as? [TResult]?) != nil { 70 | resultValid = true 71 | } 72 | queue.async() { 73 | completion?(resultValid ? rawResult as! [TResult]? : nil, error) 74 | } 75 | } 76 | } 77 | 78 | /// Perform asynchronous request. 79 | /// - Returns: array of Decodable on success. Nil on error, in which case `error` contains the details. 80 | internal func requestAsync(_ endpoint: String, _ parameters: [String: Encodable?]?..., queue: DispatchQueue = DispatchQueue.main, completion: ((_ result: [TResult]?, _ error: DataTaskError?) -> ())?) where TResult: Decodable { 81 | return requestAsync(endpoint, mergeParameters(parameters), queue: queue, completion: completion) 82 | } 83 | 84 | /// Merge request parameters into a single dictionary. Nil parameters are ignored. Keys with nil values are also ignored. 85 | /// - Returns: merged parameters. 86 | private func mergeParameters(_ parameters: [ [String: Encodable?]? ]) -> [String: Encodable?] { 87 | var result = [String: Encodable?]() 88 | for p in parameters { 89 | guard let p = p else { continue } 90 | p.forEach { key, value in 91 | guard let value = value else { return } 92 | result.updateValue(value, forKey: key) 93 | } 94 | } 95 | return result 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Rapier/Sources/Rapier/Rapier.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Yaml 3 | 4 | open class Rapier { 5 | public let ymlFile: String 6 | 7 | private var types: [TypeInfo] = [] 8 | private var methods: [MethodInfo] = [] 9 | 10 | public init(ymlFile: String) { 11 | self.ymlFile = ymlFile 12 | } 13 | 14 | public func clear() { 15 | types = [] 16 | methods = [] 17 | } 18 | 19 | public func generate(generator: CodeGenerator) throws { 20 | try generator.start() 21 | 22 | try generator.beforeGeneratingTypes() 23 | try types.forEach { typeInfo in 24 | try generator.generateType(name: typeInfo.name, info: typeInfo) 25 | } 26 | try generator.afterGeneratingTypes() 27 | 28 | try generator.beforeGeneratingMethods() 29 | try methods.forEach { methodInfo in 30 | try generator.generateMethod(name: methodInfo.name, info: methodInfo) 31 | } 32 | try generator.afterGeneratingMethods() 33 | 34 | try generator.finish() 35 | } 36 | 37 | public func parseYml() throws { 38 | let data = try String(contentsOfFile: self.ymlFile) 39 | 40 | let documents = try Yaml.loadMultiple(data) 41 | 42 | try documents.forEach { document in 43 | guard let props = document.dictionary else { throw RapierError.expectedDictionary } 44 | if nil != props["format"] { 45 | try configure(props: props) 46 | } else if nil != props["type"] { 47 | try parseType(props: props) 48 | } else if nil != props["method"] { 49 | try parseMethod(props: props) 50 | } else { 51 | throw RapierError.unknownSectionType 52 | } 53 | } 54 | } 55 | 56 | private func configure(props: [Yaml: Yaml]) throws { 57 | 58 | } 59 | 60 | private func parseType(props: [Yaml: Yaml]) throws { 61 | guard let typeName = props["type"]?.string else { throw RapierError.expectedField(name: "type", parent: nil) } 62 | guard let fieldsArray = props["fields"]?.array else { 63 | types.append(TypeInfo(name: typeName, fields: [])) 64 | return 65 | } 66 | let fields = try parseFields(fieldsArray, parent: typeName) 67 | let typeInfo = TypeInfo(name: typeName, fields: fields) 68 | types.append(typeInfo) 69 | } 70 | 71 | private func parseMethod(props: [Yaml: Yaml]) throws { 72 | guard let methodName = props["method"]?.string else { throw RapierError.expectedField(name: "method", parent: nil) } 73 | guard let result = props["result"]?.string else { throw RapierError.missingReturn(parent: methodName)} 74 | 75 | let resultField = parseType(name: methodName, field: result) 76 | 77 | guard let fieldsArray = props["parameters"]?.array else { 78 | methods.append(MethodInfo(name: methodName, parameters: [], result: resultField)) 79 | return 80 | } 81 | let fields = try parseFields(fieldsArray, parent: methodName) 82 | let methodInfo = MethodInfo(name: methodName, parameters: fields, result: resultField) 83 | methods.append(methodInfo) 84 | } 85 | 86 | private func parseFields(_ fieldsArray: [Yaml], parent: String) throws -> [FieldInfo] { 87 | var fields: [FieldInfo] = [] 88 | try fieldsArray.forEach { item in 89 | guard let field = item.dictionary else { 90 | throw RapierError.expectedDictionary 91 | } 92 | guard let fieldName = field.first?.key.string else { throw RapierError.fieldNameIsNotString(parent: parent) } 93 | guard let fieldType = field.first?.value.string else { throw RapierError.fieldTypeIsNotString(parent: parent) } 94 | 95 | fields.append(parseType(name: fieldName, field: fieldType)) 96 | } 97 | return fields 98 | } 99 | 100 | private func parseType(name: String, field: String) -> FieldInfo { 101 | var fieldType = field 102 | let isOptional = fieldType.hasSuffix("?") 103 | if isOptional { 104 | fieldType = String(fieldType.dropLast()) 105 | } 106 | 107 | let isArray = fieldType.hasSuffix("[]") 108 | if isArray { 109 | fieldType = String(fieldType.dropLast(2)) 110 | } 111 | 112 | let isArrayOfArray = fieldType.hasSuffix("[]") 113 | if isArrayOfArray { 114 | fieldType = String(fieldType.dropLast(2)) 115 | } 116 | 117 | return FieldInfo(name: name, type: fieldType, isArray: isArray, isArrayOfArray: isArrayOfArray, isOptional: isOptional) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Tests/TelegramBotSDKTests/TelegramBotTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TelegramBotTests.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2018 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import XCTest 14 | @testable import TelegramBotSDK 15 | 16 | class TelegramBotTests: XCTestCase { 17 | var token: String! 18 | let connectionTimeout = 10.0 19 | let invalidUrl = "https://localhost:7777/doesNotExist" 20 | 21 | override func setUp() { 22 | super.setUp() 23 | 24 | // ------------------- 25 | // How to create a bot 26 | // ------------------- 27 | // * In Telegram, add BotFather. 28 | // /newbot 29 | // TestBot 30 | // apitest_bot 31 | // BotFather will return a token. 32 | // * In Xcode, click on project scheme, Edit Scheme -> Run. Add to Environment Variables: 33 | // TEST_BOT_TOKEN yourToken 34 | 35 | token = readToken(from: "TEST_BOT_TOKEN") 36 | } 37 | 38 | override func tearDown() { 39 | // Put teardown code here. This method is called after the invocation of each test method in the class. 40 | super.tearDown() 41 | } 42 | 43 | // func testExample() { 44 | // // This is an example of a functional test case. 45 | // // Use XCTAssert and related functions to verify your tests produce the correct results. 46 | // } 47 | 48 | // func testPerformanceExample() { 49 | // // This is an example of a performance test case. 50 | // self.measureBlock { 51 | // // Put the code you want to measure the time of here. 52 | // } 53 | // } 54 | 55 | func testGetMeSync() { 56 | let bot = TelegramBot(token: token, fetchBotInfo: false) 57 | let user = bot.getMeSync() 58 | let error = bot.lastError 59 | print("getMeSync: user: \(user.unwrapOptional), error: \(error.unwrapOptional)") 60 | } 61 | 62 | func testGetMeAsync() { 63 | let bot = TelegramBot(token: token, fetchBotInfo: false) 64 | 65 | let expectGetMe = expectation(description: "getMe") 66 | bot.getMeAsync { user, error in 67 | print("getMeAsync: user: \(user.unwrapOptional), error: \(error.unwrapOptional)") 68 | expectGetMe.fulfill() 69 | } 70 | waitForExpectations(timeout: connectionTimeout) { error in 71 | print("getMeAsync: \(error.unwrapOptional)") 72 | } 73 | } 74 | 75 | func testGetUpdatesSync() { 76 | let bot = TelegramBot(token: token, fetchBotInfo: false) 77 | let updates = bot.getUpdatesSync() 78 | let error = bot.lastError 79 | print("getUpdatesSync: \(updates.unwrapOptional), error: \(error.unwrapOptional)") 80 | } 81 | 82 | func testGetUpdatesAsync() { 83 | let bot = TelegramBot(token: token, fetchBotInfo: false) 84 | 85 | let expectGetUpdates = expectation(description: "getUpdates") 86 | bot.getUpdatesAsync { updates, error in 87 | print("getUpdatesAsync: updates: \(updates.unwrapOptional), error: \(error.unwrapOptional)") 88 | expectGetUpdates.fulfill() 89 | } 90 | waitForExpectations(timeout: connectionTimeout) { error in 91 | print("getUpdatesAsync: \(error.unwrapOptional)") 92 | } 93 | } 94 | 95 | func testBadToken() { 96 | let badBot = TelegramBot(token: "badToken", fetchBotInfo: false) 97 | let user = badBot.getMeSync() 98 | let error = badBot.lastError 99 | print("getMeSync: user: \(user.unwrapOptional), error: \(error.unwrapOptional)") 100 | } 101 | 102 | // func testErrorHandlingAsync() { 103 | // let bot = TelegramBot(token: token, fetchBotInfo: false) 104 | // bot.url = invalidUrl 105 | // 106 | // let expectGetMe = expectation(description: "getMe") 107 | // 108 | // // Comment out custom errorHandler to see 109 | // // autoreconnects in action (but the test 110 | // // will fail) 111 | //#if true 112 | // bot.errorHandler = { task, taskData, error in 113 | // print("getMe: errorHandler: task: \(task), taskData: \(taskData), error: \(error)") 114 | // expectGetMe.fulfill() 115 | // } 116 | //#endif 117 | // 118 | // bot.getMeAsync { user in 119 | // XCTFail("Expected error handler to run") 120 | // } 121 | // 122 | // waitForExpectations(timeout: connectionTimeout) { error in 123 | // print("getMeAsync: \(error)") 124 | // } 125 | // 126 | // } 127 | 128 | static var allTests : [(String, (TelegramBotTests) -> () throws -> Void)] { 129 | return [ 130 | //("testExample", testExample), 131 | ] 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Examples/word-reverse-bot/Sources/word-reverse-bot/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // 4 | // This file containing the example code is in public domain. 5 | // Feel free to copy-paste it and edit it in any way you like. 6 | // 7 | 8 | import Foundation 9 | import TelegramBotSDK 10 | 11 | let token = readToken(from: "WORD_REVERSE_BOT_TOKEN") 12 | 13 | class Controller { 14 | let bot: TelegramBot 15 | var startedInChatId = Set() 16 | 17 | func started(in chatId: Int64) -> Bool { 18 | return startedInChatId.contains(chatId) 19 | } 20 | 21 | init(bot: TelegramBot) { 22 | self.bot = bot 23 | } 24 | 25 | func start(context: Context) -> Bool { 26 | guard let chatId = context.chatId else { return false } 27 | 28 | guard !started(in: chatId) else { 29 | context.respondAsync("@\(bot.username) already started.") 30 | return true 31 | } 32 | startedInChatId.insert(chatId) 33 | 34 | var startText: String 35 | if !context.privateChat { 36 | startText = "@\(bot.username) started. Use '/reverse some text' to reverse the text.\n" 37 | } else { 38 | startText = "@\(bot.username) started. Please type some text to reverse.\n" 39 | } 40 | startText += "To stop, type /stop" 41 | 42 | context.respondAsync(startText) 43 | return true 44 | } 45 | 46 | func stop(context: Context) -> Bool { 47 | guard let chatId = context.chatId else { return false } 48 | 49 | guard started(in: chatId) else { 50 | context.respondAsync("@\(bot.username) already stopped.") 51 | return true 52 | } 53 | startedInChatId.remove(chatId) 54 | 55 | context.respondSync("@\(bot.username) stopped. To restart, type /start") 56 | return true 57 | } 58 | 59 | func help(context: Context) -> Bool { 60 | guard let from = context.message?.from else { return false } 61 | let helpText = "What can this bot do?\n" + 62 | "\n" + 63 | "This is a sample bot which reverses sentences or words. " + 64 | "If you want to invite friends, simply open the bot's profile " + 65 | "and use the 'Add to group' button to invite them.\n" + 66 | "\n" + 67 | "Send /start to begin reversing sentences.\n" + 68 | "Tell the bot to /stop when you're done.\n" + 69 | "\n" + 70 | "In private chat simply type some text and it will be reversed.\n" + 71 | "In group chats use this command:\n" + 72 | "/reverse Sentence\n" + 73 | "\n" + 74 | "To reverse words, use /word_reverse word1 word2 word3..." 75 | 76 | context.respondPrivatelyAsync(helpText, 77 | groupText: "\(from.firstName), please find usage instructions in a personal message.") 78 | return true 79 | } 80 | 81 | func settings(context: Context) -> Bool { 82 | guard let from = context.message?.from else { return false } 83 | let settingsText = "Settings\n" + 84 | "\n" + 85 | "No settings are available for this bot." 86 | 87 | context.respondPrivatelyAsync(settingsText, 88 | groupText: "\(from.firstName), please find a list of settings in a personal message.") 89 | return true 90 | } 91 | 92 | func partialMatchHandler(context: Context) -> Bool { 93 | context.respondAsync("❗ Part of your input was ignored: \(context.args.scanRestOfString())") 94 | return true 95 | } 96 | 97 | func reverseText(context: Context) -> Bool { 98 | guard let chatId = context.chatId else { return false } 99 | guard started(in: chatId) else { return false } 100 | 101 | let text = context.args.scanRestOfString() 102 | 103 | context.respondAsync(String(text.reversed())) 104 | return true 105 | } 106 | 107 | func reverseWords(context: Context) -> Bool { 108 | guard let chatId = context.chatId else { return false } 109 | guard started(in: chatId) else { return false } 110 | 111 | let words = context.args.scanWords() 112 | switch words.isEmpty { 113 | case true: context.respondAsync("Please specify some words to reverse.") 114 | case false: context.respondAsync(words.reversed().joined(separator: " ")) 115 | } 116 | return true 117 | } 118 | } 119 | 120 | let bot = TelegramBot(token: token) 121 | let controller = Controller(bot: bot) 122 | 123 | let router = Router(bot: bot) 124 | router["start"] = controller.start 125 | router["stop"] = controller.stop 126 | router["help"] = controller.help 127 | router["settings"] = controller.settings 128 | router["reverse", .slashRequired] = controller.reverseText 129 | router["word_reverse"] = controller.reverseWords 130 | // Default handler 131 | router.unmatched = controller.reverseText 132 | // If command has unprocessed arguments, report them: 133 | router.partialMatch = controller.partialMatchHandler 134 | 135 | print("Ready to accept commands") 136 | while let update = bot.nextUpdateSync() { 137 | print("--- update: \(update.debugDescription)") 138 | 139 | try router.process(update: update) 140 | } 141 | print("Server stopped due to error: \(bot.lastError.unwrapOptional)") 142 | exit(1) 143 | -------------------------------------------------------------------------------- /Tests/TelegramBotSDKTests/RequestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestTests.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2018 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import XCTest 14 | import Foundation 15 | @testable import TelegramBotSDK 16 | 17 | class RequestTests: XCTestCase { 18 | var token: String! 19 | var bot: TelegramBot! 20 | var chatId: Int64! 21 | var messageId: Int! 22 | 23 | override func setUp() { 24 | super.setUp() 25 | continueAfterFailure = false 26 | 27 | // ------------------- 28 | // How to create a bot 29 | // ------------------- 30 | // * In Telegram, add BotFather. 31 | // /newbot 32 | // TestBot 33 | // apitest_bot 34 | // BotFather will return a token. 35 | // * In Xcode, click on project scheme, Edit Scheme -> Run. Add to Environment Variables: 36 | // TEST_BOT_TOKEN yourToken 37 | 38 | token = readToken(from: "TEST_BOT_TOKEN") 39 | chatId = readConfigurationValue("TEST_CHAT_ID") 40 | messageId = readConfigurationValue("TEST_MESSAGE_ID") 41 | bot = TelegramBot(token: token, fetchBotInfo: false) 42 | 43 | if chatId == nil { 44 | XCTFail("Please set TEST_CHAT_ID") 45 | } 46 | 47 | bot.defaultParameters["sendMessage"] = ["disable_notification": true] 48 | } 49 | 50 | override func tearDown() { 51 | // Put teardown code here. This method is called after the invocation of each test method in the class. 52 | super.tearDown() 53 | } 54 | 55 | // 56 | // getMe 57 | // 58 | 59 | func testGetMe() { 60 | check( bot.getMeSync() ) 61 | } 62 | 63 | // 64 | // sendMessage 65 | // 66 | 67 | func testSendMessage() { 68 | let response = bot.sendMessageSync(chatId: .chat(chatId), text: "testSendMessage1: this is a simple message") 69 | check(response) 70 | 71 | let messageId = response!.messageId 72 | check( bot.sendMessageSync(chatId: .chat(chatId), text: "testSendMessage2: a reply to previous message", ["reply_to_message_id": messageId]) ) 73 | 74 | check( bot.sendMessageSync(chatId: .chat(chatId), text: "testSendMessage3: url without preview: http://google.com", ["disable_web_page_preview": true]) ) 75 | 76 | check( bot.sendMessageSync(chatId: .chat(chatId), text: "testSendMessage4: markdown: *bold* _italic_ [link](http://google.com)", ["parse_mode": "Markdown"]) ) 77 | 78 | check( bot.sendMessageSync(chatId: .chat(chatId), text: "testSendMessage5: html: bold italic\nvoid main() {\n return 0;\n}", parseMode: .html) ) 79 | } 80 | 81 | func testShowKeyboardWithText() { 82 | let markup = ReplyKeyboardMarkup(keyboard: [ 83 | [KeyboardButton(text: "Button 1"), KeyboardButton(text: "Button 2")], 84 | [KeyboardButton(text: "Button 3")] 85 | ]) 86 | check( bot.sendMessageSync(chatId: .chat(chatId), text: "Here is a keyboard", ["reply_markup": markup]) ) 87 | } 88 | 89 | func testShowKeyboardWithButtons() { 90 | let markup = ReplyKeyboardMarkup(keyboard: []) 91 | 92 | let button1 = KeyboardButton(text: "Button 1") 93 | let button2 = KeyboardButton(text: "Button 2") 94 | let button3 = KeyboardButton(text: "Share Contact", requestContact: true) 95 | let button4 = KeyboardButton(text: "Share Location", requestLocation: true) 96 | 97 | markup.keyboard = [ 98 | [ button1, button2 ], 99 | [ button3, button4 ] 100 | ] 101 | check( bot.sendMessageSync(chatId: .chat(chatId), text: "Here is a keyboard", ["reply_markup": markup]) ) 102 | } 103 | 104 | func testHideKeyboard() { 105 | let markup = ReplyKeyboardRemove(removeKeyboard: true) 106 | check( bot.sendMessageSync(chatId: .chat(chatId), text: "Removing the keyboard", ["reply_markup": markup]) ) 107 | } 108 | 109 | func testForceReply() { 110 | let markup = ForceReply(forceReply: true) 111 | check( bot.sendMessageSync(chatId: .chat(chatId), text: "Force reply", ["reply_markup": markup]) ) 112 | } 113 | 114 | // 115 | // forwardMessage 116 | // 117 | 118 | func testForwardMessage() { 119 | check( bot.forwardMessageSync(chatId: .chat(chatId), fromChatId: .chat(chatId), messageId: messageId) ) 120 | } 121 | 122 | // 123 | // sendLocation 124 | // 125 | 126 | func testSendLocation() { 127 | check( bot.sendChatActionSync(chatId: .chat(chatId), action: .findLocation) ) 128 | check( bot.sendLocationSync(chatId: .chat(chatId), latitude: 50.4501, longitude: 30.5234) ) 129 | } 130 | 131 | // 132 | // sendPhoto 133 | // 134 | 135 | func testSendPhoto() { 136 | let filename = "/tmp/test.jpg" 137 | let url = URL(fileURLWithPath: filename) 138 | let imageData: Data 139 | do { 140 | imageData = try Data(contentsOf: url) 141 | } catch { 142 | XCTFail("\(error)") 143 | return 144 | } 145 | let inputFile = InputFile(filename: "test.jpg", data: imageData) 146 | bot.sendPhotoSync(chatId: .chat(chatId), photo: .inputFile(inputFile)) 147 | } 148 | 149 | // Helper functions 150 | 151 | func check(_ result: T?) where T: Codable { 152 | XCTAssert(result != nil) 153 | } 154 | 155 | static var allTests : [(String, (RequestTests) -> () throws -> Void)] { 156 | return [ 157 | ("testGetMe", testGetMe), 158 | ("testSendMessage", testSendMessage), 159 | ("testShowKeyboardWithText", testShowKeyboardWithText), 160 | ("testShowKeyboardWithButtons", testShowKeyboardWithButtons), 161 | ("testHideKeyboard", testHideKeyboard), 162 | ("testForceReply", testForceReply), 163 | ("testForwardMessage", testForwardMessage), 164 | ("testSendLocation", testSendLocation), 165 | ("testSendPhoto", testSendPhoto), 166 | //("testExample", testExample), 167 | ] 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Tests/TelegramBotSDKTests/UrlencodeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UrlencodeTests.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2018 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import XCTest 14 | @testable import TelegramBotSDK 15 | 16 | class UrlencodeTests: XCTestCase { 17 | 18 | override func setUp() { 19 | super.setUp() 20 | } 21 | 22 | override func tearDown() { 23 | super.tearDown() 24 | } 25 | 26 | func testQueryUrlencode() { 27 | let v1 = "value 1" 28 | let uv1 = v1.urlQueryEncode() 29 | XCTAssert(uv1 == "value%201") 30 | 31 | let v2 = "валюе\t2" 32 | let uv2 = v2.urlQueryEncode() 33 | XCTAssert(uv2 == "%D0%B2%D0%B0%D0%BB%D1%8E%D0%B5%092") 34 | 35 | let v3 = "!@#$%^&*()-=+_" 36 | let uv3 = v3.urlQueryEncode() 37 | XCTAssert(uv3 == "%21%40%23%24%25%5E%26%2A%28%29-%3D%2B_") 38 | } 39 | 40 | func testFormUrlencodeSimple() { 41 | let parameters = [ 42 | "key1": "value1", 43 | "key2": "value2", 44 | "key3": "value3" 45 | ] 46 | let encoded = HTTPUtils.formUrlencode(parameters) 47 | XCTAssert(encoded.contains("key1=value1") && encoded.contains("key2=value2") && encoded.contains("key3=value3")) 48 | } 49 | 50 | func testFormUrlencodePercentEscaping() { 51 | let parameters = [ 52 | "key1": "value 1", 53 | "key2": "валюе\t2", 54 | "key3": "!@#$%^&*()-=+_" 55 | ] 56 | let encoded = HTTPUtils.formUrlencode(parameters) 57 | XCTAssert(encoded.contains("key1=value+1") && encoded.contains("key2=%D0%B2%D0%B0%D0%BB%D1%8E%D0%B5%092") && encoded.contains("key3=%21%40%23%24%25%5E%26*%28%29-%3D%2B_")) 58 | 59 | } 60 | 61 | func testFormUrlencodeMixed() { 62 | let value3: Int? = nil 63 | let parameters: [String: Encodable?] = [ 64 | "key1": "value1", 65 | "key2": 123, 66 | "key3": value3 67 | ] 68 | guard let encoded = HTTPUtils.formUrlencode(parameters) else { 69 | XCTFail() 70 | return 71 | } 72 | XCTAssert(encoded.contains("key1=value1") && encoded.contains("key2=123") && !encoded.contains("key3")) 73 | } 74 | 75 | func testFormUrlencodeNilValue() { 76 | let parameters: [String: Encodable?] = [ 77 | "key": nil 78 | ] 79 | let encoded = HTTPUtils.formUrlencode(parameters) 80 | XCTAssert(encoded == "") 81 | } 82 | 83 | func testFormUrlencodeOptionalString() { 84 | let value: String? = "value" 85 | let parameters: [String: Encodable?] = [ 86 | "key": value 87 | ] 88 | let encoded = HTTPUtils.formUrlencode(parameters) 89 | XCTAssert(encoded == "key=value") 90 | } 91 | 92 | func testFormUrlencodeAny() { 93 | let parameters: [String: Encodable?] = [ 94 | "key": "value" 95 | ] 96 | let encoded = HTTPUtils.formUrlencode(parameters) 97 | XCTAssert(encoded == "key=value") 98 | } 99 | 100 | func testFormUrlencodeOptionalAsAny() { 101 | let value: String? = "value" 102 | let parameters: [String: Encodable?] = [ 103 | "key": value 104 | ] 105 | let encoded = HTTPUtils.formUrlencode(parameters) 106 | XCTAssert(encoded == "key=value") 107 | } 108 | 109 | func testFormUrlencodeNilAsAny() { 110 | let value: String? = nil 111 | let parameters: [String: Encodable?] = [ 112 | "key": value 113 | ] 114 | let encoded = HTTPUtils.formUrlencode(parameters) 115 | XCTAssert(encoded == "") 116 | } 117 | 118 | func testFormUrlencodeTypes() { 119 | let parameters: [String: Encodable?] = [ 120 | "key1": 123, 121 | "key2": 123.456, 122 | "key3": true, 123 | "key4": false, 124 | "key5": "text" 125 | ] 126 | guard let encoded = HTTPUtils.formUrlencode(parameters) else { 127 | XCTFail() 128 | return 129 | } 130 | XCTAssert(encoded.contains("key1=123") && encoded.contains("key2=123.456") && encoded.contains("key3=true") && !encoded.contains("key4") && encoded.contains("key5=text")) 131 | } 132 | 133 | func testFormUrlencodeReplyMarkup() { 134 | let keyboardMarkup = ReplyKeyboardMarkup(keyboard: [ 135 | [ KeyboardButton(text: "A"), KeyboardButton(text: "B"), KeyboardButton(text: "C") ], 136 | [ KeyboardButton(text: "D"), KeyboardButton(text: "E") ] 137 | ]) 138 | let parameters: [String: Encodable?] = [ 139 | "key": keyboardMarkup 140 | ] 141 | let encoded = HTTPUtils.formUrlencode(parameters) 142 | print(encoded) 143 | // key={"keyboard":[["A","B","C"],["D","E"]]} 144 | XCTAssert(encoded == "key=%7B%22keyboard%22%3A%5B%5B%7B%22text%22%3A%22A%22%7D%2C%7B%22text%22%3A%22B%22%7D%2C%7B%22text%22%3A%22C%22%7D%5D%2C%5B%7B%22text%22%3A%22D%22%7D%2C%7B%22text%22%3A%22E%22%7D%5D%5D%7D") 145 | } 146 | 147 | static var allTests : [(String, (UrlencodeTests) -> () throws -> Void)] { 148 | return [ 149 | ("testQueryUrlencode", testQueryUrlencode), 150 | ("testFormUrlencodeSimple", testFormUrlencodeSimple), 151 | ("testFormUrlencodePercentEscaping", testFormUrlencodePercentEscaping), 152 | ("testFormUrlencodeMixed", testFormUrlencodeMixed), 153 | ("testFormUrlencodeNilValue", testFormUrlencodeNilValue), 154 | ("testFormUrlencodeOptionalString", testFormUrlencodeOptionalString), 155 | ("testFormUrlencodeAny", testFormUrlencodeAny), 156 | ("testFormUrlencodeOptionalAsAny", testFormUrlencodeOptionalAsAny), 157 | ("testFormUrlencodeNilAsAny", testFormUrlencodeNilAsAny), 158 | ("testFormUrlencodeTypes", testFormUrlencodeTypes), 159 | ("testFormUrlencodeReplyMarkup", testFormUrlencodeReplyMarkup), 160 | ] 161 | } 162 | } 163 | 164 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Extensions/Scanner+Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Scanner { 4 | func skipping(_ characters: CharacterSet?, closure: () throws->()) rethrows { 5 | let previous = charactersToBeSkipped 6 | defer { charactersToBeSkipped = previous } 7 | charactersToBeSkipped = characters 8 | try closure() 9 | } 10 | 11 | @discardableResult 12 | func skipInt() -> Bool { 13 | return scanInt() != nil 14 | } 15 | 16 | @discardableResult 17 | func skipInt32() -> Bool { 18 | #if os(OSX) 19 | return scanInt32(nil) 20 | #else 21 | return scanInt() != nil 22 | #endif 23 | } 24 | 25 | @discardableResult 26 | func skipInt64() -> Bool { 27 | return scanInt64() != nil 28 | } 29 | 30 | @discardableResult 31 | func skipUInt64() -> Bool { 32 | return scanUInt64() != nil 33 | } 34 | 35 | @discardableResult 36 | func skipFloat() -> Bool { 37 | return scanFloat() != nil 38 | } 39 | 40 | @discardableResult 41 | func skipDouble() -> Bool { 42 | return scanDouble() != nil 43 | } 44 | 45 | @discardableResult 46 | func skipHexUInt32() -> Bool { 47 | return scanHexUInt32() != nil 48 | } 49 | 50 | @discardableResult 51 | func skipHexUInt64() -> Bool { 52 | return scanHexUInt64() != nil 53 | } 54 | 55 | @discardableResult 56 | func skipHexFloat() -> Bool { 57 | return scanHexFloat() != nil 58 | } 59 | 60 | @discardableResult 61 | func skipHexDouble() -> Bool { 62 | return scanHexDouble() != nil 63 | } 64 | 65 | @discardableResult 66 | func skipString(_ string: String) -> Bool { 67 | #if true 68 | return scanString(string) != nil 69 | #else 70 | let utf16 = self.string.utf16 71 | let startOffset = skippingCharacters(startingAt: scanLocation, in: utf16) 72 | let toSkip = string.utf16 73 | let toSkipCount = toSkip.count 74 | let fromIndex = utf16.index(utf16.startIndex, offsetBy: startOffset) 75 | if let toIndex = utf16.index(fromIndex, offsetBy: toSkipCount, limitedBy: utf16.endIndex), 76 | utf16[fromIndex.. Bool { 86 | return scanCharacters(from: from) != nil 87 | } 88 | 89 | @discardableResult 90 | func skipUpTo(_ string: String) -> Bool { 91 | return scanUpTo(string) != nil 92 | } 93 | 94 | @discardableResult 95 | func skipUpToCharacters(from set: CharacterSet) -> Bool { 96 | return scanUpToCharacters(from: set) != nil 97 | } 98 | 99 | func peekUtf16CodeUnit() -> UTF16.CodeUnit? { 100 | let originalScanLocation = scanLocation 101 | defer { scanLocation = originalScanLocation } 102 | 103 | let originalCharactersToBeSkipped = charactersToBeSkipped 104 | defer { charactersToBeSkipped = originalCharactersToBeSkipped } 105 | 106 | if let characters = charactersToBeSkipped { 107 | charactersToBeSkipped = nil 108 | let _ = scanCharacters(from: characters) 109 | } 110 | 111 | guard scanLocation < string.utf16.count else { return nil } 112 | let index = string.utf16.index(string.utf16.startIndex, offsetBy: scanLocation) 113 | return string.utf16[index] 114 | } 115 | 116 | var scanLocationInCharacters: Int { 117 | let utf16 = string.utf16 118 | guard let to16 = utf16.index(utf16.startIndex, offsetBy: scanLocation, limitedBy: utf16.endIndex), 119 | let to = String.Index(to16, within: string) else { 120 | return 0 121 | } 122 | return string.distance(from: string.startIndex, to: to) 123 | } 124 | 125 | private var currentCharacterIndex: String.Index? { 126 | let utf16 = string.utf16 127 | guard let to16 = utf16.index(utf16.startIndex, offsetBy: scanLocation, limitedBy: utf16.endIndex), 128 | let to = String.Index(to16, within: string) else { 129 | return nil 130 | } 131 | // to is a String.CharacterView.Index 132 | return to 133 | } 134 | 135 | var parsedText: Substring { 136 | guard let index = currentCharacterIndex else { return "" } 137 | return string[.. targetLine { 152 | break 153 | } 154 | 155 | if character == "\n" || character == "\r\n" { 156 | currentLine += 1 157 | continue 158 | } 159 | 160 | if currentLine == targetLine { 161 | line.append(character) 162 | } 163 | } 164 | return line 165 | } 166 | 167 | /// Very slow, do not in use in loops 168 | func line() -> Int { 169 | var newLinesCount = 0 170 | parsedText.forEach { 171 | if $0 == "\n" || $0 == "\r\n" { 172 | newLinesCount += 1 173 | } 174 | } 175 | return 1 + newLinesCount 176 | } 177 | 178 | /// Very slow, do not in use in loops 179 | func column() -> Int { 180 | let text = parsedText 181 | if let range = text.range(of: "\n", options: .backwards) { 182 | return text.distance(from: range.upperBound, to: text.endIndex) + 1 183 | } 184 | return parsedText.count + 1 185 | } 186 | 187 | #if fålse 188 | private func skippingCharacters(startingAt: Int, in utf16: String.UTF16View) -> Int { 189 | guard let charactersToBeSkipped = charactersToBeSkipped else { return startingAt } 190 | let fromIndex = utf16.index(utf16.startIndex, offsetBy: startingAt) 191 | var newLocation = startingAt 192 | for c in utf16[fromIndex...] { 193 | guard let scalar = UnicodeScalar(c) else { break } 194 | guard charactersToBeSkipped.contains(scalar) else { break } 195 | newLocation += 1 196 | } 197 | return newLocation 198 | } 199 | #endif 200 | } 201 | 202 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Types/InlineQueryResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InlineQueryResult.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | public enum InlineQueryResult: Codable { 16 | case cachedAudio(InlineQueryResultCachedAudio) 17 | case cachedDocument(InlineQueryResultCachedDocument) 18 | case cachedGif(InlineQueryResultCachedGif) 19 | case cachedMpeg4Gif(InlineQueryResultCachedMpeg4Gif) 20 | case cachedPhoto(InlineQueryResultCachedPhoto) 21 | case cachedSticker(InlineQueryResultCachedSticker) 22 | case cachedVideo(InlineQueryResultCachedVideo) 23 | case cachedVoice(InlineQueryResultCachedVoice) 24 | case article(InlineQueryResultArticle) 25 | case audio(InlineQueryResultAudio) 26 | case contact(InlineQueryResultContact) 27 | case document(InlineQueryResultDocument) 28 | case gif(InlineQueryResultGif) 29 | case location(InlineQueryResultLocation) 30 | case mpeg4Gif(InlineQueryResultMpeg4Gif) 31 | case photo(InlineQueryResultPhoto) 32 | case venue(InlineQueryResultVenue) 33 | case video(InlineQueryResultVideo) 34 | case voice(InlineQueryResultVoice) 35 | case unknown 36 | 37 | public init(from decoder: Decoder) throws { 38 | if let cachedAudio = try? decoder.singleValueContainer().decode(InlineQueryResultCachedAudio.self) { 39 | self = .cachedAudio(cachedAudio) 40 | return 41 | } 42 | if let cachedDocument = try? decoder.singleValueContainer().decode(InlineQueryResultCachedDocument.self) { 43 | self = .cachedDocument(cachedDocument) 44 | return 45 | } 46 | if let cachedGif = try? decoder.singleValueContainer().decode(InlineQueryResultCachedGif.self) { 47 | self = .cachedGif(cachedGif) 48 | return 49 | } 50 | if let cachedMpeg4Gif = try? decoder.singleValueContainer().decode(InlineQueryResultCachedMpeg4Gif.self) { 51 | self = .cachedMpeg4Gif(cachedMpeg4Gif) 52 | return 53 | } 54 | if let cachedPhoto = try? decoder.singleValueContainer().decode(InlineQueryResultCachedPhoto.self) { 55 | self = .cachedPhoto(cachedPhoto) 56 | return 57 | } 58 | 59 | if let cachedSticker = try? decoder.singleValueContainer().decode(InlineQueryResultCachedSticker.self) { 60 | self = .cachedSticker(cachedSticker) 61 | return 62 | } 63 | 64 | if let cachedVideo = try? decoder.singleValueContainer().decode(InlineQueryResultCachedVideo.self) { 65 | self = .cachedVideo(cachedVideo) 66 | return 67 | } 68 | 69 | if let cachedVoice = try? decoder.singleValueContainer().decode(InlineQueryResultCachedVoice.self) { 70 | self = .cachedVoice(cachedVoice) 71 | return 72 | } 73 | 74 | if let article = try? decoder.singleValueContainer().decode(InlineQueryResultArticle.self) { 75 | self = .article(article) 76 | return 77 | } 78 | 79 | if let audio = try? decoder.singleValueContainer().decode(InlineQueryResultAudio.self) { 80 | self = .audio(audio) 81 | return 82 | } 83 | 84 | if let contact = try? decoder.singleValueContainer().decode(InlineQueryResultContact.self) { 85 | self = .contact(contact) 86 | return 87 | } 88 | 89 | if let document = try? decoder.singleValueContainer().decode(InlineQueryResultDocument.self) { 90 | self = .document(document) 91 | return 92 | } 93 | 94 | if let gif = try? decoder.singleValueContainer().decode(InlineQueryResultGif.self) { 95 | self = .gif(gif) 96 | return 97 | } 98 | 99 | if let location = try? decoder.singleValueContainer().decode(InlineQueryResultLocation.self) { 100 | self = .location(location) 101 | return 102 | } 103 | 104 | if let mpeg4Gif = try? decoder.singleValueContainer().decode(InlineQueryResultMpeg4Gif.self) { 105 | self = .mpeg4Gif(mpeg4Gif) 106 | return 107 | } 108 | 109 | if let photo = try? decoder.singleValueContainer().decode(InlineQueryResultPhoto.self) { 110 | self = .photo(photo) 111 | return 112 | } 113 | 114 | if let venue = try? decoder.singleValueContainer().decode(InlineQueryResultVenue.self) { 115 | self = .venue(venue) 116 | return 117 | } 118 | 119 | if let video = try? decoder.singleValueContainer().decode(InlineQueryResultVideo.self) { 120 | self = .video(video) 121 | return 122 | } 123 | 124 | if let voice = try? decoder.singleValueContainer().decode(InlineQueryResultVoice.self) { 125 | self = .voice(voice) 126 | return 127 | } 128 | 129 | self = .unknown 130 | } 131 | 132 | public func encode(to encoder: Encoder) throws { 133 | var container = encoder.singleValueContainer() 134 | switch self { 135 | case let .cachedAudio(cachedAudio): 136 | try container.encode(cachedAudio) 137 | case let .cachedDocument(cachedDocument): 138 | try container.encode(cachedDocument) 139 | case let .cachedGif(cachedGif): 140 | try container.encode(cachedGif) 141 | case let .cachedMpeg4Gif(cachedMpeg4Gif): 142 | try container.encode(cachedMpeg4Gif) 143 | case let .cachedPhoto(cachedPhoto): 144 | try container.encode(cachedPhoto) 145 | case let .cachedSticker(cachedSticker): 146 | try container.encode(cachedSticker) 147 | case let .cachedVideo(cachedVideo): 148 | try container.encode(cachedVideo) 149 | case let .cachedVoice(cachedVoice): 150 | try container.encode(cachedVoice) 151 | case let .article(article): 152 | try container.encode(article) 153 | case let .audio(audio): 154 | try container.encode(audio) 155 | case let .contact(contact): 156 | try container.encode(contact) 157 | case let .document(document): 158 | try container.encode(document) 159 | case let .gif(gif): 160 | try container.encode(gif) 161 | case let .location(location): 162 | try container.encode(location) 163 | case let .mpeg4Gif(mpeg4Gif): 164 | try container.encode(mpeg4Gif) 165 | case let .photo(photo): 166 | try container.encode(photo) 167 | case let .venue(venue): 168 | try container.encode(venue) 169 | case let .video(video): 170 | try container.encode(video) 171 | case let .voice(voice): 172 | try container.encode(voice) 173 | default: 174 | fatalError("Unknown should not be used") 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /Examples/shopster-bot/Sources/shopster-bot/Controllers/MainController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Telegram Bot SDK for Swift (unofficial). 3 | // 4 | // This file containing the example code is in public domain. 5 | // Feel free to copy-paste it and edit it in any way you like. 6 | // 7 | 8 | import Foundation 9 | import TelegramBotSDK 10 | 11 | class MainController { 12 | typealias T = MainController 13 | 14 | init() { 15 | routers["main"] = Router(bot: bot) { router in 16 | router[Commands.start] = onStart 17 | router[Commands.stop] = onStop 18 | router[Commands.help] = onHelp 19 | router[Commands.add] = onAdd 20 | router[Commands.delete] = onDelete 21 | router[Commands.list] = onList 22 | router[Commands.support] = onSupport 23 | router[.newChatMembers] = onNewChatMember 24 | router[.callback_query(data: nil)] = onCallbackQuery 25 | } 26 | } 27 | 28 | func onStart(context: Context) throws -> Bool { 29 | try showMainMenu(context: context, text: "Please choose an option.") 30 | return true 31 | } 32 | 33 | func onStop(context: Context) -> Bool { 34 | let replyTo = context.privateChat ? nil : context.message?.messageId 35 | 36 | var markup = ReplyKeyboardRemove() 37 | markup.selective = replyTo != nil 38 | context.respondAsync("Stopping.", 39 | replyToMessageId: replyTo, 40 | replyMarkup: markup) 41 | return true 42 | } 43 | 44 | 45 | func onHelp(context: Context) throws -> Bool { 46 | let text = "Usage:\n" + 47 | "/add name - add a new item to list\n" + 48 | "/list - list items\n" + 49 | "/delete - delete purchased items from list\n" + 50 | "/support - join the support group" 51 | try showMainMenu(context: context, text: text) 52 | return true 53 | } 54 | 55 | func onAdd(context: Context) throws -> Bool { 56 | guard let chatId = context.chatId else { return false } 57 | let name = context.args.scanRestOfString() 58 | if name.isEmpty { 59 | addController.showHelp(context: context) 60 | context.session.routerName = "add" 61 | try context.session.save() 62 | } else { 63 | try Item.add(name: name, chatId: chatId) 64 | context.respondAsync("Added: \(name)") 65 | } 66 | return true 67 | } 68 | 69 | func onDelete(context: Context) throws -> Bool { 70 | deleteController.showConfirmationKeyboard(context: context, text: "Delete purchased items? /confirm_deletion or /cancel") 71 | context.session.routerName = "delete" 72 | try context.session.save() 73 | return true 74 | } 75 | 76 | func onList(context: Context) -> Bool { 77 | guard let markup = itemListInlineKeyboardMarkup(context: context) else { return false } 78 | context.respondAsync("Item List:", 79 | replyMarkup: markup) 80 | return true 81 | } 82 | 83 | func onSupport(context: Context) -> Bool { 84 | // Please update support group name to point to your group! 85 | // Don't send people to Zabiyaka Support group. 86 | // Delete this guard condition when this is done. 87 | guard bot.username.lowercased().hasPrefix("shopster") else { 88 | context.respondAsync("Invalid support URL.") 89 | return true 90 | } 91 | 92 | var button = InlineKeyboardButton() 93 | button.text = "Join Zabiyaka Support" 94 | button.url = "https://telegram.me/zabiyaka_support" 95 | 96 | var markup = InlineKeyboardMarkup() 97 | let keyboard = [[button]] 98 | markup.inlineKeyboard = keyboard 99 | 100 | context.respondAsync("Please click the button below to join *Zabiyaka Support* group.", parseMode: "Markdown", replyMarkup: markup) 101 | 102 | return true 103 | } 104 | 105 | func onNewChatMember(context: Context) -> Bool { 106 | guard let newChatMembers = context.message?.newChatMembers, 107 | newChatMembers.first?.id == bot.user.id else { return false } 108 | 109 | context.respondAsync("Hi All. I'll maintain your shared shopping list. Type /start to start working with me.") 110 | return true 111 | } 112 | 113 | func onCallbackQuery(context: Context) throws -> Bool { 114 | guard let callbackQuery = context.update.callbackQuery else { return false } 115 | guard let chatId = callbackQuery.message?.chat.id else { return false } 116 | guard let messageId = callbackQuery.message?.messageId else { return false } 117 | guard let data = callbackQuery.data else { return false } 118 | let scanner = Scanner(string: data) 119 | 120 | // "toggle 1234567" 121 | guard scanner.skipString("toggle") else { return false } 122 | guard let itemId = scanner.scanInt64() else { return false } 123 | 124 | guard let item = try Item.item(itemId: itemId, from: chatId) else { 125 | context.respondAsync("This item no longer exists.") 126 | return true 127 | } 128 | item.purchased = !item.purchased 129 | try item.save() 130 | 131 | if let markup = itemListInlineKeyboardMarkup(context: context) { 132 | bot.editMessageReplyMarkupAsync(chatId: chatId, messageId: messageId, replyMarkup: markup) 133 | } 134 | return true 135 | } 136 | 137 | func showMainMenu(context: Context, text: String) throws { 138 | // Use replies in group chats, otherwise bot won't be able to see the text typed by user. 139 | // In private chats don't clutter the chat with quoted replies. 140 | let replyTo = context.privateChat ? nil : context.message?.messageId 141 | 142 | var markup = ReplyKeyboardMarkup() 143 | //markup.one_time_keyboard = true 144 | markup.resizeKeyboard = true 145 | markup.selective = replyTo != nil 146 | markup.keyboardStrings = [ 147 | [ Commands.add[0], Commands.list[0], Commands.delete[0] ], 148 | [ Commands.help[0], Commands.support[0] ] 149 | ] 150 | context.respondAsync(text, 151 | replyToMessageId: replyTo, // ok to pass nil, it will be ignored 152 | replyMarkup: markup) 153 | 154 | } 155 | 156 | func itemListInlineKeyboardMarkup(context: Context) -> InlineKeyboardMarkup? { 157 | guard let chatId = context.chatId else { return nil } 158 | let items = Item.allItems(in: chatId) 159 | var keyboard = [[InlineKeyboardButton]]() 160 | for item in items { 161 | var button = InlineKeyboardButton() 162 | button.text = "\(item.purchased ? "✅" : "◻️") \(item.name)" 163 | // A hack to left-align the text: 164 | button.text += 165 | " " + 166 | " " + 167 | " ." 168 | button.callbackData = "toggle \(item.itemId!)" 169 | keyboard.append([button]) 170 | } 171 | 172 | var markup = InlineKeyboardMarkup() 173 | markup.inlineKeyboard = keyboard 174 | return markup 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Tests/TelegramBotSDKTests/RouterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouterTests.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2018 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import XCTest 14 | @testable import TelegramBotSDK 15 | 16 | class RouterTests: XCTestCase { 17 | var token: String! 18 | var bot: TelegramBot! 19 | 20 | var update = Update(updateId: 1) 21 | 22 | override func setUp() { 23 | super.setUp() 24 | continueAfterFailure = false 25 | 26 | let chat = Chat(id: 1, type: .privateChat) 27 | update.message = Message(messageId: 1, date: Date(), chat: chat) 28 | 29 | token = readToken(from: "TEST_BOT_TOKEN") 30 | bot = TelegramBot(token: token, fetchBotInfo: false) 31 | 32 | } 33 | 34 | override func tearDown() { 35 | // Put teardown code here. This method is called after the invocation of each test method in the class. 36 | super.tearDown() 37 | } 38 | 39 | func testRouter() { 40 | XCTAssertTrue ( matches(path: "hello", text: "hello") ) 41 | XCTAssertTrue ( matches(path: "hello", text: "he") ) 42 | XCTAssertTrue ( matches(path: "hello", text: "/hello") ) 43 | XCTAssertTrue ( matches(path: "hello", text: "/he") ) 44 | XCTAssertFalse( matches(path: "hello", text: "helllo") ) 45 | 46 | XCTAssertTrue ( matches(paths: ["hello", "world"], text: "hello") ) 47 | XCTAssertTrue ( matches(paths: ["hello", "world"], text: "world") ) 48 | } 49 | 50 | func testCaseSensitivity() { 51 | XCTAssertTrue ( matches(path: "HEllo", text: "helLO") ) 52 | XCTAssertTrue ( matches(path: "HEllo", text: "/helLO") ) 53 | XCTAssertTrue ( matches(path: "HEllo111", text: "helLO") ) 54 | 55 | XCTAssertFalse( matches(path: "HEllo", text: "helLO111") ) 56 | 57 | // This one should show a warning: 58 | XCTAssertTrue ( matches(path: "/HEllo", text: "helLO") ) 59 | } 60 | 61 | func testMultiPath() { 62 | XCTAssertTrue ( matches(paths: ["path1", "path2"], 63 | text: "path1") ) 64 | XCTAssertTrue ( matches(paths: ["path1", "path2"], 65 | text: "path2") ) 66 | XCTAssertFalse( matches(paths: ["path1", "path2"], 67 | text: "path3") ) 68 | } 69 | 70 | func testMultiWordCommands() { 71 | XCTAssertTrue ( matches(path: "hello world", text: "hello world") ) 72 | XCTAssertFalse( matches(path: "hello world", text: "hello") ) 73 | XCTAssertFalse( matches(path: "hello world", text: "") ) 74 | 75 | XCTAssertTrue ( matches(path: "hello world", text: "he wo") ) 76 | XCTAssertFalse( matches(path: "hello world", text: "he word") ) 77 | 78 | XCTAssertFalse( matches(path: "hello world", text: "he wo", options: .slashRequired) ) 79 | XCTAssertTrue ( matches(path: "hello world", text: "/he wo", options: .slashRequired) ) 80 | 81 | XCTAssertFalse( matches(path: "hello world", text: "/he wo", options: .exactMatch) ) 82 | XCTAssertTrue ( matches(path: "/hello world", text: "/he wo", options: .exactMatch) ) 83 | 84 | XCTAssertTrue ( matches(path: "/hello world", text: "/he wo", options: .exactMatch) ) 85 | } 86 | 87 | func testPartialMatch() { 88 | update.message?.text = "hello a b c" 89 | 90 | var matched = false 91 | 92 | let router = Router(bot: bot) 93 | router["hello"] = { context in 94 | return true 95 | } 96 | router.partialMatch = { context in 97 | matched = true 98 | return true 99 | } 100 | 101 | do { try router.process(update: update) } 102 | catch { XCTFail() } 103 | 104 | XCTAssertTrue(matched) 105 | } 106 | 107 | func testUnknownCommand() { 108 | update.message?.text = "/badcmd a b c" 109 | 110 | var matched = false 111 | 112 | let router = Router(bot: bot) 113 | router["hello"] = { context in 114 | return true 115 | } 116 | router.unmatched = { context in 117 | print("Unknown command: \(context.args.scanRestOfString())") 118 | matched = true 119 | return true 120 | } 121 | 122 | do { try router.process(update: update) } 123 | catch { XCTFail() } 124 | 125 | XCTAssertTrue(matched) 126 | } 127 | 128 | func testRouterChaining() { 129 | update.message?.text = "/hello" 130 | 131 | var matched = false 132 | 133 | let router1 = Router(bot: bot) 134 | let router2 = Router(bot: bot) 135 | 136 | router2["hello"] = { context in 137 | matched = true 138 | return true 139 | } 140 | 141 | router1.unmatched = { context in 142 | do { try router2.process(update: self.update) } 143 | catch { XCTFail() } 144 | return true 145 | } 146 | 147 | do { try router1.process(update: update) } 148 | catch { XCTFail() } 149 | 150 | XCTAssertTrue(matched) 151 | } 152 | 153 | func testRouterChaining2() { 154 | update.message?.text = "/hello" 155 | 156 | var matched = false 157 | 158 | let router1 = Router(bot: bot) 159 | let router2 = Router(bot: bot) 160 | 161 | router2["hello"] = { context in 162 | matched = true 163 | return true 164 | } 165 | 166 | router1.unmatched = router2.handler 167 | 168 | do { try router1.process(update: update) } 169 | catch { XCTFail() } 170 | 171 | XCTAssertTrue(matched) 172 | } 173 | 174 | func matches(path: String, text: String, options: Command.Options = []) -> Bool { 175 | update.message?.text = text 176 | 177 | var matched = false 178 | 179 | let router = Router(bot: bot) 180 | router[path, options] = { context in 181 | print("path=\(path) text=\(text) command=\(context.command)") 182 | matched = true 183 | return true 184 | } 185 | 186 | do { try router.process(update: update) } 187 | catch { XCTFail() } 188 | 189 | return matched 190 | } 191 | 192 | func matches(paths: [String], text: String) -> Bool { 193 | update.message?.text = text 194 | 195 | var matched = false 196 | 197 | let router = Router(bot: bot) 198 | router[paths] = { context in 199 | print("paths=\(paths) text=\(text) command=\(context.command)") 200 | matched = true 201 | return true 202 | } 203 | 204 | do { try router.process(update: update) } 205 | catch { XCTFail() } 206 | 207 | return matched 208 | } 209 | 210 | static var allTests : [(String, (RouterTests) -> () throws -> Void)] { 211 | return [ 212 | ("testRouter", testRouter), 213 | ("testCaseSensitivity", testCaseSensitivity), 214 | ("testMultiPath", testMultiPath), 215 | ("testMultiWordCommands", testMultiWordCommands), 216 | ("testPartialMatch", testPartialMatch), 217 | ("testUnknownCommand", testUnknownCommand), 218 | ("testRouterChaining", testRouterChaining), 219 | ("testRouterChaining2", testRouterChaining2), 220 | ] 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Router/Context.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Context.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | import Dispatch 15 | 16 | public class Context { 17 | typealias T = Context 18 | 19 | public let bot: TelegramBot 20 | public let update: Update 21 | /// `update.message` shortcut. Make sure that the message exists before using it, 22 | /// otherwise it will be empty. For paths supported by Router the message is guaranteed to exist. 23 | public var message: Message? { 24 | return update.message ?? 25 | update.editedMessage ?? 26 | update.callbackQuery?.message 27 | } 28 | 29 | /// Command starts with slash (useful if you want to skip commands not starting with slash in group chats) 30 | public let slash: Bool 31 | public let command: String 32 | public let args: Arguments 33 | 34 | public var privateChat: Bool { 35 | guard let message = message else { return false } 36 | return message.chat.type == .privateChat 37 | } 38 | public var chatId: Int64? { return message?.chat.id ?? 39 | update.callbackQuery?.message?.chat.id 40 | } 41 | public var fromId: Int64? { 42 | return update.message?.from?.id ?? 43 | (update.editedMessage?.from?.id ?? 44 | update.callbackQuery?.from.id) 45 | } 46 | public var properties: [String: AnyObject] 47 | 48 | init(bot: TelegramBot, update: Update, scanner: Scanner, command: String, startsWithSlash: Bool, properties: [String: AnyObject] = [:]) { 49 | self.bot = bot 50 | self.update = update 51 | self.slash = startsWithSlash 52 | self.command = command 53 | self.args = Arguments(scanner: scanner) 54 | self.properties = properties 55 | } 56 | 57 | /// Sends a message to current chat. 58 | /// - SeeAlso: 59 | @discardableResult 60 | public func respondSync(_ text: String, 61 | parseMode: ParseMode? = nil, 62 | disableWebPagePreview: Bool? = nil, 63 | disableNotification: Bool? = nil, 64 | replyToMessageId: Int? = nil, 65 | replyMarkup: ReplyMarkup? = nil, 66 | _ parameters: [String: Encodable?] = [:]) -> Message? { 67 | guard let chatId = chatId else { 68 | assertionFailure("respondSync() used when update.message is nil") 69 | bot.lastError = nil 70 | return nil 71 | } 72 | return bot.sendMessageSync( 73 | chatId: .chat(chatId), 74 | text: text, 75 | parseMode: parseMode, 76 | disableWebPagePreview: disableWebPagePreview, 77 | disableNotification: disableNotification, 78 | replyToMessageId: replyToMessageId, 79 | replyMarkup: replyMarkup, 80 | parameters) 81 | } 82 | 83 | /// Sends a message to current chat. 84 | /// - SeeAlso: 85 | public func respondAsync(_ text: String, 86 | parseMode: ParseMode? = nil, 87 | disableWebPagePreview: Bool? = nil, 88 | disableNotification: Bool? = nil, 89 | replyToMessageId: Int? = nil, 90 | replyMarkup: ReplyMarkup? = nil, 91 | _ parameters: [String: Encodable?] = [:], 92 | queue: DispatchQueue = .main, 93 | completion: TelegramBot.SendMessageCompletion? = nil) { 94 | guard let chatId = chatId else { 95 | assertionFailure("respondAsync() used when update.message is nil") 96 | return 97 | } 98 | return bot.sendMessageAsync( 99 | chatId: .chat(chatId), 100 | text: text, 101 | parseMode: parseMode, 102 | disableWebPagePreview: disableWebPagePreview, 103 | disableNotification: disableNotification, 104 | replyToMessageId: replyToMessageId, 105 | replyMarkup: replyMarkup, 106 | parameters, queue: queue, 107 | completion: completion) 108 | } 109 | 110 | /// Respond privately also sending a message to a group. 111 | /// - SeeAlso: 112 | @discardableResult 113 | public func respondPrivatelySync(_ userText: String, groupText: String) -> (userMessage: Message?, groupMessage: Message?) { 114 | var userMessage: Message? 115 | if let fromId = fromId { 116 | userMessage = bot.sendMessageSync(chatId: .chat(fromId), text: userText) 117 | } 118 | var groupMessage: Message? = nil 119 | if !privateChat { 120 | if let chatId = chatId { 121 | groupMessage = bot.sendMessageSync(chatId: .chat(chatId), text: groupText) 122 | } else { 123 | assertionFailure("respondPrivatelySync() used when update.message is nil") 124 | bot.lastError = nil 125 | } 126 | } 127 | return (userMessage, groupMessage) 128 | } 129 | 130 | /// Respond privately also sending a message to a group. 131 | /// - SeeAlso: 132 | public func respondPrivatelyAsync(_ userText: String, groupText: String, 133 | onDidSendToUser userCompletion: TelegramBot.SendMessageCompletion? = nil, 134 | onDidSendToGroup groupCompletion: TelegramBot.SendMessageCompletion? = nil) { 135 | if let fromId = fromId { 136 | bot.sendMessageAsync(chatId: .chat(fromId), text: userText, completion: userCompletion) 137 | } 138 | if !privateChat { 139 | if let chatId = chatId { 140 | bot.sendMessageAsync(chatId: .chat(chatId), text: groupText, completion: groupCompletion) 141 | } else { 142 | assertionFailure("respondPrivatelyAsync() used when update.message is nil") 143 | } 144 | } 145 | } 146 | 147 | @discardableResult 148 | public func reportErrorSync(text: String, errorDescription: String) -> Message? { 149 | guard let chatId = chatId else { 150 | assertionFailure("reportErrorSync() used when update.message is nil") 151 | bot.lastError = nil 152 | return nil 153 | } 154 | return bot.reportErrorSync(chatId: chatId, text: text, errorDescription: errorDescription) 155 | } 156 | 157 | @discardableResult 158 | public func reportErrorSync(errorDescription: String) -> Message? { 159 | guard let chatId = chatId else { 160 | assertionFailure("reportErrorSync() used when update.message is nil") 161 | bot.lastError = nil 162 | return nil 163 | } 164 | return bot.reportErrorSync(chatId: chatId, errorDescription: errorDescription) 165 | } 166 | 167 | public func reportErrorAsync(text: String, errorDescription: String, completion: TelegramBot.SendMessageCompletion? = nil) { 168 | guard let chatId = chatId else { 169 | assertionFailure("reportErrorAsync() used when update.message is nil") 170 | return 171 | } 172 | bot.reportErrorAsync(chatId: chatId, text: text, errorDescription: errorDescription, completion: completion) 173 | } 174 | 175 | public func reportErrorAsync(errorDescription: String, completion: TelegramBot.SendMessageCompletion? = nil) { 176 | guard let chatId = chatId else { 177 | assertionFailure("reportErrorAsync() used when update.message is nil") 178 | return 179 | } 180 | bot.reportErrorAsync(chatId: chatId, errorDescription: errorDescription, completion: completion) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Rapier/Sources/RapierCLI/Generators/TelegramBotSDKCodableGenerator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Rapier 3 | 4 | private struct Context { 5 | let directory: String 6 | 7 | var outTypes: String = "" 8 | var outMethods: String = "" 9 | 10 | init(directory: String) { 11 | self.directory = directory 12 | } 13 | } 14 | 15 | class TelegramBotSDKCodableGenerator: CodeGenerator { 16 | required init(directory: String) { 17 | self.context = Context(directory: directory) 18 | } 19 | 20 | func start() throws { 21 | 22 | } 23 | 24 | func beforeGeneratingTypes() throws { 25 | let header = """ 26 | // This file is automatically generated by Rapier 27 | 28 | import Foundation 29 | 30 | 31 | """ 32 | context.outTypes.append(header) 33 | } 34 | 35 | func generateType(name: String, info: TypeInfo) throws { 36 | context.outTypes.append(""" 37 | public class \(name): Codable {\n 38 | """) 39 | var allInitParams: [String] = [] 40 | info.fields.forEach { fieldInfo in 41 | let getterName = makeGetterName(typeName: name, fieldName: fieldInfo.name, fieldType: fieldInfo.type) 42 | if fieldInfo.type == "True" { 43 | allInitParams.append(#""\#(fieldInfo.name)" = true"#) 44 | } else { 45 | if let field = buildFieldTemplate(fieldName: getterName, fieldInfo: fieldInfo) { 46 | context.outTypes.append(field) 47 | } 48 | } 49 | 50 | } 51 | var initParamsString = allInitParams.joined(separator: ", ") 52 | if initParamsString.isEmpty { 53 | initParamsString = "[:]" 54 | } 55 | 56 | context.outTypes.append(generateTypeInit(typeName: name, fields: info.fields)) 57 | 58 | context.outTypes.append(""" 59 | }\n\n\n 60 | """) 61 | } 62 | 63 | func generateTypeInit(typeName: String, fields: [FieldInfo]) -> String { 64 | let initParameters = fields.map { (fieldInfo) -> String in 65 | let name = makeGetterName(typeName: typeName, fieldName: fieldInfo.name, fieldType: fieldInfo.type) 66 | var type = fieldInfo.type 67 | 68 | if fieldInfo.isArray { 69 | type = "[\(type)]" 70 | } 71 | 72 | if fieldInfo.isArrayOfArray { 73 | type = "[\(type)]" 74 | } 75 | 76 | if fieldInfo.isOptional { 77 | type = "\(type)? = nil" 78 | } 79 | 80 | return "\(name.camelized()): \(type)" 81 | }.joined(separator: ", ") 82 | 83 | var initString = "" 84 | initString.append(""" 85 | public init(\(initParameters)) {\n 86 | """) 87 | 88 | for field in fields { 89 | let name = field.name.camelized() 90 | initString.append(" self.\(name) = \(name)\n") 91 | } 92 | 93 | initString.append(" }\n\n") 94 | 95 | return initString 96 | } 97 | 98 | func afterGeneratingTypes() throws { 99 | } 100 | 101 | func beforeGeneratingMethods() throws { 102 | let methodsHeader = """ 103 | // This file is automatically generated by Rapier 104 | 105 | 106 | import Foundation 107 | import Dispatch 108 | 109 | public extension TelegramBot { 110 | 111 | 112 | """ 113 | 114 | context.outMethods.append(methodsHeader) 115 | } 116 | 117 | func generateMethod(name: String, info: MethodInfo) throws { 118 | 119 | let parameters = info.parameters 120 | 121 | let fields: [String] = parameters.map { fieldInfo in 122 | var result = "\(fieldInfo.name.camelized()): \(buildSwiftType(fieldInfo: fieldInfo))" 123 | if fieldInfo.isOptional { 124 | result.append(" = nil") 125 | } 126 | 127 | return result 128 | } 129 | 130 | let arrayFields: [String] = parameters.map { fieldInfo in 131 | return #""\#(fieldInfo.name)": \#(fieldInfo.name.camelized())"# 132 | } 133 | 134 | var fieldsString = fields.joined(separator: ",\n ") 135 | var arrayFieldsString = arrayFields.joined(separator: ",\n") 136 | 137 | let completionName = (name.first?.uppercased() ?? "") + name.dropFirst() + "Completion" 138 | let resultSwiftType = buildSwiftType(fieldInfo: info.result) 139 | 140 | if !fieldsString.isEmpty { 141 | fieldsString.append(",") 142 | } 143 | 144 | if arrayFieldsString.isEmpty { 145 | arrayFieldsString = ":" 146 | } 147 | 148 | let method = """ 149 | typealias \(completionName) = (_ result: \(resultSwiftType), _ error: DataTaskError?) -> () 150 | 151 | @discardableResult 152 | func \(name)Sync( 153 | \(fieldsString) 154 | _ parameters: [String: Encodable?] = [:]) -> \(resultSwiftType) { 155 | return requestSync("\(name)", defaultParameters["\(name)"], parameters, [ 156 | \(arrayFieldsString)]) 157 | } 158 | 159 | func \(name)Async( 160 | \(fieldsString) 161 | _ parameters: [String: Encodable?] = [:], 162 | queue: DispatchQueue = .main, 163 | completion: \(completionName)? = nil) { 164 | return requestAsync("\(name)", defaultParameters["\(name)"], parameters, [ 165 | \(arrayFieldsString)], 166 | queue: queue, completion: completion) 167 | } 168 | 169 | """ 170 | 171 | context.outMethods.append(method) 172 | } 173 | 174 | func afterGeneratingMethods() throws { 175 | context.outMethods.append("\n}\n") 176 | } 177 | 178 | func finish() throws { 179 | try saveTypes() 180 | try saveMethods() 181 | } 182 | 183 | private func saveTypes() throws { 184 | let dir = URL(fileURLWithPath: context.directory, isDirectory: true) 185 | let file = dir.appendingPathComponent("Types.swift", isDirectory: false) 186 | try context.outTypes.write(to: file, atomically: true, encoding: .utf8) 187 | } 188 | 189 | private func saveMethods() throws { 190 | let dir = URL(fileURLWithPath: context.directory, isDirectory: true) 191 | let file = dir.appendingPathComponent("Methods.swift", isDirectory: false) 192 | try context.outMethods.write(to: file, atomically: true, encoding: .utf8) 193 | } 194 | 195 | private func buildSwiftType(fieldInfo: FieldInfo) -> String { 196 | var type: String 197 | if (fieldInfo.isArray) { 198 | type = "[\(fieldInfo.type)]" 199 | } else { 200 | type = fieldInfo.type 201 | } 202 | if (fieldInfo.isOptional) { 203 | type.append("?") 204 | } 205 | return type 206 | } 207 | 208 | private func buildFieldTemplate(fieldName: String, fieldInfo: FieldInfo) -> String? { 209 | let type = fieldInfo.type 210 | let isOptional = fieldInfo.isOptional 211 | let name = fieldName.camelized() 212 | 213 | if fieldInfo.isArrayOfArray { 214 | return """ 215 | public var \(name): [[\(type)]]\(isOptional ? "?" : "") = [[]]\n 216 | """ 217 | } else if fieldInfo.isArray { 218 | return """ 219 | public var \(name): [\(type)]\(isOptional ? "?" : "") = []\n 220 | """ 221 | } else { 222 | return """ 223 | public var \(name): \(type)\(isOptional ? "?" : "")\n 224 | """ 225 | } 226 | } 227 | 228 | private var context: Context 229 | } 230 | 231 | extension TelegramBotSDKCodableGenerator { 232 | private func makeGetterName(typeName: String, fieldName: String, fieldType: String) -> String { 233 | return fieldName 234 | } 235 | } 236 | 237 | extension String { 238 | fileprivate func camelized() -> String { 239 | let components = self.components(separatedBy: "_") 240 | 241 | let firstLowercasedWord = components.first?.lowercased() 242 | 243 | let remainingWords = components.dropFirst().map { 244 | $0.prefix(1).uppercased() + $0.dropFirst().lowercased() 245 | } 246 | return ([firstLowercasedWord].compactMap{ $0 } + remainingWords).joined() 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/HTTPUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPUtils.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | struct AnyEncodable: Encodable { 16 | let value: Encodable 17 | 18 | func encode(to encoder: Encoder) throws { 19 | var container = encoder.singleValueContainer() 20 | try value.encode(to: &container) 21 | } 22 | } 23 | 24 | extension Encodable { 25 | func encode(to container: inout SingleValueEncodingContainer) throws { 26 | try container.encode(self) 27 | } 28 | } 29 | 30 | public class HTTPUtils { 31 | enum EncodeResult { 32 | case success(String) 33 | case skipThisValue 34 | case error 35 | } 36 | 37 | private class func encodeValue(_ value: Encodable) -> EncodeResult { 38 | if let boolValue = value as? Bool { 39 | if !boolValue { 40 | return .skipThisValue 41 | } 42 | // If true, add "key=" to encoded string 43 | return .success("true") 44 | } 45 | 46 | if let value = value as? String { 47 | return .success(value) 48 | } 49 | 50 | let encodableBox = AnyEncodable(value: value) 51 | let encoder = JSONEncoder() 52 | encoder.dateEncodingStrategy = .secondsSince1970 53 | encoder.keyEncodingStrategy = .convertToSnakeCase 54 | let jsonEncodedData = try? encoder.encode(encodableBox) 55 | guard let jsonEncodedUnwrappedData = jsonEncodedData else { return .error } 56 | guard var jsonEncodedString = String(data: jsonEncodedUnwrappedData, encoding: .utf8) else { return .error } 57 | 58 | if jsonEncodedString.hasPrefix("\"") && jsonEncodedString.hasSuffix("\"") { 59 | jsonEncodedString = String(jsonEncodedString.dropFirst().dropLast()) 60 | } 61 | 62 | return .success(jsonEncodedString) 63 | } 64 | 65 | /// Encodes keys and values in a dictionary for using with 66 | /// `application/x-www-form-urlencoded` Content-Type and 67 | /// joins them into a single string. 68 | /// 69 | /// Keys corresponding to nil values are skipped and 70 | /// are not added to the resulting string. 71 | /// 72 | /// - SeeAlso: Encoding is performed using String's `formUrlencode` method. 73 | /// - Returns: Encoded string. 74 | public class func formUrlencode(_ dictionary: [String: Encodable?]) -> String? { 75 | var result = "" 76 | for (key, valueOrNil) in dictionary { 77 | guard let value = valueOrNil else { continue } 78 | switch encodeValue(value) { 79 | case .success(let valueString): 80 | if !result.isEmpty { 81 | result += "&" 82 | } 83 | let keyUrlencoded = key.formUrlencode() 84 | let valueUrlencoded = valueString.formUrlencode() 85 | result += "\(keyUrlencoded)=\(valueUrlencoded)" 86 | case .skipThisValue: 87 | continue 88 | case .error: 89 | return nil 90 | } 91 | 92 | } 93 | return result 94 | } 95 | 96 | /// Encodes keys and values in a dictionary for using with 97 | /// `application/x-www-form-urlencoded` Content-Type and 98 | /// joins them into a single string. 99 | /// 100 | /// - SeeAlso: Encoding is performed using String's `formUrlencode` method. 101 | /// - Returns: Encoded string. 102 | public class func formUrlencode(_ dictionary: [String: String]) -> String { 103 | var result = "" 104 | for (keyString, valueString) in dictionary { 105 | if !result.isEmpty { 106 | result += "&" 107 | } 108 | let keyUrlencoded = keyString.formUrlencode() 109 | let valueUrlencoded = valueString.formUrlencode() 110 | result += "\(keyUrlencoded)=\(valueUrlencoded)" 111 | } 112 | return result 113 | } 114 | 115 | // http://stackoverflow.com/questions/26162616/upload-image-with-parameters-in-swift 116 | 117 | /// Create boundary string for multipart/form-data request 118 | /// 119 | /// - returns: The boundary string that consists of "Boundary-" followed by a UUID string. 120 | public class func generateBoundaryString() -> String { 121 | return "Boundary-\(NSUUID().uuidString)" 122 | //return "-----------------------------Boundary-\(NSUUID().uuidString)" 123 | } 124 | 125 | /// Create body of the multipart/form-data request 126 | /// 127 | /// - parameter parameters: The dictionary containing keys and values to be passed to web service 128 | /// - parameter boundary: The multipart/form-data boundary 129 | /// 130 | /// - returns: The Data of the body of the request 131 | 132 | public class func createMultipartFormDataBody(with parameters: [String: Encodable?], boundary: String) -> Data? { 133 | var body = Data() 134 | 135 | guard let boundary1 = "--\(boundary)\r\n".data(using: .utf8) else { 136 | return nil 137 | } 138 | guard let boundary2 = ("--\(boundary)--\r\n").data(using: .utf8) else { 139 | return nil 140 | } 141 | 142 | for (key, valueOrNil) in parameters { 143 | guard let value = valueOrNil else { continue } 144 | 145 | body.append(boundary1) 146 | 147 | if let inputFile = value as? InputFile { 148 | let filename = inputFile.filename 149 | let mimetype = inputFile.mimeType ?? mimeType(for: filename) 150 | let data = inputFile.data 151 | guard let contentDisposition = "Content-Disposition: form-data; name=\"\(key)\"; filename=\"\(filename)\"\r\n".data(using: .utf8) else { 152 | return nil 153 | } 154 | body.append(contentDisposition) 155 | guard let mimeType = "Content-Type: \(mimetype)\r\n\r\n".data(using: .utf8) else { 156 | return nil 157 | } 158 | body.append(mimeType) 159 | body.append(data) 160 | body.append("\r\n".data(using: .utf8)!) 161 | } else if let inputFileOrString = value as? InputFileOrString { 162 | if case InputFileOrString.inputFile(let inputFile) = inputFileOrString { 163 | let filename = inputFile.filename 164 | let mimetype = inputFile.mimeType ?? mimeType(for: filename) 165 | let data = inputFile.data 166 | guard let contentDisposition = "Content-Disposition: form-data; name=\"\(key)\"; filename=\"\(filename)\"\r\n".data(using: .utf8) else { 167 | return nil 168 | } 169 | body.append(contentDisposition) 170 | guard let mimeType = "Content-Type: \(mimetype)\r\n\r\n".data(using: .utf8) else { 171 | return nil 172 | } 173 | body.append(mimeType) 174 | body.append(data) 175 | body.append("\r\n".data(using: .utf8)!) 176 | } 177 | } else { 178 | switch encodeValue(value) { 179 | case .success(let valueString): 180 | guard let contentDisposition = "Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8) else { 181 | return nil 182 | } 183 | body.append(contentDisposition) 184 | guard let valueData = "\(valueString)\r\n".data(using: .utf8) else { 185 | return nil 186 | } 187 | body.append(valueData) 188 | case .skipThisValue: 189 | continue 190 | case .error: 191 | return nil 192 | } 193 | } 194 | } 195 | body.append(boundary2) 196 | 197 | return body 198 | } 199 | 200 | /// Determine mime type on the basis of extension of a file. 201 | /// 202 | /// This requires MobileCoreServices framework. 203 | /// 204 | /// - parameter path: The path of the file for which we are going to determine the mime type. 205 | /// 206 | /// - returns: Returns the mime type if successful. Returns application/octet-stream if unable to determine mime type. 207 | 208 | public class func mimeType(for path: String) -> String { 209 | let url = URL(fileURLWithPath: path) 210 | if !url.pathExtension.isEmpty, 211 | let mimeType = mimeTypes[url.pathExtension.lowercased()] { 212 | return mimeType 213 | } 214 | return "application/octet-stream" 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /Sources/TelegramBotSDK/Router/Router.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Router.swift 3 | // 4 | // This source file is part of the Telegram Bot SDK for Swift (unofficial). 5 | // 6 | // Copyright (c) 2015 - 2020 Andrey Fidrya and the project authors 7 | // Licensed under Apache License v2.0 with Runtime Library Exception 8 | // 9 | // See LICENSE.txt for license information 10 | // See AUTHORS.txt for the list of the project authors 11 | // 12 | 13 | import Foundation 14 | 15 | public class Router { 16 | public typealias Handler = (_ context: Context) throws -> Bool 17 | public typealias Path = (contentType: ContentType, handler: Handler) 18 | 19 | public var caseSensitive = false 20 | public var charactersToBeSkipped: CharacterSet? = CharacterSet.whitespacesAndNewlines 21 | 22 | public var bot: TelegramBot 23 | 24 | public lazy var partialMatch: Handler? = { context in 25 | context.respondAsync("❗ Part of your input was ignored: \(context.args.scanRestOfString())") 26 | return true 27 | } 28 | 29 | public lazy var unmatched: Handler? = { context in 30 | guard context.privateChat else { return false } 31 | guard let command = context.args.scanWord() else { return false } 32 | context.respondAsync("Unrecognized command: \(command). Type /help for help.") 33 | return true 34 | } 35 | 36 | public lazy var unsupportedContentType: Handler? = { context in 37 | guard context.privateChat else { return false } 38 | if !context.args.isAtEnd { 39 | context.respondAsync("Unsupported action.") 40 | } else { 41 | context.respondAsync("Unsupported content type.") 42 | } 43 | return true 44 | } 45 | 46 | public var handler: Handler { 47 | return { [weak self] context in 48 | try self?.process(update: context.update) 49 | return true 50 | } 51 | } 52 | 53 | public init(bot: TelegramBot) { 54 | self.bot = bot 55 | } 56 | 57 | public convenience init(bot: TelegramBot, setup: (_ router: Router)->()) { 58 | self.init(bot: bot) 59 | setup(self) 60 | } 61 | 62 | public func add(_ contentType: ContentType, _ handler: @escaping (Context) throws -> Bool) { 63 | paths.append(Path(contentType, handler)) 64 | } 65 | 66 | public func add(_ command: Command, _ handler: @escaping (Context) throws -> Bool) { 67 | paths.append(Path(.command(command), handler)) 68 | } 69 | 70 | public func add(_ commands: [Command], _ handler: @escaping (Context) throws -> Bool) { 71 | paths.append(Path(.commands(commands), handler)) 72 | } 73 | 74 | @discardableResult 75 | public func process(update: Update, properties: [String: AnyObject] = [:]) throws -> Bool { 76 | let string = update.message?.extractCommand(for: bot) ?? "" 77 | let scanner = Scanner(string: string) 78 | scanner.caseSensitive = caseSensitive 79 | scanner.charactersToBeSkipped = charactersToBeSkipped 80 | let originalScanLocation = scanner.scanLocation 81 | 82 | for path in paths { 83 | var command = "" 84 | var startsWithSlash = false 85 | if !match(contentType: path.contentType, update: update, commandScanner: scanner, userCommand: &command, startsWithSlash: &startsWithSlash) { 86 | scanner.scanLocation = originalScanLocation 87 | continue; 88 | } 89 | 90 | let context = Context(bot: bot, update: update, scanner: scanner, command: command, startsWithSlash: startsWithSlash, properties: properties) 91 | let handler = path.handler 92 | 93 | if try handler(context) { 94 | try checkPartialMatch(context: context) 95 | return true 96 | } 97 | 98 | scanner.scanLocation = originalScanLocation 99 | } 100 | 101 | if update.message != nil && !string.isEmpty { 102 | if let unmatched = unmatched { 103 | let context = Context(bot: bot, update: update, scanner: scanner, command: "", startsWithSlash: false, properties: properties) 104 | return try unmatched(context) 105 | } 106 | } else { 107 | if let unsupportedContentType = unsupportedContentType { 108 | let context = Context(bot: bot, update: update, scanner: scanner, command: "", startsWithSlash: false, properties: properties) 109 | return try unsupportedContentType(context) 110 | } 111 | } 112 | 113 | return false 114 | } 115 | 116 | func match(contentType: ContentType, update: Update, commandScanner: Scanner, userCommand: inout String, startsWithSlash: inout Bool) -> Bool { 117 | 118 | if let message = update.message { 119 | switch contentType { 120 | case .command(let command): 121 | guard let result = command.fetchFrom(commandScanner, caseSensitive: caseSensitive) else { 122 | return false // Does not match path command 123 | } 124 | userCommand = result.command 125 | startsWithSlash = result.startsWithSlash 126 | return true 127 | case .commands(let commands): 128 | let originalScanLocation = commandScanner.scanLocation 129 | for command in commands { 130 | guard let result = command.fetchFrom(commandScanner, caseSensitive: caseSensitive) else { 131 | commandScanner.scanLocation = originalScanLocation 132 | continue 133 | } 134 | userCommand = result.command 135 | startsWithSlash = result.startsWithSlash 136 | return true 137 | } 138 | return false 139 | case .from: return message.from != nil 140 | case .forwardFrom: return message.forwardFrom != nil 141 | case .forwardFromChat: return message.forwardFromChat != nil 142 | case .forwardDate: return message.forwardDate != nil 143 | case .replyToMessage: return message.replyToMessage != nil 144 | case .editDate: return message.editDate != nil 145 | case .text: return message.text != nil 146 | case .entities: return !(message.entities ?? []).isEmpty 147 | case .audio: return message.audio != nil 148 | case .document: return message.document != nil 149 | case .photo: return !(message.photo ?? []).isEmpty 150 | case .sticker: return message.sticker != nil 151 | case .video: return message.video != nil 152 | case .voice: return message.voice != nil 153 | case .caption: return message.caption != nil 154 | case .contact: return message.contact != nil 155 | case .location: return message.location != nil 156 | case .venue: return message.venue != nil 157 | case .newChatMembers: return !(message.newChatMembers ?? []).isEmpty 158 | case .leftChatMember: return message.leftChatMember != nil 159 | case .newChatTitle: return message.newChatTitle != nil 160 | case .newChatPhoto: return !(message.newChatPhoto ?? []).isEmpty 161 | case .deleteChatPhoto: return message.deleteChatPhoto ?? false 162 | case .groupChatCreated: return message.groupChatCreated ?? false 163 | case .supergroupChatCreated: return message.supergroupChatCreated ?? false 164 | case .channelChatCreated: return message.channelChatCreated ?? false 165 | case .migrateToChatId: return message.migrateToChatId != nil 166 | case .migrateFromChatId: return message.migrateFromChatId != nil 167 | case .pinnedMessage: return message.pinnedMessage != nil 168 | default: break 169 | } 170 | } else if let message = update.editedMessage { 171 | switch contentType { 172 | case .editedFrom: return message.from != nil 173 | case .editedForwardFrom: return message.forwardFrom != nil 174 | case .editedForwardFromChat: return message.forwardFromChat != nil 175 | case .editedForwardDate: return message.forwardDate != nil 176 | case .editedReplyToMessage: return message.replyToMessage != nil 177 | case .editedEditDate: return message.editDate != nil 178 | case .editedText: return message.text != nil 179 | case .editedEntities: return !(message.entities ?? []).isEmpty 180 | case .editedAudio: return message.audio != nil 181 | case .editedDocument: return message.document != nil 182 | case .editedPhoto: return !(message.photo ?? []).isEmpty 183 | case .editedSticker: return message.sticker != nil 184 | case .editedVideo: return message.video != nil 185 | case .editedVoice: return message.voice != nil 186 | case .editedCaption: return message.caption != nil 187 | case .editedContact: return message.contact != nil 188 | case .editedLocation: return message.location != nil 189 | case .editedVenue: return message.venue != nil 190 | default: break 191 | } 192 | } else { 193 | switch contentType { 194 | case .callback_query(let data): 195 | if let data = data { 196 | return update.callbackQuery?.data == data 197 | } 198 | return update.callbackQuery != nil 199 | default: break 200 | } 201 | } 202 | return false 203 | } 204 | 205 | // After processing the command, check that no unprocessed text is left 206 | @discardableResult 207 | func checkPartialMatch(context: Context) throws -> Bool { 208 | 209 | // Note that scanner.atEnd automatically ignores charactersToBeSkipped 210 | if !context.args.isAtEnd { 211 | // Partial match 212 | if let handler = partialMatch { 213 | return try handler(context) 214 | } 215 | } 216 | 217 | return true 218 | } 219 | 220 | var paths = [Path]() 221 | } 222 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # telegram-bot-swift changelog 2 | 3 | ## 0.16.1 (2017-04-16) 4 | 5 | - Bugfix: POST data length was calculated incorrectly resulting in 6 | getFile returning HTTP error 400 "invalid file_id". 7 | 8 | ## 0.16.0 (2017-04-03) 9 | 10 | - Switch to IBM's version of SwiftyJSON for Linux compatibility. 11 | 12 | ## 0.15.0 (2017-04-03) 13 | 14 | - Memory leak fixed. 15 | 16 | - Callback_query bug fixed. 17 | 18 | - Message editing requests generation fixed. 19 | 20 | - Better error reporting. 21 | 22 | - Move back to original SwiftyJSON. 23 | 24 | ## 0.14.0 (2017-02-17) 25 | 26 | - Library rewritten to use libcurl because Foundation's URLSession is not working reliably on Linux yet. 27 | 28 | - File attachments are now supported (via InputFile). Check testSendPhoto() test in RequestTests.swift for an example. 29 | 30 | ## 0.13.0 (2016-12-04) 31 | 32 | - Update to Bot API 2.3.1. 33 | 34 | ## 0.12.0 (2016-12-03) 35 | 36 | - Linux port, thanks to Andrea de Marco who fixed most of the bugs! 37 | 38 | - Telegram API updated to most recent one. 39 | 40 | ## 0.11.0 (2016-08-30) 41 | 42 | - Updated to 2016-08-26 Swift toolchain. 43 | 44 | ## 0.10.0 (2016-07-03) 45 | 46 | - Added `Examples/shopster-bot`: a sample bot which maintains a shopping list using sqlite3 database. [GRDB library](https://github.com/groue/GRDB.swift) is used for working with database. This bot allows creating shared shopping lists in group chats. 47 | 48 | - Callback query data used in InlineButtons can now be parsed similarly to plaintext commands using arguments scanner. Simply call `context.args.scanWord()` to fetch a word from callback data and so on. 49 | 50 | - Router path `.callback_query` now accepts nil: `callback_query(data: nil)`. Pass nil to match any data, then parse it in handler using arguments scanner. 51 | 52 | - Router now supports context-sensitive user properties. Pass them to `process` method: 53 | 54 | ```swift 55 | var properties = [String: AnyObject]() 56 | properties["myField"] = myValue 57 | try router.process(update: update, properties: properties) 58 | ``` 59 | 60 | And use them in handlers: 61 | 62 | ```swift 63 | func myHandler(context: Context) -> Bool { 64 | let myValue = context.properties["myField"] as? MyValueType 65 | // ... 66 | } 67 | ``` 68 | 69 | Or make a `Context` category for easier access to your properties, for example: 70 | 71 | ```swift 72 | extension Context { 73 | var session: Session { return properties["session"] as! Session } 74 | } 75 | ``` 76 | 77 | - Added `scanInt64()` to arguments scanner. 78 | 79 | - `readToken("filename or env var")` is now `readToken(from: "filename or env var")` 80 | 81 | ## 0.9.0 (2016-06-27) 82 | 83 | ### Major changes: 84 | 85 | - Project ported to Swift 3.0 Snapshot 2016-06-20 (a). Xcode8 is required. 86 | 87 | - Types and Requests (methods) are now generated automatically from Telegram docs by a Ruby script located in `API/` directory. 88 | 89 | - All types and requests are now supported. 90 | 91 | - Optional parameters added to request signatures. This code: 92 | 93 | ```swift 94 | bot.sendMessage(chatId, "text", ["reply_markup": markup]) 95 | ``` 96 | 97 | Can now be written as: 98 | 99 | ```swift 100 | bot.sendMessage(chatId, "text", reply_markup: markup) 101 | ``` 102 | 103 | You can still pass an array with additional arguments at the end of parameters list if needed. 104 | 105 | ### Other changes: 106 | 107 | - Router now supports multiple comma separated paths: 108 | 109 | ```swift 110 | router["List Items", "list"] = onListItems 111 | ``` 112 | 113 | - Router is now case insensitive by default. 114 | 115 | - Multiword commands are now supported: 116 | 117 | ```swift 118 | router["list add"] = onListAdd 119 | router["list remove"] = onListRemove 120 | ``` 121 | 122 | - Router chaining is now supported. Use `handler` method to use Router as a handler: 123 | 124 | `router1.unmatched = router2.handler` 125 | 126 | - To force the use of slash, instead of `slash: .required` option use: 127 | 128 | ```swift 129 | router["command", .slashRequired] = handler 130 | ``` 131 | 132 | Multiple flags can be specified: 133 | 134 | ```swift 135 | router["command", [.caseSensitive, .slashRequired]] = handler 136 | ``` 137 | 138 | - `context.args.command` is now `context.command`. 139 | 140 | - New variable: `context.slash`. True, if command was prefixed with a slash. 141 | 142 | - `router.unknownCommand` handler renamed to `router.unmatched` 143 | 144 | - Support `callback_query` in Router. 145 | 146 | - `JsonObject` protocol renamed to `JsonConvertible`. 147 | 148 | - `Context.message` is now optional. Also, it fallbacks to `edited_message` and `callback_query.message` when nil. 149 | 150 | - Unknown command handler will no longer treat the first word as a command and will pass the entire string to a handler unchanged. `Context.command` will be empty. 151 | 152 | - partialMatchHandler return value is now ignored. It can no longer cancel commands. 153 | 154 | - All types changed from classes to structs. 155 | 156 | - HTTP error codes except 401 (authentication error) are no longer fatal errors. TelegramBot will try to reconnect when encountering them. 157 | 158 | ## 0.8.0 (2016-06-15) 159 | 160 | - Upgrade to Xcode 8 (Swift 3.0 Preview 1) 161 | - Bugfix: "unknown command" / "unsupported content type" messages are no longer sent to group chats. 162 | 163 | ## 0.7.0 (2016-06-13) 164 | 165 | - All enums renamed to match Swift 3 guidelines. 166 | - Request function signatures changed: `parameters` label is no longer explicit, added missing enum to `sendChatAction` and a few other fixes. 167 | - Bugfix: `ReplyKeyboardHide` is now JsonObject. 168 | - Bugfix: getMeAsync is now public. 169 | - Bugfix: default parameters now work correctly. For example, to disable notifications for all sendMessage calls, do: 170 | 171 | ```swift 172 | bot.defaultParameters["sendMessage"] = {"disable_notification": true} 173 | ``` 174 | 175 | - `Int` now conforms to `JsonObject` and can be used as Request's return value. 176 | - Fixed existing tests and added more tests. 177 | - When trying to access the message via `context.message` non-optional shortcut and the message was nil initially, a warning will be printed. 178 | - Added .new_chat_member router path to hello-bot sample project. 179 | 180 | 181 | ## 0.6.2 (2016-06-08) 182 | 183 | - Ported to `Swift-DEVELOPMENT-SNAPSHOT-2016-06-06-a`. 184 | 185 | ## 0.6.0 (2016-06-08) 186 | 187 | - Router handlers now take Context and return Bool. Other overloads were removed. This was done to simplify Router usage with closures. Closure signatures had to be specified explicitly, but now they can be inferred automatically. 188 | - Message.from is now Optional. 189 | - Add readConfigurationValue helper function which tries to read a single value from environment variable or from a file. 190 | - Request methods are now snake case like structure fields to match Telegram Bot API docs. 191 | - Examples updated to use the new API. 192 | - Added all types and requests except inline types. 193 | - Added missing fields which appeared in API 2.0 to all types. 194 | - Router now works with Updates instead of Messages. 195 | - Allow using raw JSON in requestAsync / requestSync. 196 | - Chat and user ids are now Int64. 197 | 198 | ## 0.5.1 (2016-05-31) 199 | 200 | Added `ReplyKeyboardMarkup` which can be used with `Strings`: 201 | 202 | ```swift 203 | let markup = ReplyKeyboardMarkup() 204 | markup.keyboardStrings = [["/a", "/b"], ["/c", "/d"]] 205 | context.respondAsync("Simple keyboard", parameters: ["reply_markup": markup]) 206 | ``` 207 | 208 | Or with `KeyboardButtons`: 209 | 210 | ```swift 211 | let button1 = KeyboardButton() 212 | button1.text = "Request contact" 213 | button1.request_contact = true 214 | 215 | let button2 = KeyboardButton() 216 | button2.text = "Request location" 217 | button2.request_location = true 218 | 219 | markup.keyboardButtons = [ [ button1, button2 ] ] 220 | 221 | context.respondAsync("Another keyboard", parameters: ["reply_markup": markup]) 222 | ``` 223 | 224 | ## 0.5.0 (2016-05-30) 225 | 226 | ### Message context 227 | 228 | The biggest change in this version is the addition of `context` in router handlers. 229 | It's also an API breaking change. 230 | 231 | Consider the old code below: 232 | 233 | ```swift 234 | func commandHandler(args: Arguments) { 235 | bot.respondAsync("Hello, \(bot.lastMessage.from.first_name)") { // OK 236 | 237 | print("Succesfully sent message to \(bot.lastMessage.from.first_name)!") 238 | // BAD: bot.lastMessage was overwritten by nextMessage() at this point 239 | // and now belongs to another chat and/or user! 240 | 241 | bot.respondAsync("Bye!") 242 | // BAD: bot.respondAsync here will send the message to wrong user 243 | // because it uses lastMessage internally! 244 | } 245 | } 246 | ``` 247 | 248 | You had to copy `lastMessage` before using it in async block, which was very error-prone: 249 | 250 | ```swift 251 | func commandHandler(args: Arguments) { 252 | let message = bot.lastMessage 253 | bot.respondAsync("Hello, \(message.from.first_name)") { // OK 254 | print("Succesfully sent message to \(message.from.first_name)!") // OK 255 | bot.respondAsync("Bye!") // STILL BAD, uses bot.lastMessage internally 256 | bot.sendMessage(message.chat.id, "Bye!") // OK 257 | } 258 | } 259 | ``` 260 | 261 | So, now router handlers have `context` parameter which contains: 262 | 263 | - `bot`: a reference to bot. 264 | - `message`: a copy of message. 265 | - `args`: command arguments which can be fetched word-by-word etc. 266 | - helper methods like `respondAsync`, `respondPrivately(groupText:)` etc. 267 | 268 | The code above now works as expected without any additional steps: 269 | 270 | ```swift 271 | func commandHandler(context: Context) { 272 | context.respondAsync("Hello, \(context.message.from.first_name)") { // OK 273 | print("Succesfully sent message to \(context.message.from.first_name)!") // OK 274 | context.respondAsync("Bye!") // OK 275 | bot.sendMessage(context.message.chat.id, "Bye!") // OK 276 | } 277 | } 278 | ``` 279 | > It's ok to use global `bot` variable, but `context.bot` is also available. It doesn't matter which one you use. 280 | 281 | The `hello-bot` and `word-reverse-bot` examples have been updated to use the new API. 282 | 283 | Some helper methods were added to `Context` for frequently used variables. 284 | For example, you can use: 285 | 286 | - `context.fromId` in place of `context.message.from.id` 287 | - `context.chatId` in place of `context.message.chat.id` 288 | - `context.privateChat` in place of `context.message.chat.type == .privateChat` 289 | 290 | ### Router can now work with any message types 291 | 292 | Another big change: router now accepts messages. You can do things like: 293 | 294 | ```swift 295 | router[.newChatMember] = newChatMember 296 | router[.leftChatMember] = leftChatMember 297 | router[.document] = onDocument 298 | etc 299 | 300 | func newChatMember(context: Context) throws { 301 | let message = context.message 302 | guard let newChatMember = message.new_chat_member where 303 | newChatMember.id == bot.user.id else { return } 304 | 305 | ...someone invited bot to chat... 306 | } 307 | ``` 308 | 309 | In addition to `partialMatch` handler there are two new fallback handlers: 310 | 311 | ```swift 312 | router.partialMatch = partialMatchHandler 313 | router.unknownCommand = unknownCommandHandler 314 | router.unsupportedContentType = unsupportedContentTypeHandler 315 | ``` 316 | 317 | They have a reasonable default implementations, but can be overridden. 318 | 319 | ### Other changes 320 | 321 | - `bot.lastCommand`, `bot.lastMessage` and `bot.lastUpdate` are no longer available. Added `bot.lastUpdateId` which can be used for debugging purposes. 322 | - Added generic `requestSync` and `requestAsync` requests which can be used for any requests. All other requests now use these functions internally. 323 | - All async request completion handlers now consistently return `(result, error)` tuple. Result type is different depending on request. 324 | - Supported `array of objects` as request return value, simplified `getUpdates` request. 325 | - Added `leaveChat` request. 326 | - `Bool` type now conforms to `JsonObject` and can be used as request result. See `leaveChat` for example. 327 | 328 | --------------------------------------------------------------------------------