layout: add inline query results support

This commit is contained in:
Demian 2021-07-25 14:15:42 +03:00
parent 3281935f9c
commit 5108157783
5 changed files with 238 additions and 16 deletions

View File

@ -24,3 +24,8 @@ func (dlt *DefaultLayout) Button(k string, args ...interface{}) *tele.Btn {
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...)
}

View File

@ -43,3 +43,12 @@ markups:
one_time_keyboard: true
inline:
- [ stop ]
results:
article:
type: article
id: '{{ .ID }}'
title: '{{ .Title }}'
description: '{{ .Description }}'
message_text: '{{ .Content }}'
thumb_url: '{{ .PreviewURL }}'

View File

@ -2,6 +2,7 @@ package layout
import (
"bytes"
"encoding/json"
"io/ioutil"
"log"
"sync"
@ -22,6 +23,7 @@ type (
buttons map[string]Button
markups map[string]Markup
results map[string]Result
locales map[string]*template.Template
*Config
@ -40,6 +42,22 @@ type (
RemoveKeyboard bool `yaml:"remove_keyboard,omitempty"`
Selective bool `yaml:"selective,omitempty"`
}
// 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{}
)
// New reads and parses the given layout file.
@ -157,7 +175,7 @@ func (lt *Layout) Text(c tele.Context, k string, args ...interface{}) string {
return lt.TextLocale(locale, k, args...)
}
// TextLocale returns a localized text processed with standard template engine.
// 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]
@ -227,7 +245,7 @@ func (lt *Layout) Button(c tele.Context, k string, args ...interface{}) *tele.Bt
return lt.ButtonLocale(locale, k, args...)
}
// ButtonLocale returns a localized button processed with standard template engine.
// 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]
@ -279,7 +297,8 @@ func (lt *Layout) ButtonLocale(locale, k string, args ...interface{}) *tele.Btn
// func onStart(c tele.Context) error {
// return c.Send(
// lt.Text(c, "start"),
// lt.Markup(c, "menu"))
// lt.Markup(c, "menu"),
// )
// }
//
func (lt *Layout) Markup(c tele.Context, k string, args ...interface{}) *tele.ReplyMarkup {
@ -291,7 +310,7 @@ func (lt *Layout) Markup(c tele.Context, k string, args ...interface{}) *tele.Re
return lt.MarkupLocale(locale, k, args...)
}
// MarkupLocale returns a localized markup processed with standard template engine.
// 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]
@ -329,6 +348,143 @@ func (lt *Layout) MarkupLocale(locale, k string, args ...interface{}) *tele.Repl
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:
// 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
}
func (lt *Layout) template(tmpl *template.Template, locale string) *template.Template {
funcs := make(template.FuncMap)
@ -339,3 +495,8 @@ func (lt *Layout) template(tmpl *template.Template, locale string) *template.Tem
return tmpl.Funcs(funcs)
}
// IsInputMessageContent implements telebot.InputMessageContent.
func (ResultContent) IsInputMessageContent() bool {
return true
}

View File

@ -28,6 +28,20 @@ func TestLayout(t *testing.T) {
assert.Equal(t, float64(123), lt.Float("num"))
assert.Equal(t, 10*time.Minute, lt.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"}},
@ -46,17 +60,26 @@ func TestLayout(t *testing.T) {
InlineKeyboard: [][]tele.InlineButton{{{Unique: "stop", Text: "Stop", Data: "1"}}},
}, lt.MarkupLocale("en", "inline", 1))
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
assert.Equal(t, &tele.ArticleResult{
ResultBase: tele.ResultBase{
ID: "1853",
Type: "article",
},
Title: "Some title",
Description: "Some description",
Text: "The text of the article",
ThumbURL: "https://preview.picture",
}, lt.ResultLocale("en", "article", struct {
ID int
Title string
Description string
Content string
PreviewURL string
}{
UserID: 1,
Amount: "100.00",
Currency: "USD",
ID: 1853,
Title: "Some title",
Description: "Some description",
Content: "The text of the article",
PreviewURL: "https://preview.picture",
}))
}

View File

@ -17,7 +17,7 @@ type Settings struct {
Token string
Updates int
LocalesDir string `"locales_dir"`
LocalesDir string `yaml:"locales_dir"`
TokenEnv string `yaml:"token_env"`
ParseMode string `yaml:"parse_mode"`
@ -31,6 +31,7 @@ func (lt *Layout) UnmarshalYAML(data []byte) error {
Config map[string]interface{}
Buttons yaml.MapSlice
Markups yaml.MapSlice
Results yaml.MapSlice
Locales map[string]map[string]string
}
if err := yaml.Unmarshal(data, &aux); err != nil {
@ -192,6 +193,29 @@ func (lt *Layout) UnmarshalYAML(data []byte) error {
}
}
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"