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>
147 lines
4.4 KiB
Go
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,
|
|
}
|
|
}
|