В предыдущей части был написан бот, который по заданному имени или части имени находит подходящие и показывает пользователю в отдельных сообщениях. В этот раз немного улучшим его: улучшим форматирование, добавим inline- режим и реализуем пару команд.

Улучшаем форматирование

Скажу сразу, что Telegram умеет в HTML и Markdown. Но, к сожалению, очень ограничено: если посмотреть документацию, то поддерживается только полужирное начертание, курсив, моноширинный шрифт и ссылки: Markdown:

*полужирный*
_курсив_
[ссылка](http://www.example.com/)
`строчный моноширинный`
```text
блочный моноширинный (можно писать код)
```

HTML:

<b>полужирный</b>, <strong>полужирный</strong>
<i>курсив</i>
<a href="http://www.example.com/">ссылка</a>
<code>строчный моноширинный</code>
<pre>блочный моноширинный (можно писать код)</pre>

Лично мне больше по душе Markdown. К счастью, библиотека, которую я использую для разработки бота так же умеет в HTML и Markdown. Немного улучшим наши сообщения:

  1. Название заклинания будем отображать полужирным
  2. Курсивом добавим информацию про уровень заклинания, школу, класс, время действия и прочее
  3. Описание заклинания оставим обычным текстом

То есть сообщение будет выглядеть (в исходном виде) примерно так:

*Alter Self*
*Level* _2_
*School* _T_
*Time* _1 action_
*Range* _Self_
*Components* _V, S_
*Duration* _Concentration, up to 1 hour_
*Classes* _Sorcerer, Wizard_
*Roll* _1d6+1_

You assume a different form. When you cast the spell, 
choose one of the following options, the effects of 
which last for the duration of the spell. 
While the spell lasts, you can end one option as 
an action to gain the benefits of a different one.

Aquatic Adaptation: You adapt your body to an aquatic environment, 
sprouting gills, and growing webbing between your fingers. 
You can breathe underwater and gain 
a swimming speed equal to your walking speed.

Change Appearance: You transform your appearance. 
You decide what you look like, including your height, 
weight, facial features, sound of your voice, hair length, 
coloration, and distinguishing characteristics, if any. 
You can make yourself appear as a member of another race, 
though none of your statistics change. 
You also don't appear as a creature of a different size than you, 
and your basic shape stays the same, if you're bipedal, 
you can't use this spell to become quadrupedal, for instance. 
At any time for the duration of the spell, 
you can use your action to change your appearance in this way again.

Natural Weapons: You grow claws, fangs, spines, horns, 
or a different natural weapon of your choice. 
Your unarmed strikes deal 1d6 bludgeoning, piercing, 
or slashing damage, as appropriate to the natural weapon you chose, 
and you are proficient with you unarmed strikes. 
Finally, the natural weapon is magic and you have a +1 bonus 
to the attack and damage rolls you make using it.

В общем - ничего сложного, правда? Для того, чтобы отправить сообщение в Markdown-формате необходимо сказать об этом telegram-у при помощи свойства ParseMode структуры MessageConfig:

text := fmt.Sprintf(
       "*%s*\n" +
       "*Level* _%v_\n" +
       "*School* _%s_\n" +
       "*Time* _%s_\n" +
       "*Range* _%s_\n" +
       "*Components* _%s_\n" +
       "*Duration* _%s_\n" +
       "*Classes* _%s_\n" +
       "*Roll* _%s_\n" +
       "%s",
       spell.Name,
       spell.Level,
       spell.School,
       spell.Time,
       spell.Range,
       spell.Components,
       spell.Duration,
       spell.Classes,
       strings.Join(spell.Rolls, ", "),
       strings.Join(spell.Texts, "\n"))

msg := tgbotapi.NewMessage(update.Message.Chat.ID, text)
msg.ParseMode = "markdown"
bot.Send(msg)

Можно немного упороться и сделать дополнительную предобработку: вместо школы или компонент выводить текстовое название школы и компоненты, но оставим это на самостоятельное изучение - сейчас можно обойтись без этого. Как мне кажется, выглядит довольно пристойно: 2016-10-08_23-57-20.png

inline боты

Я не буду рассказывать что это такое - можете посмотреть в документации. Вкратце - это штука позволяет обращаться к боту из любого чата telegram и отправить ответ бота в этот чат (или в приватный чат - если это реализовано в боте). Для реализации этой функциональности в telegram-bot-api уже есть нужная функция: NewInlineQueryResultArticleMarkdown принимающая три параметра:

  1. id - идентификатор сообщения (толком не понял, для чего используется)
  2. title - текст, который будет отображаться в выпадающем списке
  3. messageText - ответ бота, если пользователь нажмёт на этот элемент списка.

Сделаем так: если в inline режиме что-то пишут боту - считаем это имя заклинания и находим всё подходящие и их описания. Далее отправляем весь список подходящих заклинаний и ждём действий пользователя. Но для начала необходимо изменить код, который обрабатывает получение новых обновлений от telegram: при получении inline сообщения свойство update.Message не будет означено, вместо него будет получено update.InlineQuery и это необходим корректно обрабатывать:

if update.Message == nil && update.InlineQuery != nil {
    // код для inline режима
} else {
    // код для "обычного" режима
}

ОК, добавим обработку inline сообщений:

query := update.InlineQuery.Query
filteredSpells := Filter(spells.Spells, func(spell Spell) bool {
       return strings.Index(strings.ToLower(spell.Name), strings.ToLower(query)) >= 0
})

var articles []interface{}
if len(filteredSpells) == 0 {
       msg := tgbotapi.NewInlineQueryResultArticleMarkdown(update.InlineQuery.ID, "No one spells matches", "No one spells matches")
       articles = append(articles, msg)
} else {
       var i = 0 // добавим счётчик заклинаний, чтобы не показывать больше 10
       for _, spell := range(filteredSpells) {
              text := fmt.Sprintf(
                     "*%s*\n" +
                     "*Level* _%v_\n" +
                     "*School* _%s_\n" +
                     "*Time* _%s_\n" +
                     "*Range* _%s_\n" +
                     "*Components* _%s_\n" +
                     "*Duration* _%s_\n" +
                     "*Classes* _%s_\n" +
                     "*Roll* _%s_\n" +
                     "%s",
                     spell.Name,
                     spell.Level,
                     spell.School,
                     spell.Time,
                     spell.Range,
                     spell.Components,
                     spell.Duration,
                     spell.Classes,
                     strings.Join(spell.Rolls, ", "),
                     strings.Join(spell.Texts, "\n"))

              msg := tgbotapi.NewInlineQueryResultArticleMarkdown(spell.Name, spell.Name, text)
              articles = append(articles, msg)
              if i >= 10 {
                     break
              }
       }
}

inlineConfig := tgbotapi.InlineConfig{
       InlineQueryID: update.InlineQuery.ID,
       IsPersonal:    true,
       CacheTime:     0,
       Results: articles,
}
_, err := bot.AnswerInlineQuery(inlineConfig)
if err != nil {
       log.Println(err)
}

Запускаем бота, пишем ему @dndspellsbot friendship и ничего не происходит. Потому что необходимо договориться об inline боте с @BotFather: идём в чатик с ним и делаем следующее:

  1. Вводим команду /setinline
  2. Выбираем DndSpellsBot
  3. Пишем сообщение, которое будет отображаться в качестве подсказки

2016-10-09_00-33-01.png Снова запустим бота и попробуем обратиться к нему: 2016-10-09_01-17-00.png Похоже, всё работает как надо. Осталось добавить команды

Команды

Команда - особо сформированное сообщение боту. Всегда начинаются с / и длиной не более 32 символов. Имеют следующий вид:

/command [optional] [argument]

Подробнее можно посмотреть в документации. Добавим боту возможность фильтрации заклинаний только для определённого класса. Для этого будем использовать команду /setclass. Имя класса будем выбирать на inline- клавиатуре. Во- первых, необходимо сказать @BotFather-у, что бот умеет принимать команды отправив ему команду /setcommands. @BotFather предложит описать команды и на этом пока всё: 2016-10-10_23-25-06.png Во-вторых, необходимо в логике бота отличать команду от обычного сообщения. telegram-bot-api так же позволяет делать это довольно просто при помощи метода Command() входящего сообщения:

// Если сообщение - не команда, то Command() будет пустой строкой,
// иначе - текст команды
command := update.Message.Command()
if command == "" {
    // Здесь логика для "обычных" сообщений
} else {
    // Здесь - для команд
}

Можно сделать просто - параметром к команде передавать имя класса. А можно воспользоваться возможностями мессенджера и показать пользователю список доступных классов в виде кнопок: 2016-10-14_08-21-36.png Сделать это несложно (когда знаешь как делать):

switch command {
case "setclass":
       msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Select your class")

       keyboard := tgbotapi.InlineKeyboardMarkup{}
       for _, class := range classes {
              var row []tgbotapi.InlineKeyboardButton
              btn := tgbotapi.NewInlineKeyboardButtonData(class, class)
              row = append(row, btn)
              keyboard.InlineKeyboard = append(keyboard.InlineKeyboard, row)
       }

       msg.ReplyMarkup = keyboard
       bot.Send(msg)
}

В-третьих - запомним, по идентификатору пользователя, что он хочет получать заклинания для выбранного класса (для простоты - создадим map[int]string):

// Перед функцие main добави наш словарь
var classesMap map[int]string

В цикле обработки обновлений чата добавим обработку когда update.Message не означен, но есть update.CallbackQuery - ответ от inline-клавиатуры:

if update.CallbackQuery != nil {
    class := update.CallbackQuery.Data
    classesMap[update.CallbackQuery.From.ID] = class
    bot.Send(tgbotapi.NewMessage(update.CallbackQuery.Message.Chat.ID, 
        "Ok, I remember"))
}

И, в-четвертых, нам необходимо учитывать этот класс в наших запросах на поиск заклинаний:

filteredSpells := Filter(spells.Spells, func(spell Spell) bool {
       class, ok := classesMap[update.Message.From.ID]
       classCond := true
       if ok {
              classCond = strings.Index(strings.ToLower(spell.Classes), strings.ToLower(class)) >= 0
       }
       return strings.Index(strings.ToLower(spell.Name), strings.ToLower(query)) >= 0 && classCond
})

Команду для очистки класса оставим на самостоятельную реализацию :) Полный исходный код можно найти на bitbucket.