diff --git a/Telegram/modules/inline.go b/Telegram/modules/inline.go
index b2ad59a..f73c17a 100644
--- a/Telegram/modules/inline.go
+++ b/Telegram/modules/inline.go
@@ -3,9 +3,11 @@ package modules
import (
"encoding/json"
"fmt"
+ "io"
"log"
"math/rand"
"net/http"
+
"regexp"
"strconv"
"strings"
@@ -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 {
@@ -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"`
@@ -34,7 +38,6 @@ 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"`
@@ -42,7 +45,6 @@ type Type struct {
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"`
@@ -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"`
@@ -69,9 +73,9 @@ func fetchAPI() error {
}
apiCache.Lock()
+ defer apiCache.Unlock()
apiCache.Methods = apiDocs.Methods
apiCache.Types = apiDocs.Types
- apiCache.Unlock()
return nil
}
@@ -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 {
@@ -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{
@@ -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))
}
}
@@ -159,42 +169,32 @@ 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("%s\n", method.Name))
- msgBuilder.WriteString(fmt.Sprintf("Description: %s\n\n", sanitizeHTML(strings.Join(method.Description, ", "))))
- msgBuilder.WriteString("Returns: " + strings.Join(method.Returns, ", ") + "\n")
-
- if len(method.Fields) > 0 {
- msgBuilder.WriteString("Fields:\n")
- for _, field := range method.Fields {
- msgBuilder.WriteString(fmt.Sprintf("%s
(%s) - Required: %t
\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("%s\n", typ.Name))
- msgBuilder.WriteString(fmt.Sprintf("Description: %s\n\n", sanitizeHTML(strings.Join(typ.Description, ", "))))
+ msgBuilder.WriteString(fmt.Sprintf("%s\n", name))
+ msgBuilder.WriteString(fmt.Sprintf("Description: %s\n\n", sanitizeHTML(strings.Join(description, ", "))))
+ if returns != nil {
+ msgBuilder.WriteString("Returns: " + strings.Join(returns, ", ") + "\n")
+ }
- if len(typ.Fields) > 0 {
+ if len(fields) > 0 {
msgBuilder.WriteString("Fields:\n")
- for _, field := range typ.Fields {
+ for _, field := range fields {
msgBuilder.WriteString(fmt.Sprintf("%s
(%s) - Required: %t
\n", field.Name, strings.Join(field.Types, ", "), field.Required))
msgBuilder.WriteString(sanitizeHTML(field.Description) + "\n\n")
}
@@ -202,8 +202,9 @@ func buildTypeMessage(typ Type) string {
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
}
@@ -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{
@@ -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!",
@@ -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, "")
}
diff --git a/Telegram/modules/loadModules.go b/Telegram/modules/loadModules.go
index 0b38155..90189fb 100644
--- a/Telegram/modules/loadModules.go
+++ b/Telegram/modules/loadModules.go
@@ -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))
}
diff --git a/Telegram/modules/misc.go b/Telegram/modules/misc.go
new file mode 100644
index 0000000..8eceb43
--- /dev/null
+++ b/Telegram/modules/misc.go
@@ -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
+}
diff --git a/Telegram/modules/start.go b/Telegram/modules/start.go
index 81a0678..5a686f4 100644
--- a/Telegram/modules/start.go
+++ b/Telegram/modules/start.go
@@ -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: @%s your_query
- Quickly search for any method or type in the Telegram Bot API documentation.", b.User.Username)
@@ -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, "Pinging
", &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
+}