mattermost-plugin-shelfmark/server/requestbook.go
Felipe M. e05ab1bc96
Add i18n support for user-facing messages and rewrite README
Replace hardcoded English strings throughout the plugin with localized
messages via a new i18n system supporting English and Spanish. Store the
requester's locale on the download task so background job messages are
properly localized. Replace the starter template README with
project-specific documentation covering features, usage, configuration,
and development.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 17:55:33 +01:00

147 lines
4.4 KiB
Go

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 <code> or --language=<code> 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=<code>, Group 2: --language <code>
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,
}
}