Skip to content

Commit

Permalink
Refactor: API inline query handler (#2)
Browse files Browse the repository at this point in the history
* Refactor API inline query handler: consolidate message building, improve readability, and enhance error handling.

* add: ping and improve
  • Loading branch information
AshokShau authored Sep 30, 2024
1 parent cdff7c4 commit ba28619
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 55 deletions.
110 changes: 55 additions & 55 deletions Telegram/modules/inline.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package modules
import (
"encoding/json"
"fmt"
"io"
"log"
"math/rand"
"net/http"

"regexp"
"strconv"
"strings"
Expand All @@ -16,7 +18,10 @@ import (
"github.com/PaulSonOfLars/gotgbot/v2/ext"
)

const maxMessageLength = 4096 // Telegram's maximum message length
const (
maxMessageLength = 4096 // Telegram's maximum message length
apiURL = "https://github.com/PaulSonOfLars/telegram-bot-api-spec/raw/main/api.json"
)

// apiCache is a global cache for storing API methods and types.
var apiCache struct {
Expand All @@ -25,7 +30,6 @@ var apiCache struct {
Types map[string]Type
}

// Method represents an API method with its details.
type Method struct {
Name string `json:"name"`
Description []string `json:"description"`
Expand All @@ -34,15 +38,13 @@ type Method struct {
Fields []Field `json:"fields,omitempty"`
}

// Type represents an API type with its details.
type Type struct {
Name string `json:"name"`
Description []string `json:"description"`
Href string `json:"href"`
Fields []Field `json:"fields,omitempty"`
}

// Field represents a field in an API method or type.
type Field struct {
Name string `json:"name"`
Types []string `json:"types"`
Expand All @@ -53,11 +55,13 @@ type Field struct {
// fetchAPI fetches the API documentation from a remote source and updates the apiCache.
func fetchAPI() error {
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("https://github.com/PaulSonOfLars/telegram-bot-api-spec/raw/main/api.json")
resp, err := client.Get(apiURL)
if err != nil {
return fmt.Errorf("failed to fetch API: %w", err)
}
defer resp.Body.Close()
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)

var apiDocs struct {
Methods map[string]Method `json:"methods"`
Expand All @@ -69,9 +73,9 @@ func fetchAPI() error {
}

apiCache.Lock()
defer apiCache.Unlock()
apiCache.Methods = apiDocs.Methods
apiCache.Types = apiDocs.Types
apiCache.Unlock()

return nil
}
Expand All @@ -88,29 +92,32 @@ func StartAPICacheUpdater(interval time.Duration) {
}()
}

// getAPICache returns a snapshot of the current API cache.
func getAPICache() (map[string]Method, map[string]Type) {
apiCache.RLock()
defer apiCache.RUnlock()
return apiCache.Methods, apiCache.Types
}

// inlineQueryHandler handles inline queries from the bot.
func inlineQueryHandler(bot *gotgbot.Bot, ctx *ext.Context) error {
query := strings.TrimSpace(ctx.InlineQuery.Query)
parts := strings.Fields(query)

if len(parts) < 1 {
if len(parts) == 0 {
return sendEmptyQueryResponse(bot, ctx)
}

kueri := strings.Join(parts, " ")
if strings.ToLower(parts[0]) == "botapi" {
kueri = strings.Join(parts[1:], " ")
kueri := strings.Join(parts[1:], " ")
if strings.EqualFold(parts[0], "botapi") {
query = kueri
}

apiCache.RLock()
methods := apiCache.Methods
types := apiCache.Types
apiCache.RUnlock()

results := searchAPI(kueri, methods, types)
methods, types := getAPICache()
results := searchAPI(query, methods, types)

if len(results) == 0 {
return sendNoResultsResponse(bot, ctx, kueri)
return sendNoResultsResponse(bot, ctx, query)
}

if len(results) > 50 {
Expand All @@ -123,7 +130,7 @@ func inlineQueryHandler(bot *gotgbot.Bot, ctx *ext.Context) error {

// sendEmptyQueryResponse sends a response for an empty inline query.
func sendEmptyQueryResponse(bot *gotgbot.Bot, ctx *ext.Context) error {
_, err := ctx.InlineQuery.Answer(bot, []gotgbot.InlineQueryResult{}, &gotgbot.AnswerInlineQueryOpts{
_, err := ctx.InlineQuery.Answer(bot, nil, &gotgbot.AnswerInlineQueryOpts{
IsPersonal: true,
CacheTime: 5,
Button: &gotgbot.InlineQueryResultsButton{
Expand All @@ -137,18 +144,21 @@ func sendEmptyQueryResponse(bot *gotgbot.Bot, ctx *ext.Context) error {
// searchAPI searches the API methods and types for the given query.
func searchAPI(query string, methods map[string]Method, types map[string]Type) []gotgbot.InlineQueryResult {
var results []gotgbot.InlineQueryResult
lowerQuery := strings.ToLower(query)

search := func(name, href, msg string) {
results = append(results, createInlineResult(name, href, msg, href))
}

for name, method := range methods {
if strings.Contains(strings.ToLower(name), strings.ToLower(query)) {
msg := buildMethodMessage(method)
results = append(results, createInlineResult(name, method.Href, msg, method.Href))
if strings.Contains(strings.ToLower(name), lowerQuery) {
search(name, method.Href, buildMethodMessage(method))
}
}

for name, typ := range types {
if strings.Contains(strings.ToLower(name), strings.ToLower(query)) {
msg := buildTypeMessage(typ)
results = append(results, createInlineResult(name, typ.Href, msg, typ.Href))
if strings.Contains(strings.ToLower(name), lowerQuery) {
search(name, typ.Href, buildTypeMessage(typ))
}
}

Expand All @@ -159,51 +169,42 @@ func searchAPI(query string, methods map[string]Method, types map[string]Type) [
func sendNoResultsResponse(bot *gotgbot.Bot, ctx *ext.Context, query string) error {
_, err := ctx.InlineQuery.Answer(bot, []gotgbot.InlineQueryResult{noResultsArticle(query)}, &gotgbot.AnswerInlineQueryOpts{
IsPersonal: true,
CacheTime: 5,
CacheTime: 500,
})
return err
}

// buildMethodMessage builds a message string for a given API method.
func buildMethodMessage(method Method) string {
var msgBuilder strings.Builder
msgBuilder.WriteString(fmt.Sprintf("<b>%s</b>\n", method.Name))
msgBuilder.WriteString(fmt.Sprintf("Description: %s\n\n", sanitizeHTML(strings.Join(method.Description, ", "))))
msgBuilder.WriteString("<b>Returns:</b> " + strings.Join(method.Returns, ", ") + "\n")

if len(method.Fields) > 0 {
msgBuilder.WriteString("<b>Fields:</b>\n")
for _, field := range method.Fields {
msgBuilder.WriteString(fmt.Sprintf("<code>%s</code> (<b>%s</b>) - Required: <code>%t</code>\n", field.Name, strings.Join(field.Types, ", "), field.Required))
msgBuilder.WriteString(sanitizeHTML(field.Description) + "\n\n")
}
}

message := msgBuilder.String()
if len(message) > maxMessageLength {
return fmt.Sprintf("See full documentation: %s", method.Href)
}
return message
return buildMessage(method.Name, method.Description, method.Returns, method.Fields, method.Href)
}

// buildTypeMessage builds a message string for a given API type.
func buildTypeMessage(typ Type) string {
return buildMessage(typ.Name, typ.Description, nil, typ.Fields, typ.Href)
}

func buildMessage(name string, description []string, returns []string, fields []Field, href string) string {
var msgBuilder strings.Builder
msgBuilder.WriteString(fmt.Sprintf("<b>%s</b>\n", typ.Name))
msgBuilder.WriteString(fmt.Sprintf("Description: %s\n\n", sanitizeHTML(strings.Join(typ.Description, ", "))))
msgBuilder.WriteString(fmt.Sprintf("<b>%s</b>\n", name))
msgBuilder.WriteString(fmt.Sprintf("Description: %s\n\n", sanitizeHTML(strings.Join(description, ", "))))
if returns != nil {
msgBuilder.WriteString("<b>Returns:</b> " + strings.Join(returns, ", ") + "\n")
}

if len(typ.Fields) > 0 {
if len(fields) > 0 {
msgBuilder.WriteString("<b>Fields:</b>\n")
for _, field := range typ.Fields {
for _, field := range fields {
msgBuilder.WriteString(fmt.Sprintf("<code>%s</code> (<b>%s</b>) - Required: <code>%t</code>\n", field.Name, strings.Join(field.Types, ", "), field.Required))
msgBuilder.WriteString(sanitizeHTML(field.Description) + "\n\n")
}
}

message := msgBuilder.String()
if len(message) > maxMessageLength {
return fmt.Sprintf("See full documentation: %s", typ.Href)
return fmt.Sprintf("See full documentation: %s", href)
}

return message
}

Expand All @@ -215,8 +216,9 @@ func createInlineResult(title, url, message, methodUrl string) gotgbot.InlineQue
Url: url,
HideUrl: true,
InputMessageContent: gotgbot.InputTextMessageContent{
MessageText: message,
ParseMode: gotgbot.ParseModeHTML,
MessageText: message,
ParseMode: gotgbot.ParseModeHTML,
LinkPreviewOptions: &gotgbot.LinkPreviewOptions{PreferSmallMedia: true},
},
Description: "View more details",
ReplyMarkup: &gotgbot.InlineKeyboardMarkup{
Expand All @@ -230,7 +232,6 @@ func createInlineResult(title, url, message, methodUrl string) gotgbot.InlineQue

// noResultsArticle creates an inline query result indicating no results were found.
func noResultsArticle(query string) gotgbot.InlineQueryResult {
ok := "botapi"
return gotgbot.InlineQueryResultArticle{
Id: strconv.Itoa(rand.Intn(100000)),
Title: "No Results Found!",
Expand All @@ -241,15 +242,14 @@ func noResultsArticle(query string) gotgbot.InlineQueryResult {
Description: "No results found for your query.",
ReplyMarkup: &gotgbot.InlineKeyboardMarkup{
InlineKeyboard: [][]gotgbot.InlineKeyboardButton{
{{Text: "Search Again", SwitchInlineQueryCurrentChat: &ok}},
{{Text: "Search Again", SwitchInlineQueryCurrentChat: &query}},
},
},
}
}

// sanitizeHTML removes unsupported HTML tags from the message
// sanitizeHTML removes unsupported HTML tags from the message.
func sanitizeHTML(input string) string {
// This regex matches any HTML tags that are not supported
re := regexp.MustCompile(`<[^>]*>`)
return re.ReplaceAllString(input, "")
}
1 change: 1 addition & 0 deletions Telegram/modules/loadModules.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ func newDispatcher() *ext.Dispatcher {

func loadModules(d *ext.Dispatcher) {
d.AddHandler(handlers.NewCommand("start", start))
d.AddHandler(handlers.NewCommand("ping", ping))
d.AddHandler(handlers.NewInlineQuery(inlinequery.All, inlineQueryHandler))
}
68 changes: 68 additions & 0 deletions Telegram/modules/misc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package modules

import (
"fmt"
"strings"
"time"
)

// getFormattedDuration returns a formatted string representing the duration
func getFormattedDuration(diff time.Duration) string {
seconds := int(diff.Seconds())
minutes := seconds / 60
hours := minutes / 60
days := hours / 24
weeks := days / 7
months := days / 30

// Calculate remaining values after each unit is accounted for
remainingSeconds := seconds % 60
remainingMinutes := minutes % 60
remainingHours := hours % 24
remainingDays := days % 7

var text string

// Format months
if months != 0 {
text += fmt.Sprintf("%d months ", months)
}

// Format weeks
if weeks != 0 {
text += fmt.Sprintf("%d weeks ", weeks)
}

// Format days
if remainingDays != 0 {
text += fmt.Sprintf("%d days ", remainingDays)
}

// Format hours
if remainingHours != 0 {
text += fmt.Sprintf("%d hours ", remainingHours)
}

// Format minutes
if remainingMinutes != 0 {
text += fmt.Sprintf("%d minutes ", remainingMinutes)
}

// Format seconds
if remainingSeconds != 0 || text == "" { // Include seconds if there's no larger unit or if there are remaining seconds
text += fmt.Sprintf("%d seconds", remainingSeconds)
}

// Trim any trailing space
text = trimSuffix(text, " ")

return text
}

// trimSuffix removes the suffix from the string if it exists
func trimSuffix(s, suffix string) string {
if strings.HasSuffix(s, suffix) {
return s[:len(s)-len(suffix)]
}
return s
}
31 changes: 31 additions & 0 deletions Telegram/modules/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import (
"github.com/PaulSonOfLars/gotgbot/v2"
"github.com/PaulSonOfLars/gotgbot/v2/ext"
"log"
"time"
_ "time/tzdata"
)

var StartTime = time.Now()

func start(b *gotgbot.Bot, ctx *ext.Context) error {
msg := ctx.EffectiveMessage
text := fmt.Sprintf("👋 Hello! I'm your handy Telegram Bot API assistant, built with GoTgBot.\n\n💡 Usage: <code>@%s your_query</code> - Quickly search for any method or type in the Telegram Bot API documentation.", b.User.Username)
Expand All @@ -19,3 +23,30 @@ func start(b *gotgbot.Bot, ctx *ext.Context) error {

return ext.EndGroups
}

func ping(b *gotgbot.Bot, ctx *ext.Context) error {
msg := ctx.EffectiveMessage
startTime := time.Now()

rest, err := msg.Reply(b, "<code>Pinging</code>", &gotgbot.SendMessageOpts{ParseMode: "HTML"})
if err != nil {
return fmt.Errorf("ping: %v", err)
}

// Calculate latency
elapsedTime := time.Since(startTime)

// Calculate uptime
uptime := time.Since(StartTime)
formattedUptime := getFormattedDuration(uptime)

location, _ := time.LoadLocation("Asia/Kolkata")
responseText := fmt.Sprintf("Pinged in %vms (Latency: %.2fs) at %s\n\nUptime: %s", elapsedTime.Milliseconds(), elapsedTime.Seconds(), time.Now().In(location).Format(time.RFC1123), formattedUptime)

_, _, err = rest.EditText(b, responseText, nil)
if err != nil {
return fmt.Errorf("ping: %v", err)
}

return ext.EndGroups
}

0 comments on commit ba28619

Please sign in to comment.