init
This commit is contained in:
122
layout/config.go
Normal file
122
layout/config.go
Normal file
@ -0,0 +1,122 @@
|
||||
package layout
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
tele "git.belvedersky.ru/common/telebot"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Config represents typed map interface related to the "config" section in layout.
|
||||
type Config struct {
|
||||
v *viper.Viper
|
||||
}
|
||||
|
||||
// Unmarshal parses the whole config into the out value. It's useful when you want to
|
||||
// describe and to pre-define the fields in your custom configuration struct.
|
||||
func (c *Config) Unmarshal(v interface{}) error {
|
||||
return c.v.Unmarshal(v)
|
||||
}
|
||||
|
||||
// UnmarshalKey parses the specific key in the config into the out value.
|
||||
func (c *Config) UnmarshalKey(k string, v interface{}) error {
|
||||
return c.v.UnmarshalKey(k, v)
|
||||
}
|
||||
|
||||
// Get returns a child map field wrapped into Config.
|
||||
// If the field isn't a map, returns nil.
|
||||
func (c *Config) Get(k string) *Config {
|
||||
v := c.v.Sub(k)
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return &Config{v: v}
|
||||
}
|
||||
|
||||
// Slice returns a child slice of objects wrapped into Config.
|
||||
// If the field isn't a slice, returns nil.
|
||||
func (c *Config) Slice(k string) (slice []*Config) {
|
||||
a, ok := c.v.Get(k).([]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := range a {
|
||||
m, ok := a[i].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
v := viper.New()
|
||||
v.MergeConfigMap(m)
|
||||
slice = append(slice, &Config{v: v})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// String returns a field casted to the string.
|
||||
func (c *Config) String(k string) string {
|
||||
return c.v.GetString(k)
|
||||
}
|
||||
|
||||
// Int returns a field casted to the int.
|
||||
func (c *Config) Int(k string) int {
|
||||
return c.v.GetInt(k)
|
||||
}
|
||||
|
||||
// Int64 returns a field casted to the int64.
|
||||
func (c *Config) Int64(k string) int64 {
|
||||
return c.v.GetInt64(k)
|
||||
}
|
||||
|
||||
// Float returns a field casted to the float64.
|
||||
func (c *Config) Float(k string) float64 {
|
||||
return c.v.GetFloat64(k)
|
||||
}
|
||||
|
||||
// Bool returns a field casted to the bool.
|
||||
func (c *Config) Bool(k string) bool {
|
||||
return c.v.GetBool(k)
|
||||
}
|
||||
|
||||
// Duration returns a field casted to the time.Duration.
|
||||
// Accepts number-represented duration or a string in 0nsuµmh format.
|
||||
func (c *Config) Duration(k string) time.Duration {
|
||||
return c.v.GetDuration(k)
|
||||
}
|
||||
|
||||
// ChatID returns a field casted to the ChatID.
|
||||
// The value must be an integer.
|
||||
func (c *Config) ChatID(k string) tele.ChatID {
|
||||
return tele.ChatID(c.Int64(k))
|
||||
}
|
||||
|
||||
// Strings returns a field casted to the string slice.
|
||||
func (c *Config) Strings(k string) []string {
|
||||
return c.v.GetStringSlice(k)
|
||||
}
|
||||
|
||||
// Ints returns a field casted to the int slice.
|
||||
func (c *Config) Ints(k string) []int {
|
||||
return c.v.GetIntSlice(k)
|
||||
}
|
||||
|
||||
// Int64s returns a field casted to the int64 slice.
|
||||
func (c *Config) Int64s(k string) (ints []int64) {
|
||||
for _, s := range c.Strings(k) {
|
||||
i, _ := strconv.ParseInt(s, 10, 64)
|
||||
ints = append(ints, i)
|
||||
}
|
||||
return ints
|
||||
}
|
||||
|
||||
// Floats returns a field casted to the float slice.
|
||||
func (c *Config) Floats(k string) (floats []float64) {
|
||||
for _, s := range c.Strings(k) {
|
||||
i, _ := strconv.ParseFloat(s, 64)
|
||||
floats = append(floats, i)
|
||||
}
|
||||
return floats
|
||||
}
|
43
layout/default.go
Normal file
43
layout/default.go
Normal file
@ -0,0 +1,43 @@
|
||||
package layout
|
||||
|
||||
import (
|
||||
tele "git.belvedersky.ru/common/telebot"
|
||||
)
|
||||
|
||||
// DefaultLayout is a simplified layout instance with pre-defined locale by default.
|
||||
type DefaultLayout struct {
|
||||
locale string
|
||||
lt *Layout
|
||||
|
||||
Config
|
||||
}
|
||||
|
||||
// Settings returns layout settings.
|
||||
func (dlt *DefaultLayout) Settings() tele.Settings {
|
||||
return dlt.lt.Settings()
|
||||
}
|
||||
|
||||
// Text wraps localized layout function Text using your default locale.
|
||||
func (dlt *DefaultLayout) Text(k string, args ...interface{}) string {
|
||||
return dlt.lt.TextLocale(dlt.locale, k, args...)
|
||||
}
|
||||
|
||||
// Callback returns a callback endpoint used to handle buttons.
|
||||
func (dlt *DefaultLayout) Callback(k string) tele.CallbackEndpoint {
|
||||
return dlt.lt.Callback(k)
|
||||
}
|
||||
|
||||
// Button wraps localized layout function Button using your default locale.
|
||||
func (dlt *DefaultLayout) Button(k string, args ...interface{}) *tele.Btn {
|
||||
return dlt.lt.ButtonLocale(dlt.locale, k, args...)
|
||||
}
|
||||
|
||||
// Markup wraps localized layout function Markup using your default locale.
|
||||
func (dlt *DefaultLayout) Markup(k string, args ...interface{}) *tele.ReplyMarkup {
|
||||
return dlt.lt.MarkupLocale(dlt.locale, k, args...)
|
||||
}
|
||||
|
||||
// Result wraps localized layout function Result using your default locale.
|
||||
func (dlt *DefaultLayout) Result(k string, args ...interface{}) tele.Result {
|
||||
return dlt.lt.ResultLocale(dlt.locale, k, args...)
|
||||
}
|
76
layout/example.yml
Normal file
76
layout/example.yml
Normal file
@ -0,0 +1,76 @@
|
||||
settings:
|
||||
token_env: TOKEN
|
||||
parse_mode: html
|
||||
long_poller: {}
|
||||
|
||||
commands:
|
||||
/start: Start the bot
|
||||
/help: How to use the bot
|
||||
|
||||
config:
|
||||
str: string
|
||||
num: 123
|
||||
strs:
|
||||
- abc
|
||||
- def
|
||||
nums:
|
||||
- 123
|
||||
- 456
|
||||
obj: &obj
|
||||
dur: 10m
|
||||
arr:
|
||||
- <<: *obj
|
||||
- <<: *obj
|
||||
|
||||
|
||||
buttons:
|
||||
# Shortened reply buttons
|
||||
help: Help
|
||||
settings: Settings
|
||||
|
||||
# Extended reply button
|
||||
contact:
|
||||
text: Send a contact
|
||||
request_contact: true
|
||||
|
||||
# Inline button
|
||||
stop:
|
||||
unique: stop
|
||||
text: Stop
|
||||
data: '{{.}}'
|
||||
|
||||
# Callback data
|
||||
pay:
|
||||
unique: pay
|
||||
text: Pay
|
||||
data:
|
||||
- '{{ .UserID }}'
|
||||
- '{{ .Amount }}'
|
||||
- '{{ .Currency }}'
|
||||
|
||||
web_app:
|
||||
text: This is a web app
|
||||
web_app:
|
||||
url: https://google.com
|
||||
|
||||
markups:
|
||||
reply_shortened:
|
||||
- [ help ]
|
||||
- [ settings ]
|
||||
reply_extended:
|
||||
keyboard:
|
||||
- [ contact ]
|
||||
one_time_keyboard: true
|
||||
inline:
|
||||
- [ stop ]
|
||||
web_app:
|
||||
- [ web_app ]
|
||||
|
||||
results:
|
||||
article:
|
||||
type: article
|
||||
id: '{{ .ID }}'
|
||||
title: '{{ .Title }}'
|
||||
description: '{{ .Description }}'
|
||||
thumb_url: '{{ .PreviewURL }}'
|
||||
message_text: '{{ text `article_message` }}'
|
579
layout/layout.go
Normal file
579
layout/layout.go
Normal file
@ -0,0 +1,579 @@
|
||||
package layout
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
tele "git.belvedersky.ru/common/telebot"
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
type (
|
||||
// Layout provides an interface to interact with the layout,
|
||||
// parsed from the config file and locales.
|
||||
Layout struct {
|
||||
pref *tele.Settings
|
||||
mu sync.RWMutex // protects ctxs
|
||||
ctxs map[tele.Context]string
|
||||
funcs template.FuncMap
|
||||
|
||||
commands map[string]string
|
||||
buttons map[string]Button
|
||||
markups map[string]Markup
|
||||
results map[string]Result
|
||||
locales map[string]*template.Template
|
||||
|
||||
Config
|
||||
}
|
||||
|
||||
// Button is a shortcut for tele.Btn.
|
||||
Button struct {
|
||||
tele.Btn `yaml:",inline"`
|
||||
Data interface{} `yaml:"data"`
|
||||
IsReply bool `yaml:"reply"`
|
||||
}
|
||||
|
||||
// Markup represents layout-specific markup to be parsed.
|
||||
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"`
|
||||
}
|
||||
|
||||
// Result represents layout-specific result to be parsed.
|
||||
Result struct {
|
||||
result *template.Template
|
||||
tele.ResultBase `yaml:",inline"`
|
||||
Content ResultContent `yaml:"content"`
|
||||
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{}
|
||||
)
|
||||
|
||||
// New parses the given layout file.
|
||||
func New(path string, funcs ...template.FuncMap) (*Layout, error) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lt := Layout{
|
||||
ctxs: make(map[tele.Context]string),
|
||||
funcs: make(template.FuncMap),
|
||||
}
|
||||
|
||||
for k, v := range builtinFuncs {
|
||||
lt.funcs[k] = v
|
||||
}
|
||||
for i := range funcs {
|
||||
for k, v := range funcs[i] {
|
||||
lt.funcs[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return <, yaml.Unmarshal(data, <)
|
||||
}
|
||||
|
||||
// NewDefault parses the given layout file without localization features.
|
||||
// See Layout.Default for more details.
|
||||
func NewDefault(path, locale string, funcs ...template.FuncMap) (*DefaultLayout, error) {
|
||||
lt, err := New(path, funcs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lt.Default(locale), nil
|
||||
}
|
||||
|
||||
var builtinFuncs = template.FuncMap{
|
||||
// Built-in blank and helper functions.
|
||||
"locale": func() string { return "" },
|
||||
"config": func(string) string { return "" },
|
||||
"text": func(string, ...interface{}) string { return "" },
|
||||
}
|
||||
|
||||
// Settings returns built telebot Settings required for bot initializing.
|
||||
//
|
||||
// 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!
|
||||
func (lt *Layout) Settings() tele.Settings {
|
||||
if lt.pref == nil {
|
||||
panic("telebot/layout: settings is empty")
|
||||
}
|
||||
return *lt.pref
|
||||
}
|
||||
|
||||
// Default returns a simplified layout instance with the pre-defined locale.
|
||||
// It's useful when you have no need for localization and don't want to pass
|
||||
// context each time you use layout functions.
|
||||
func (lt *Layout) Default(locale string) *DefaultLayout {
|
||||
return &DefaultLayout{
|
||||
locale: locale,
|
||||
lt: lt,
|
||||
Config: lt.Config,
|
||||
}
|
||||
}
|
||||
|
||||
// Locales returns all presented locales.
|
||||
func (lt *Layout) Locales() []string {
|
||||
var keys []string
|
||||
for k := range lt.locales {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// Commands returns a list of telebot commands, which can be
|
||||
// used in b.SetCommands later.
|
||||
func (lt *Layout) Commands() (cmds []tele.Command) {
|
||||
for k, v := range lt.commands {
|
||||
cmds = append(cmds, tele.Command{
|
||||
Text: strings.TrimLeft(k, "/"),
|
||||
Description: v,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CommandsLocale returns a list of telebot commands and localized description, which can be
|
||||
// used in b.SetCommands later.
|
||||
//
|
||||
// Example of bot.yml:
|
||||
//
|
||||
// commands:
|
||||
// /start: '{{ text "cmdStart" }}'
|
||||
//
|
||||
// en.yml:
|
||||
//
|
||||
// cmdStart: Start the bot
|
||||
//
|
||||
// ru.yml:
|
||||
//
|
||||
// cmdStart: Запуск бота
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// b.SetCommands(lt.CommandsLocale("en"), "en")
|
||||
// b.SetCommands(lt.CommandsLocale("ru"), "ru")
|
||||
func (lt *Layout) CommandsLocale(locale string, args ...interface{}) (cmds []tele.Command) {
|
||||
var arg interface{}
|
||||
if len(args) > 0 {
|
||||
arg = args[0]
|
||||
}
|
||||
|
||||
for k, v := range lt.commands {
|
||||
tmpl, err := lt.template(template.New(k).Funcs(lt.funcs), locale).Parse(v)
|
||||
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
|
||||
}
|
||||
|
||||
cmds = append(cmds, tele.Command{
|
||||
Text: strings.TrimLeft(k, "/"),
|
||||
Description: buf.String(),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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:
|
||||
//
|
||||
// func onStart(c tele.Context) error {
|
||||
// return c.Send(lt.Text(c, "start", c.Sender()))
|
||||
// }
|
||||
func (lt *Layout) Text(c tele.Context, k string, args ...interface{}) string {
|
||||
locale, ok := lt.Locale(c)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return lt.TextLocale(locale, k, args...)
|
||||
}
|
||||
|
||||
// TextLocale returns a localized text processed with text/template engine.
|
||||
// See Text for more details.
|
||||
func (lt *Layout) TextLocale(locale, k string, args ...interface{}) string {
|
||||
tmpl, ok := lt.locales[locale]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
var arg interface{}
|
||||
if len(args) > 0 {
|
||||
arg = args[0]
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := lt.template(tmpl, locale).ExecuteTemplate(&buf, k, arg); err != nil {
|
||||
log.Println("telebot/layout:", err)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Callback returns a callback endpoint used to handle buttons.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Handling settings button
|
||||
// b.Handle(lt.Callback("settings"), onSettings)
|
||||
func (lt *Layout) Callback(k string) tele.CallbackEndpoint {
|
||||
btn, ok := lt.buttons[k]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &btn
|
||||
}
|
||||
|
||||
// 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.
|
||||
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.
|
||||
// See Button for more details.
|
||||
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).Funcs(lt.funcs), 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.Btn
|
||||
}
|
||||
|
||||
// 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:
|
||||
//
|
||||
// func onStart(c tele.Context) error {
|
||||
// return c.Send(
|
||||
// lt.Text(c, "start"),
|
||||
// lt.Markup(c, "menu"),
|
||||
// )
|
||||
// }
|
||||
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.
|
||||
// See Markup for more details.
|
||||
func (lt *Layout) MarkupLocale(locale, k string, args ...interface{}) *tele.ReplyMarkup {
|
||||
markup, ok := lt.markups[k]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var arg interface{}
|
||||
if len(args) > 0 {
|
||||
arg = args[0]
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := lt.template(markup.keyboard, locale).Execute(&buf, arg); err != nil {
|
||||
log.Println("telebot/layout:", err)
|
||||
}
|
||||
|
||||
r := &tele.ReplyMarkup{}
|
||||
if *markup.inline {
|
||||
if err := yaml.Unmarshal(buf.Bytes(), &r.InlineKeyboard); err != nil {
|
||||
log.Println("telebot/layout:", err)
|
||||
}
|
||||
} else {
|
||||
r.ResizeKeyboard = markup.ResizeKeyboard == nil || *markup.ResizeKeyboard
|
||||
r.ForceReply = markup.ForceReply
|
||||
r.OneTimeKeyboard = markup.OneTimeKeyboard
|
||||
r.RemoveKeyboard = markup.RemoveKeyboard
|
||||
r.Selective = markup.Selective
|
||||
|
||||
if err := yaml.Unmarshal(buf.Bytes(), &r.ReplyKeyboard); err != nil {
|
||||
log.Println("telebot/layout:", err)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Result returns an inline result, which locale is dependent on the context.
|
||||
// The given optional argument will be passed to the template engine.
|
||||
//
|
||||
// results:
|
||||
// article:
|
||||
// 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 Result
|
||||
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, args...)
|
||||
if markup == nil {
|
||||
log.Printf("telebot/layout: markup with name %s was not found\n", result.Markup)
|
||||
} else {
|
||||
r.SetReplyMarkup(markup)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
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, args ...interface{}) string { return lt.TextLocale(locale, k, args...) }
|
||||
funcs["locale"] = func() string { return locale }
|
||||
|
||||
return tmpl.Funcs(funcs)
|
||||
}
|
||||
|
||||
// IsInputMessageContent implements telebot.InputMessageContent.
|
||||
func (ResultContent) IsInputMessageContent() bool {
|
||||
return true
|
||||
}
|
125
layout/layout_test.go
Normal file
125
layout/layout_test.go
Normal file
@ -0,0 +1,125 @@
|
||||
package layout
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tele "git.belvedersky.ru/common/telebot"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLayout(t *testing.T) {
|
||||
os.Setenv("TOKEN", "TEST")
|
||||
|
||||
lt, err := New("example.yml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pref := lt.Settings()
|
||||
assert.Equal(t, "TEST", pref.Token)
|
||||
assert.Equal(t, "html", pref.ParseMode)
|
||||
assert.Equal(t, &tele.LongPoller{}, pref.Poller)
|
||||
|
||||
assert.ElementsMatch(t, []tele.Command{{
|
||||
Text: "start",
|
||||
Description: "Start the bot",
|
||||
}, {
|
||||
Text: "help",
|
||||
Description: "How to use the bot",
|
||||
}}, lt.Commands())
|
||||
|
||||
assert.Equal(t, "string", lt.String("str"))
|
||||
assert.Equal(t, 123, lt.Int("num"))
|
||||
assert.Equal(t, int64(123), lt.Int64("num"))
|
||||
assert.Equal(t, float64(123), lt.Float("num"))
|
||||
assert.Equal(t, tele.ChatID(123), lt.ChatID("num"))
|
||||
|
||||
assert.Equal(t, []string{"abc", "def"}, lt.Strings("strs"))
|
||||
assert.Equal(t, []int{123, 456}, lt.Ints("nums"))
|
||||
assert.Equal(t, []int64{123, 456}, lt.Int64s("nums"))
|
||||
assert.Equal(t, []float64{123, 456}, lt.Floats("nums"))
|
||||
|
||||
obj := lt.Get("obj")
|
||||
assert.NotNil(t, obj)
|
||||
|
||||
const dur = 10 * time.Minute
|
||||
assert.Equal(t, dur, obj.Duration("dur"))
|
||||
assert.True(t, lt.Duration("obj.dur") == obj.Duration("dur"))
|
||||
|
||||
arr := lt.Slice("arr")
|
||||
assert.Len(t, arr, 2)
|
||||
|
||||
for _, v := range arr {
|
||||
assert.Equal(t, dur, v.Duration("dur"))
|
||||
}
|
||||
|
||||
assert.Equal(t, &tele.Btn{
|
||||
Unique: "pay",
|
||||
Text: "Pay",
|
||||
Data: "1|100.00|USD",
|
||||
}, lt.ButtonLocale("en", "pay", struct {
|
||||
UserID int
|
||||
Amount string
|
||||
Currency string
|
||||
}{
|
||||
UserID: 1,
|
||||
Amount: "100.00",
|
||||
Currency: "USD",
|
||||
}))
|
||||
|
||||
assert.Equal(t, &tele.ReplyMarkup{
|
||||
ReplyKeyboard: [][]tele.ReplyButton{
|
||||
{{Text: "Help"}},
|
||||
{{Text: "Settings"}},
|
||||
},
|
||||
ResizeKeyboard: true,
|
||||
}, lt.MarkupLocale("en", "reply_shortened"))
|
||||
|
||||
assert.Equal(t, &tele.ReplyMarkup{
|
||||
ReplyKeyboard: [][]tele.ReplyButton{{{Text: "Send a contact", Contact: true}}},
|
||||
ResizeKeyboard: true,
|
||||
OneTimeKeyboard: true,
|
||||
}, lt.MarkupLocale("en", "reply_extended"))
|
||||
|
||||
assert.Equal(t, &tele.ReplyMarkup{
|
||||
InlineKeyboard: [][]tele.InlineButton{{
|
||||
{
|
||||
Unique: "stop",
|
||||
Text: "Stop",
|
||||
Data: "1",
|
||||
},
|
||||
}},
|
||||
}, lt.MarkupLocale("en", "inline", 1))
|
||||
|
||||
assert.Equal(t, &tele.ReplyMarkup{
|
||||
InlineKeyboard: [][]tele.InlineButton{{
|
||||
{
|
||||
Text: "This is a web app",
|
||||
WebApp: &tele.WebApp{URL: "https://google.com"},
|
||||
},
|
||||
}},
|
||||
}, lt.MarkupLocale("en", "web_app"))
|
||||
|
||||
assert.Equal(t, &tele.ArticleResult{
|
||||
ResultBase: tele.ResultBase{
|
||||
ID: "1853",
|
||||
Type: "article",
|
||||
},
|
||||
Title: "Some title",
|
||||
Description: "Some description",
|
||||
ThumbURL: "https://preview.picture",
|
||||
Text: "This is an article.",
|
||||
}, lt.ResultLocale("en", "article", struct {
|
||||
ID int
|
||||
Title string
|
||||
Description string
|
||||
PreviewURL string
|
||||
}{
|
||||
ID: 1853,
|
||||
Title: "Some title",
|
||||
Description: "Some description",
|
||||
PreviewURL: "https://preview.picture",
|
||||
}))
|
||||
}
|
1
layout/locales/en.yml
Normal file
1
layout/locales/en.yml
Normal file
@ -0,0 +1 @@
|
||||
article_message: This is an article.
|
50
layout/middleware.go
Normal file
50
layout/middleware.go
Normal file
@ -0,0 +1,50 @@
|
||||
package layout
|
||||
|
||||
import (
|
||||
tele "git.belvedersky.ru/common/telebot"
|
||||
)
|
||||
|
||||
// LocaleFunc is the function used to fetch the locale of the recipient.
|
||||
// Returned locale will be remembered and linked to the corresponding context.
|
||||
type LocaleFunc func(tele.Recipient) string
|
||||
|
||||
// Middleware builds a telebot middleware to make localization work.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// b.Use(lt.Middleware("en", func(r tele.Recipient) string {
|
||||
// loc, _ := db.UserLocale(r.Recipient())
|
||||
// return loc
|
||||
// }))
|
||||
func (lt *Layout) Middleware(defaultLocale string, localeFunc ...LocaleFunc) tele.MiddlewareFunc {
|
||||
var f LocaleFunc
|
||||
if len(localeFunc) > 0 {
|
||||
f = localeFunc[0]
|
||||
}
|
||||
|
||||
return func(next tele.HandlerFunc) tele.HandlerFunc {
|
||||
return func(c tele.Context) error {
|
||||
locale := defaultLocale
|
||||
if f != nil {
|
||||
if l := f(c.Sender()); l != "" {
|
||||
locale = l
|
||||
}
|
||||
}
|
||||
|
||||
lt.SetLocale(c, locale)
|
||||
|
||||
defer func() {
|
||||
lt.mu.Lock()
|
||||
delete(lt.ctxs, c)
|
||||
lt.mu.Unlock()
|
||||
}()
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware wraps ordinary layout middleware with your default locale.
|
||||
func (dlt *DefaultLayout) Middleware() tele.MiddlewareFunc {
|
||||
return dlt.lt.Middleware(dlt.locale)
|
||||
}
|
268
layout/parser.go
Normal file
268
layout/parser.go
Normal file
@ -0,0 +1,268 @@
|
||||
package layout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
tele "git.belvedersky.ru/common/telebot"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
URL string
|
||||
Token string
|
||||
Updates int
|
||||
|
||||
LocalesDir string `yaml:"locales_dir"`
|
||||
TokenEnv string `yaml:"token_env"`
|
||||
ParseMode string `yaml:"parse_mode"`
|
||||
|
||||
Webhook *tele.Webhook `yaml:"webhook"`
|
||||
LongPoller *tele.LongPoller `yaml:"long_poller"`
|
||||
}
|
||||
|
||||
func (lt *Layout) UnmarshalYAML(data []byte) error {
|
||||
var aux struct {
|
||||
Settings *Settings
|
||||
Config map[string]interface{}
|
||||
Commands map[string]string
|
||||
Buttons yaml.MapSlice
|
||||
Markups yaml.MapSlice
|
||||
Results yaml.MapSlice
|
||||
Locales map[string]map[string]string
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v := viper.New()
|
||||
if err := v.MergeConfigMap(aux.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lt.Config = Config{v: v}
|
||||
lt.commands = aux.Commands
|
||||
|
||||
if pref := aux.Settings; pref != nil {
|
||||
lt.pref = &tele.Settings{
|
||||
URL: pref.URL,
|
||||
Token: pref.Token,
|
||||
Updates: pref.Updates,
|
||||
ParseMode: pref.ParseMode,
|
||||
}
|
||||
|
||||
if pref.TokenEnv != "" {
|
||||
lt.pref.Token = os.Getenv(pref.TokenEnv)
|
||||
}
|
||||
|
||||
if pref.Webhook != nil {
|
||||
lt.pref.Poller = pref.Webhook
|
||||
} else if pref.LongPoller != nil {
|
||||
lt.pref.Poller = pref.LongPoller
|
||||
}
|
||||
}
|
||||
|
||||
lt.buttons = make(map[string]Button, len(aux.Buttons))
|
||||
for _, item := range aux.Buttons {
|
||||
k, v := item.Key.(string), item.Value
|
||||
|
||||
// 1. Shortened reply button
|
||||
|
||||
if v, ok := v.(string); ok {
|
||||
btn := tele.Btn{Text: v}
|
||||
lt.buttons[k] = Button{Btn: btn}
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. Extended reply or inline button
|
||||
|
||||
data, err := yaml.MarshalWithOptions(v, yaml.JSON())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var btn Button
|
||||
if err := yaml.Unmarshal(data, &btn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !btn.IsReply && btn.Data != nil {
|
||||
if a, ok := btn.Data.([]interface{}); ok {
|
||||
s := make([]string, len(a))
|
||||
for i, v := range a {
|
||||
s[i] = fmt.Sprint(v)
|
||||
}
|
||||
btn.Btn.Data = strings.Join(s, "|")
|
||||
} else if s, ok := btn.Data.(string); ok {
|
||||
btn.Btn.Data = s
|
||||
} else {
|
||||
return fmt.Errorf("telebot/layout: invalid callback_data for %s button", k)
|
||||
}
|
||||
}
|
||||
|
||||
lt.buttons[k] = btn
|
||||
}
|
||||
|
||||
lt.markups = make(map[string]Markup, len(aux.Markups))
|
||||
for _, item := range aux.Markups {
|
||||
k, v := item.Key.(string), item.Value
|
||||
|
||||
data, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var shortenedMarkup [][]string
|
||||
if yaml.Unmarshal(data, &shortenedMarkup) == nil {
|
||||
// 1. Shortened reply or inline markup
|
||||
|
||||
kb := make([][]Button, len(shortenedMarkup))
|
||||
for i, btns := range shortenedMarkup {
|
||||
row := make([]Button, len(btns))
|
||||
for j, btn := range btns {
|
||||
b, ok := lt.buttons[btn]
|
||||
if !ok {
|
||||
return fmt.Errorf("telebot/layout: no %s button for %s markup", btn, k)
|
||||
}
|
||||
row[j] = b
|
||||
}
|
||||
kb[i] = row
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(kb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpl, err := template.New(k).Funcs(lt.funcs).Parse(string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
markup := Markup{keyboard: tmpl}
|
||||
for _, row := range kb {
|
||||
for _, btn := range row {
|
||||
inline := btn.URL != "" ||
|
||||
btn.Unique != "" ||
|
||||
btn.InlineQuery != "" ||
|
||||
btn.InlineQueryChat != "" ||
|
||||
btn.Login != nil ||
|
||||
btn.WebApp != nil
|
||||
inline = !btn.IsReply && inline
|
||||
|
||||
if markup.inline == nil {
|
||||
markup.inline = &inline
|
||||
} else if *markup.inline != inline {
|
||||
return fmt.Errorf("telebot/layout: mixed reply and inline buttons in %s markup", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lt.markups[k] = markup
|
||||
} else {
|
||||
// 2. Extended reply markup
|
||||
|
||||
var markup struct {
|
||||
Markup `yaml:",inline"`
|
||||
Keyboard [][]string `yaml:"keyboard"`
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &markup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kb := make([][]tele.ReplyButton, len(markup.Keyboard))
|
||||
for i, btns := range markup.Keyboard {
|
||||
row := make([]tele.ReplyButton, len(btns))
|
||||
for j, btn := range btns {
|
||||
row[j] = *lt.buttons[btn].Reply()
|
||||
}
|
||||
kb[i] = row
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(kb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpl, err := template.New(k).Funcs(lt.funcs).Parse(string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
markup.inline = new(bool)
|
||||
markup.Markup.keyboard = tmpl
|
||||
lt.markups[k] = markup.Markup
|
||||
}
|
||||
}
|
||||
|
||||
lt.results = make(map[string]Result, len(aux.Results))
|
||||
for _, item := range aux.Results {
|
||||
k, v := item.Key.(string), item.Value
|
||||
|
||||
data, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpl, err := template.New(k).Funcs(lt.funcs).Parse(string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var result Result
|
||||
if err := yaml.Unmarshal(data, &result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result.result = tmpl
|
||||
lt.results[k] = result
|
||||
}
|
||||
|
||||
if aux.Locales == nil {
|
||||
if aux.Settings.LocalesDir == "" {
|
||||
aux.Settings.LocalesDir = "locales"
|
||||
}
|
||||
return lt.parseLocales(aux.Settings.LocalesDir)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lt *Layout) parseLocales(dir string) error {
|
||||
lt.locales = make(map[string]*template.Template)
|
||||
|
||||
return filepath.Walk(dir, func(path string, fi os.FileInfo, _ error) error {
|
||||
if fi == nil || fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var texts map[string]string
|
||||
if err := yaml.Unmarshal(data, &texts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := fi.Name()
|
||||
name = strings.TrimSuffix(name, filepath.Ext(name))
|
||||
|
||||
tmpl := template.New(name).Funcs(lt.funcs)
|
||||
for key, text := range texts {
|
||||
_, err = tmpl.New(key).Parse(strings.Trim(text, "\r\n"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
lt.locales[name] = tmpl
|
||||
return nil
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user