Skip to content

rapierorg/telegram-bot-swift

Repository files navigation

Swift Chat Platform License Build Status

Chat • Changelog • Prerequisites • Getting started • Creating a new bot • Generating Xcode project • API overview • Debugging notes • Examples • Documentation • Support • License

SDK for creating Telegram Bots in Swift.

Sample projects:

Shopping list bot.

Word reverse bot.

Trivial bot:

import TelegramBotSDK

let bot = TelegramBot(token: "my token")
let router = Router(bot: bot)

router["greet"] = { context in
    guard let from = context.message?.from else { return false }
    context.respondAsync("Hello, \(from.firstName)!")
    return true
}

router[.newChatMembers] = { context in
    guard let users = context.message?.newChatMembers else { return false }
    for user in users {
        guard user.id != bot.user.id else { continue }
        context.respondAsync("Welcome, \(user.firstName)!")
    }
    return true
}

while let update = bot.nextUpdateSync() {
	try router.process(update: update)
}

fatalError("Server stopped due to error: \(bot.lastError)")

Telegram chat

Join our chat in Telegram: swiftsdkchat.

What's new

Release notes contain the significant changes in each release with migration notes.

Prerequisites

On OS X, use the latest Xcode 9 release.

On Linux, install Swift 4.2 or newer and libcurl4-openssl-dev package. Note that shopster-bot example won't build on Linux because GRDB doesn't support Linux yet, but otherwise the library should be functional.

Getting started

Please get familiar with the documentation on Telegram website:

Creating a new bot

In Telegram, add BotFather. Send him these commands:

/newbot
BotName
username_of_my_bot

BotFather will return a token.

Create a project for your bot:

mkdir hello-bot
cd hello-bot
swift package init --type executable

Create Package.swift:

// swift-tools-version:5.1
import PackageDescription

let package = Package(
    name: "hello-bot",
    products: [
        // Products define the executables and libraries produced by a package, and make them visible to other packages.
        .executable(
            name: "hello-bot",
            targets: ["hello-bot"]
        ),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(name: "TelegramBotSDK", url: "https://github.com/zmeyc/telegram-bot-swift.git", from: "2.0.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
        .target(
            name: "hello-bot",
            dependencies: ["TelegramBotSDK"]),
    ]
)

Create Sources/main.swift:

import Foundation
import TelegramBotSDK

let token = readToken(from: "HELLO_BOT_TOKEN")
let bot = TelegramBot(token: token)

while let update = bot.nextUpdateSync() {
    if let message = update.message, let from = message.from, let text = message.text {
        bot.sendMessageAsync(chatId: .chat(from.id),
                             text: "Hi \(from.firstName)! You said: \(text).\n")
    }
}

fatalError("Server stopped due to error: \(String(describing: bot.lastError))")

Do not commit your token to git!

readToken reads token from environment variable or from a file. So, either create an environment variable:

export HELLO_BOT_TOKEN='token'

Or save the token to a file and add the file to .gitignore:

echo token > HELLO_BOT_TOKEN

Build your bot:

swift build

And run it:

./.build/x86_64-apple-macosx10.10/debug/hello-bot

More details are available on Wiki: New Bot.

Generating Xcode project

It's easy:

swift package generate-xcodeproj

Open generated hello-bot.xcodeproj and switch the active scheme to the bottom one:

Don't forget to add your token to environment variables in Xcode (Scheme settings -> Run).

Press CMD-R to start the bot.

API overview

Type and request names

SDK type and request names closely mirror original Telegram ones.

Swift types and enums were added where appropriate:

if entity.type == .botCommand { ... }

In most cases raw methods accepting strings are also available. They can be used as fallbacks if required enum case is not added yet:

if entity.typeString == "bot_command" { ... }

To allow accessing fields which are still missing in SDK, every data type has json member with original json structure:

if entity.json["type"].stringValue == "bot_command" { ... }

All types conform to JsonConvertible protocol and can be created from json or serialized back to json. Use debugDescription method for human-readable json or description for json which can be sent to server.

Requests

Sync and Async

Request names closely mirror Telegram ones, but have two versions: synchronous and asynchronous with method suffixes Sync and Async correspondingly.

  • Synchronous methods block until the operation is completed.
let fromId: ChatId = .chat(12345678) // your user id
bot.sendMessageSync(fromId, "Hello!") // blocks until the message is sent
bot.sendMessageSync(fromId, "Bye.")

These methods return a server response or nil in case of error. If nil is returned, details can be obtained by querying bot.lastError.

guard let sentMessage = bot.sendMessageSync(fromId, "Hello") else {
    fatalError("Unable to send message: \(bot.lastError.unwrapOptional)")
}

Do not use synchronous methods in real apps because they're slow. Use them when debugging or for experimenting in REPL. More details: Using Swift REPL for calling API methods

  • Asynchronous methods accept an optional completion handler which will be called when operation is completed.

Completion handler is called on main thread by default.

bot.sendMessageAsync(fromId, "Hello!") { result, error in
  // message sent!
  bot.sendMessageAsync(fromId, "Bye.")
}
// execution continues immediately

In completion handler result contains the server response or nil in case of error. Details can be obtained by querying error.

For simplicity, it's possible to synchronously process messages, but respond asynchronously to avoid blocking the processing of the next message. So, a typical bot's main loop can look like this:

while let update = bot.nextUpdateSync() {
  // process the message and call Async methods
}

Request parameters

Parameter names should be specified explicitly in most cases:

bot.sendLocationAsync(chat_id: chatId, latitude: 50.4501, longitude: 30.5234)

Exception to this are sendMessageSync/Async and respondSync/Async functions which are used very often. Parameter names can be omitted in them:

bot.sendMessageAsync(chatId: chatId, text: "Text")
bot.sendMessageAsync(chatId, "Text") // will also work

Optional parameters can also be passed:

let markup = ForceReply()
bot.sendMessageAsync(chatId: chatId, text: "Force reply",
    reply_markup: markup, disable_notification: true)

If you ever encounter a situation when parameter hasn't been added to method signature yet, you can pass a dictionary with any parameters at the end of parameter list:

let markup = ForceReply()
bot.sendMessageAsync(chatId: chatId, text: "Force reply",
    ["reply_markup": markup, "disable_notification": true])

It's also possible to set default parameter values for a request:

bot.defaultParameters["sendMessage"] = ["disable_notification": true]

In dictionaries nil values will be treated as no value and won't be sent to Telegram server.

Available requests

Check TelegramBot/Requests subdirectory for a list of available requests.

If you find a missing request, please create a ticket and it will be added. Until then, an arbitrary unsupported endpoint can be called like this:

let user: User? = requestSync("sendMessage", ["chat_id": chatId, "text": text])

Or async version:

requestAsync("sendMessage", ["chat_id": chatId, "text": text]) { (result: User?, error: DataTaskError?) -> () in
    ...
}

These methods automatically deserialize the json response.

Explicitly specifying result type is important. Result type should conform to JsonConvertible protocol. Bool and Int already conform to JsonConvertible.

JSON class itself also conforms to JsonConvertible, so you can request a raw json if needed:

let user: JSON? = requestSync("sendMessage", ["chat_id": chatId, "text": text])

Routing

Router maps text commands and other events to their handler functions and helps parsing command arguments.

let router = Router(bot)
router["command1"] = handler1
router["command2"] = handler2
router[.event] = handler3
...
router.process(update: update)

Multiple commands can be specified in a single rule:

router["Full Command Name", "command"] = handler

Multiword commands are also supported:

router["list add"] = onListAdd
router["list remove"] = onListRemove

Routers can be chained. This helps creating a context-sensitive routers with fallback to a global router.

router1.unmatched = router2.handler

Handlers

Handlers take Context argument and return Bool.

  • If handler returns true, command matching stops.
  • If handler returns false, other paths will be matched.

So, in handler check preconditions and return false if they aren't satisfied:

router["reboot"] = { context in
    guard let fromId = context.fromId where isAdmin(fromId) else { return false }

    context.respondAsync("I will now reboot the PC.") { _ in
        reboot()
    }

    return true
}

Handler functions can be marked as throws and throw exceptions. Router won't process them and will simply pass the exceptions to caller.

Context is a request context, it contains:

  • bot - a reference to the bot.
  • update - current Update structure.
  • message - convenience method for accessing update.message. If update.message is nil, fallbacks to update.edited_message, then to update.callback_query?.message.
  • command - command without slash.
  • slash - true, if command was prefixed with a slash. Useful if you want to skip commands not starting with slash in group chats.
  • args - command arguments scanner.
  • properties - context sensitive properties. Pass them to process method:
var properties = [String: AnyObject]()
properties["myField"] = myValue
try router.process(update: update, properties: properties)

And use them in handlers:

func myHandler(context: Context) -> Bool {
    let myValue = context.properties["myField"] as? MyValueType
    // ...
}

Or make a Context category for easier access to your properties, for example:

extension Context {
    var session: Session { return properties["session"] as! Session }
}

Context also contains a few helper methods and variables:

  • privateChat - true, if this is a private chat with bot, false for all group chat types.
  • chatId - shortcut for message?.chat.id. If message is nil, tries to retrieve chatId from other Update fields.
  • fromId - shortcut for message?.from?.id. If message is nil, tries to retrieve fromId from other Update fields.
  • respondAsync, respondSync - works as sendMessage(chatId, ...)
  • respondPrivatelyAsync/Sync("text", groupText: "text") - respond to user privately, sending a short message to the group if this was a group chat. For example:
context.respondPrivatelyAsync("Command list: ...",
    groupText: "Please find a list of commands in a private message.")
  • reportErrorAsync/Sync(text: "User text", errorDescription: "Detailed error description for administrator") - sends a short message to user and prints detailed error description to a console. text parameter can be omitted, in which case user will receive a generic error message.

Text commands

Router can match text commands:

router["start"] = onStart

Command name is processed differently in private and group chats:

  • In private chats slash is optional. start matches /start as well as start.
  • It group chats 'start' only matches /start.

This can be overridden. The following line will require slash even in private chats:

router["start", .slashRequired] = onStart

Router is case-insensitive by default. To make it case-sensitive, pass .caseSensitive option:

router["command", .caseSensitive] = handler

Multiple options can be passed:

router["command", [.slashRequired, .caseSensitive]] = handler

In Telegram group chats, user can append bot name to a command, for example: /greet@hello_bot. Router takes care of removing the @hello_bot part from command name automatically.

Text commands with arguments

Words can be captured and then processed by using scanWord method.

router["two_words"] = { context in
    let word1 = context.args.scanWord()
    let word2 = context.args.scanWord()
}

Array of words can be captured using scanWords:

router["words"] = { context in
    let words = context.args.scanWords() // returns [String] array
}

Numbers can be captured using scanInt, scanInt64 and scanDouble. restOfString captures the remainder as a single string.

router["command"] = { context in
    let value1 = context.args.scanInt()
    let value2 = context.args.scanDouble()
    let text = context.args.scanRestOfString()
}

It's also possible to directly access NSScanner used for scanning arguments: context.args.scanner.

Handler is expected to read all the arguments, otherwise user will see a warning: Part of your input was ignored: text

So, for example, if there's a command swap which expects two arguments but user types: /swap aaa bbb ccc, he will see:

bbb aaa
Part of your input was ignored: ccc

A possible way to avoid the warning is to skip unneeded arguments by calling context.args.skipRestOfString().

Also, the warning can be overridden:

router.partialMatch = { context in
    context.respondAsync("Part of your input was ignored: \(context.args.scanRestOfString())")
    return true
}

Other events

Router can handle other event types as well. For example, when new user joins the chat, .new_chat_member path will be triggered:

router[.new_chat_member] = { context in
    guard let users = context.message?.newChatMembers else { return false }
    for user in users {
        guard user.id != bot.user.id else { return false }
        context.respondAsync("Welcome, \(user.firstName)!")
    }
    return true
}

Check TelegramBot/Router/ContentType.swift file for a complete list of events supported by Router.

Handling unmatched paths

If no paths were matched, router will call it's unmatched handler, which will print "Command not found" by default. This can be overridden by setting an explicit handler:

router.unmatched = { context in
    // Do something else with context.args
    return true
}

Debugging notes

In debugger you may want to dump the contents of a json structure, but debugDescription loses it's formatting.

prettyPrint helper function allows printing any JsonConvertible with indentation:

let user: User
user.prettyPrint()

bot.sendMessageSync(fromId, "Hello!")?.prettyPrint()

Examples

There are 3 example projects available:

  • Examples/hello-bot - a trivial bot which responds to /greet command and greets users who join the chat.

  • Examples/word-reverse-bot - demonstrates how to handle start and stop requests, keep session state and parse command arguments. Behaves differently in private and group chats. Uses a router and a controller.

  • Examples/shopster-bot - maintains a shopping list using sqlite3 database. Allows creating shared shopping lists in group chats. GRDB library is used for working with database.

Details on compiling and running the bots are available on Wiki: Building and running the example projects.

Documentation

Additional documentation is available on Telegram Bot Swift SDK Wiki.

Check Examples/ for sample bot projects.

This SDK is a work in progress, expect the API to change very often.

Need help?

Please submit an issue on Github.

If you miss a specific feature, please create an issue and it will be prioritized. Pull Requests are also welcome.

Talk with other developers in our Telegram chat: swiftsdkchat.

Happy coding!

License

Apache License Version 2.0 with Runtime Library Exception. Please see LICENSE.txt for more information.