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 +}