telebot/layout/layout.go

503 lines
12 KiB
Go
Raw Normal View History

2020-09-05 22:29:24 +00:00
package layout
import (
"bytes"
"encoding/json"
2020-09-05 22:29:24 +00:00
"io/ioutil"
2020-10-11 13:35:35 +00:00
"log"
2020-09-25 20:17:49 +00:00
"sync"
2020-09-05 22:29:24 +00:00
"text/template"
"github.com/goccy/go-yaml"
tele "gopkg.in/tucnak/telebot.v3"
)
type (
2020-11-04 20:50:09 +00:00
// Layout provides an interface to interact with the layout,
// parsed from the config file and locales.
2020-09-05 22:29:24 +00:00
Layout struct {
2020-09-25 20:17:49 +00:00
pref *tele.Settings
mu sync.RWMutex // protects ctxs
ctxs map[tele.Context]string
funcs template.FuncMap
2020-09-26 15:48:49 +00:00
buttons map[string]Button
2020-09-25 20:17:49 +00:00
markups map[string]Markup
results map[string]Result
2020-09-25 20:17:49 +00:00
locales map[string]*template.Template
2020-10-08 19:37:57 +00:00
*Config
2020-09-05 22:29:24 +00:00
}
2020-11-04 20:50:09 +00:00
// Button is a shortcut for tele.Btn.
2020-09-26 15:48:49 +00:00
Button = tele.Btn
2020-09-05 22:29:24 +00:00
2020-11-04 20:50:09 +00:00
// Markup represents layout-specific markup to be parsed.
2020-09-26 15:48:49 +00:00
Markup struct {
inline *bool
keyboard *template.Template
ResizeKeyboard *bool `yaml:"resize_keyboard,omitempty"` // nil == true
ForceReply bool `yaml:"force_reply,omitempty"`
OneTimeKeyboard bool `yaml:"one_time_keyboard,omitempty"`
RemoveKeyboard bool `yaml:"remove_keyboard,omitempty"`
Selective bool `yaml:"selective,omitempty"`
2020-09-05 22:29:24 +00:00
}
// Result represents layout-specific result to be parsed.
Result struct {
result *template.Template
ResultBase `yaml:",inline"`
Markup string `yaml:"markup"`
}
// ResultBase represents layout-specific result's base to be parsed.
ResultBase struct {
tele.ResultBase `yaml:",inline"`
Content ResultContent `yaml:"content"`
}
// ResultContent represents any kind of InputMessageContent and implements it.
ResultContent map[string]interface{}
2020-09-05 22:29:24 +00:00
)
2020-11-04 20:50:09 +00:00
// New reads and parses the given layout file.
2020-09-05 22:29:24 +00:00
func New(path string) (*Layout, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
2020-09-25 20:17:49 +00:00
lt := Layout{
ctxs: make(map[tele.Context]string),
funcs: make(template.FuncMap),
}
for k, v := range funcs {
lt.funcs[k] = v
}
2020-09-05 22:29:24 +00:00
return &lt, yaml.Unmarshal(data, &lt)
}
var funcs = template.FuncMap{
// Built-in blank and helper functions.
"locale": func() string { return "" },
"config": func(string) string { return "" },
"text": func(string) string { return "" },
}
2020-09-25 20:17:49 +00:00
2020-11-04 20:50:09 +00:00
// AddFunc adds the given function to the template FuncMap.
// Note: to make it come into effect, always add functions before New().
2020-09-25 20:17:49 +00:00
func AddFunc(key string, fn interface{}) {
funcs[key] = fn
}
2020-11-04 20:50:09 +00:00
// AddFuncs extends the template FuncMap with the given one.
// Note: to make it come into effect, always add functions before New().
2020-09-25 20:17:49 +00:00
func AddFuncs(fm template.FuncMap) {
for k, v := range fm {
funcs[k] = v
}
}
2020-11-04 20:50:09 +00:00
// Settings returns built telebot Settings required for bot initialising.
//
// settings:
// url: (custom url if needed)
// token: (not recommended)
// updates: (chan capacity)
// locales_dir: (optional)
// token_env: (token env var name, example: TOKEN)
// parse_mode: (default parse mode)
// long_poller: (long poller settings)
// webhook: (or webhook settings)
//
// Usage:
// lt, err := layout.New("bot.yml")
// b, err := tele.NewBot(lt.Settings())
// // That's all!
//
2020-09-05 22:29:24 +00:00
func (lt *Layout) Settings() tele.Settings {
if lt.pref == nil {
panic("telebot/layout: settings is empty")
}
return *lt.pref
}
2021-07-22 17:43:32 +00:00
// Default returns a simplified layout instance with pre-defined locale.
// It's useful when you have no need in localization and don't want to pass
// context each time you use layout functions.
func (lt *Layout) Default(locale string) *DefaultLayout {
return &DefaultLayout{lt: lt, locale: locale}
}
2021-07-07 10:28:41 +00:00
// Locales returns all presented locales.
func (lt *Layout) Locales() []string {
var keys []string
for k := range lt.locales {
keys = append(keys, k)
}
return keys
}
2020-11-04 20:50:09 +00:00
// Locale returns the context locale.
func (lt *Layout) Locale(c tele.Context) (string, bool) {
lt.mu.RLock()
defer lt.mu.RUnlock()
locale, ok := lt.ctxs[c]
return locale, ok
}
// SetLocale allows you to change a locale for the passed context.
func (lt *Layout) SetLocale(c tele.Context, locale string) {
lt.mu.Lock()
lt.ctxs[c] = locale
lt.mu.Unlock()
}
// Text returns a text, which locale is dependent on the context.
// The given optional argument will be passed to the template engine.
//
// Example of en.yml:
// start: Hi, {{.FirstName}}!
//
// Usage:
2021-07-22 17:43:32 +00:00
// func onStart(c tele.Context) error {
2020-11-04 20:50:09 +00:00
// return c.Send(lt.Text(c, "start", c.Sender()))
// }
//
2020-09-25 20:17:49 +00:00
func (lt *Layout) Text(c tele.Context, k string, args ...interface{}) string {
2020-09-26 11:03:01 +00:00
locale, ok := lt.Locale(c)
2020-09-25 20:17:49 +00:00
if !ok {
2020-09-05 22:29:24 +00:00
return ""
}
return lt.TextLocale(locale, k, args...)
2020-09-25 20:17:49 +00:00
}
// TextLocale returns a localized text processed with text/template engine.
2020-11-04 20:50:09 +00:00
// See Text for more details.
func (lt *Layout) TextLocale(locale, k string, args ...interface{}) string {
2020-09-25 20:17:49 +00:00
tmpl, ok := lt.locales[locale]
2020-09-05 22:29:24 +00:00
if !ok {
return ""
}
var arg interface{}
if len(args) > 0 {
arg = args[0]
}
var buf bytes.Buffer
2020-09-25 20:17:49 +00:00
if err := lt.template(tmpl, locale).ExecuteTemplate(&buf, k, arg); err != nil {
2020-10-11 13:35:35 +00:00
log.Println("telebot/layout:", err)
2020-09-05 22:29:24 +00:00
}
2020-09-25 20:17:49 +00:00
2020-09-05 22:29:24 +00:00
return buf.String()
}
2020-11-04 20:50:09 +00:00
// Callback returns casted to CallbackEndpoint button, which mostly
// useful for handlers registering.
//
// Example:
// // Handling settings button
2021-07-22 17:43:32 +00:00
// b.Handle(lt.Callback("settings"), onSettings)
2020-11-04 20:50:09 +00:00
//
2020-11-02 14:49:50 +00:00
func (lt *Layout) Callback(k string) tele.CallbackEndpoint {
btn, ok := lt.buttons[k]
if !ok {
return nil
}
2020-09-26 15:48:49 +00:00
return &btn
}
2020-11-04 20:50:09 +00:00
// Button returns a button, which locale is dependent on the context.
// The given optional argument will be passed to the template engine.
//
// buttons:
// item:
// unique: item
// callback_data: {{.ID}}
// text: Item #{{.Number}}
//
// Usage:
// btns := make([]tele.Btn, len(items))
// for i, item := range items {
// btns[i] = lt.Button(c, "item", struct {
// Number int
// Item Item
// }{
// Number: i,
// Item: item,
// })
// }
//
// m := b.NewMarkup()
// m.Inline(m.Row(btns...))
// // Your generated markup is ready.
//
2020-11-02 14:49:50 +00:00
func (lt *Layout) Button(c tele.Context, k string, args ...interface{}) *tele.Btn {
locale, ok := lt.Locale(c)
if !ok {
return nil
}
return lt.ButtonLocale(locale, k, args...)
}
// ButtonLocale returns a localized button processed with text/template engine.
2020-11-04 20:50:09 +00:00
// See Button for more details.
2020-11-02 14:49:50 +00:00
func (lt *Layout) ButtonLocale(locale, k string, args ...interface{}) *tele.Btn {
btn, ok := lt.buttons[k]
if !ok {
return nil
}
var arg interface{}
if len(args) > 0 {
arg = args[0]
}
data, err := yaml.Marshal(btn)
if err != nil {
log.Println("telebot/layout:", err)
return nil
}
tmpl, err := lt.template(template.New(k), locale).Parse(string(data))
if err != nil {
log.Println("telebot/layout:", err)
return nil
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, arg); err != nil {
log.Println("telebot/layout:", err)
return nil
}
if err := yaml.Unmarshal(buf.Bytes(), &btn); err != nil {
log.Println("telebot/layout:", err)
return nil
}
return &btn
}
2020-11-04 20:50:09 +00:00
// Markup returns a markup, which locale is dependent on the context.
// The given optional argument will be passed to the template engine.
//
// buttons:
// settings: 'Settings'
// markups:
// menu:
// - [settings]
//
// Usage:
2021-07-22 17:43:32 +00:00
// func onStart(c tele.Context) error {
2020-11-04 20:50:09 +00:00
// return c.Send(
// lt.Text(c, "start"),
// lt.Markup(c, "menu"),
// )
2020-11-04 20:50:09 +00:00
// }
//
2020-09-25 20:17:49 +00:00
func (lt *Layout) Markup(c tele.Context, k string, args ...interface{}) *tele.ReplyMarkup {
locale, ok := lt.Locale(c)
if !ok {
return nil
}
return lt.MarkupLocale(locale, k, args...)
}
// MarkupLocale returns a localized markup processed with text/template engine.
2020-11-04 20:50:09 +00:00
// See Markup for more details.
func (lt *Layout) MarkupLocale(locale, k string, args ...interface{}) *tele.ReplyMarkup {
2020-09-25 20:17:49 +00:00
markup, ok := lt.markups[k]
2020-09-05 22:29:24 +00:00
if !ok {
return nil
}
var arg interface{}
if len(args) > 0 {
arg = args[0]
}
var buf bytes.Buffer
2020-09-25 20:17:49 +00:00
if err := lt.template(markup.keyboard, locale).Execute(&buf, arg); err != nil {
2020-10-11 13:35:35 +00:00
log.Println("telebot/layout:", err)
2020-09-05 22:29:24 +00:00
}
2020-09-26 15:48:49 +00:00
r := &tele.ReplyMarkup{}
if *markup.inline {
2020-09-05 22:29:24 +00:00
if err := yaml.Unmarshal(buf.Bytes(), &r.InlineKeyboard); err != nil {
2020-10-11 13:35:35 +00:00
log.Println("telebot/layout:", err)
2020-09-05 22:29:24 +00:00
}
} else {
2020-09-26 15:48:49 +00:00
r.ResizeKeyboard = markup.ResizeKeyboard == nil || *markup.ResizeKeyboard
r.ForceReply = markup.ForceReply
r.OneTimeKeyboard = markup.OneTimeKeyboard
r.RemoveKeyboard = markup.RemoveKeyboard
r.Selective = markup.Selective
2020-09-05 22:29:24 +00:00
if err := yaml.Unmarshal(buf.Bytes(), &r.ReplyKeyboard); err != nil {
2020-10-11 13:35:35 +00:00
log.Println("telebot/layout:", err)
2020-09-05 22:29:24 +00:00
}
}
2020-09-26 15:48:49 +00:00
return r
2020-09-05 22:29:24 +00:00
}
2020-09-25 20:17:49 +00:00
// Result returns an inline result, which locale is dependent on the context.
// The given optional argument will be passed to the template engine.
//
// results:
// type: article
// id: '{{ .ID }}'
// title: '{{ .Title }}'
// description: '{{ .Description }}'
// message_text: '{{ .Content }}'
// thumb_url: '{{ .PreviewURL }}'
//
// Usage:
// func onQuery(c tele.Context) error {
// results := make(tele.Results, len(articles))
// for i, article := range articles {
// results[i] = lt.Result(c, "article", article)
// }
// return c.Answer(&tele.QueryResponse{
// Results: results,
// CacheTime: 100,
// })
// }
//
func (lt *Layout) Result(c tele.Context, k string, args ...interface{}) tele.Result {
locale, ok := lt.Locale(c)
if !ok {
return nil
}
return lt.ResultLocale(locale, k, args...)
}
// ResultLocale returns a localized result processed with text/template engine.
// See Result for more details.
func (lt *Layout) ResultLocale(locale, k string, args ...interface{}) tele.Result {
result, ok := lt.results[k]
if !ok {
return nil
}
var arg interface{}
if len(args) > 0 {
arg = args[0]
}
var buf bytes.Buffer
if err := lt.template(result.result, locale).Execute(&buf, arg); err != nil {
log.Println("telebot/layout:", err)
}
var (
data = buf.Bytes()
base ResultBase
r tele.Result
)
if err := yaml.Unmarshal(data, &base); err != nil {
log.Println("telebot/layout:", err)
}
switch base.Type {
case "article":
r = &tele.ArticleResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "audio":
r = &tele.AudioResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "contact":
r = &tele.ContactResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "document":
r = &tele.DocumentResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "gif":
r = &tele.GifResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "location":
r = &tele.LocationResult{ResultBase: base.ResultBase}
if err := json.Unmarshal(data, &r); err != nil {
log.Println("telebot/layout:", err)
}
case "mpeg4_gif":
r = &tele.Mpeg4GifResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "photo":
r = &tele.PhotoResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "venue":
r = &tele.VenueResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "video":
r = &tele.VideoResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "voice":
r = &tele.VoiceResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
case "sticker":
r = &tele.StickerResult{ResultBase: base.ResultBase}
if err := yaml.Unmarshal(data, r); err != nil {
log.Println("telebot/layout:", err)
}
default:
log.Println("telebot/layout: unsupported inline result type")
return nil
}
if base.Content != nil {
r.SetContent(base.Content)
}
if result.Markup != "" {
markup := lt.MarkupLocale(locale, result.Markup)
r.SetReplyMarkup(markup.InlineKeyboard)
}
return r
}
2020-09-25 20:17:49 +00:00
func (lt *Layout) template(tmpl *template.Template, locale string) *template.Template {
funcs := make(template.FuncMap)
// Redefining built-in blank functions
funcs["config"] = lt.String
funcs["text"] = func(k string) string { return lt.TextLocale(locale, k) }
2020-09-25 20:17:49 +00:00
funcs["locale"] = func() string { return locale }
return tmpl.Funcs(funcs)
}
// IsInputMessageContent implements telebot.InputMessageContent.
func (ResultContent) IsInputMessageContent() bool {
return true
}