package main import ( "bytes" "fmt" "regexp" "strings" "text/template" "time" "github.com/mattermost/mattermost/server/public/model" ) // templateData holds the data available to the post template. type templateData struct { Title string Authors []string AuthorsList string } // languageFlagRe matches --language or --language= anywhere in the string. // The ISO code is captured in group 1 or group 2. var languageFlagRe = regexp.MustCompile(`--language(?:=(\S+)|\s+(\S+))`) // parseLanguageFlag extracts the --language flag from the command text. // Returns the language code and the remaining query with the flag removed. func parseLanguageFlag(text string) (language string, query string) { match := languageFlagRe.FindStringSubmatchIndex(text) if match == nil { return "", strings.TrimSpace(text) } // Extract the language code from whichever group matched. // Group 1: --language=, Group 2: --language var lang string if match[2] >= 0 && match[3] >= 0 { lang = text[match[2]:match[3]] } else if match[4] >= 0 && match[5] >= 0 { lang = text[match[4]:match[5]] } // Remove the flag from the text. remaining := text[:match[0]] + text[match[1]:] return strings.TrimSpace(lang), strings.TrimSpace(remaining) } // handleRequestBook handles the /requestbook slash command. func (p *Plugin) handleRequestBook(args *model.CommandArgs, locale string) (*model.CommandResponse, error) { config := p.getConfiguration() if err := config.IsValid(); err != nil { return ephemeralResponse(T(locale, MsgNotConfigured, err.Error())), nil } // Strip the /requestbook trigger from the command text. text := strings.TrimSpace(strings.TrimPrefix(args.Command, "/requestbook")) // Parse the --language flag if present. language, query := parseLanguageFlag(text) // Fall back to the configured default language. if language == "" { language = config.getDefaultLanguage() } if query == "" { return ephemeralResponse(T(locale, MsgUsage)), nil } // Search for books on Shelfmark. searchResult, err := p.shelfmarkClient.SearchBooks(query) if err != nil { p.API.LogError("Failed to search books on Shelfmark", "query", query, "error", err.Error()) return ephemeralResponse(T(locale, MsgSearchFailed)), nil } if len(searchResult.Books) == 0 { return ephemeralResponse(T(locale, MsgNoBooksFound, query)), nil } book := searchResult.Books[0] // Create a download task and persist it. The actual post will be created // once the book file has been downloaded and is ready to attach. // The post message is rendered later (in processTaskComplete) so that // the localized title from the releases response can be used. taskID := fmt.Sprintf("%s_%s_%d", book.Provider, book.ProviderID, time.Now().UnixMilli()) task := &DownloadTask{ ID: taskID, ChannelID: config.ChannelID, BookTitle: book.Title, BookProvider: book.Provider, BookProviderID: book.ProviderID, BookCoverURL: book.CoverURL, BookAuthors: book.Authors, Language: language, Status: TaskStatusPending, CreatedAt: time.Now(), UpdatedAt: time.Now(), RequestedBy: args.UserId, RequesterLocale: locale, } if err := p.taskStore.SaveTask(task); err != nil { p.API.LogError("Failed to save download task", "error", err.Error()) return ephemeralResponse(T(locale, MsgQueueFailed)), nil } langInfo := "" if language != "" { langInfo = T(locale, MsgLanguageInfo, language) } return ephemeralResponse(T(locale, MsgBookFound, book.Title, langInfo)), nil } // renderPostMessage renders the post template with the given task data. func (p *Plugin) renderPostMessage(task *DownloadTask) (string, error) { config := p.getConfiguration() tmplStr := config.getPostTemplate() tmpl, err := template.New("post").Parse(tmplStr) if err != nil { return "", fmt.Errorf("failed to parse template: %w", err) } data := templateData{ Title: task.EffectiveTitle(), Authors: task.BookAuthors, AuthorsList: strings.Join(task.BookAuthors, ", "), } var buf bytes.Buffer if err := tmpl.Execute(&buf, data); err != nil { return "", fmt.Errorf("failed to execute template: %w", err) } return buf.String(), nil } // ephemeralResponse creates an ephemeral command response visible only to the user. func ephemeralResponse(message string) *model.CommandResponse { return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, Text: message, } }