package main import ( "net/http" "strings" "sync" "time" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/pluginapi" "github.com/mattermost/mattermost/server/public/pluginapi/cluster" "github.com/pkg/errors" "git.nakama.town/fmartingr/mattermost-plugin-shelfmark/server/shelfmark" ) // Plugin implements the interface expected by the Mattermost server to communicate between the server and plugin processes. type Plugin struct { plugin.MattermostPlugin // client is the Mattermost server API client. client *pluginapi.Client // botUserID is the user ID of the plugin's bot account. botUserID string // shelfmarkClient is the HTTP client for the Shelfmark server API. shelfmarkClient *shelfmark.Client // taskStore is the KV-backed store for download tasks. taskStore *taskStore // backgroundJob is the scheduled cluster job for processing downloads. backgroundJob *cluster.Job // configurationLock synchronizes access to the configuration. configurationLock sync.RWMutex // configuration is the active plugin configuration. Consult getConfiguration and // setConfiguration for usage. configuration *configuration } // OnActivate is invoked when the plugin is activated. If an error is returned, the plugin will be deactivated. func (p *Plugin) OnActivate() error { p.client = pluginapi.NewClient(p.API, p.Driver) // Ensure the bot user exists. botUserID, err := p.client.Bot.EnsureBot(&model.Bot{ Username: "shelfmark", DisplayName: "Shelfmark", Description: "Posts book requests from Shelfmark.", }, pluginapi.ProfileImagePath("assets/icon.png")) if err != nil { return errors.Wrap(err, "failed to ensure bot user") } p.botUserID = botUserID // Initialize the task store. p.taskStore = newTaskStore(p.client, func(msg string, keyvals ...string) { args := make([]any, 0, len(keyvals)) for _, kv := range keyvals { args = append(args, kv) } p.API.LogWarn(msg, args...) }) // Initialize the Shelfmark client with current configuration. config := p.getConfiguration() p.shelfmarkClient = shelfmark.NewClient(config.shelfmarkCredentials()) // Register the /requestbook slash command. if regErr := p.API.RegisterCommand(&model.Command{ Trigger: "requestbook", DisplayName: "Request Book", Description: "Search for a book on Shelfmark and post it in the configured channel.", AutoComplete: true, AutoCompleteDesc: "Search for a book by title, author, or ISBN. Use --language to specify language.", AutoCompleteHint: "[--language ] ", }); regErr != nil { return errors.Wrap(regErr, "failed to register /requestbook command") } // Schedule the background job to process download tasks every 5 seconds. job, err := cluster.Schedule( p.API, "DownloadJob", cluster.MakeWaitForInterval(5*time.Second), p.runDownloadJob, ) if err != nil { return errors.Wrap(err, "failed to schedule download job") } p.backgroundJob = job p.API.LogInfo("Shelfmark plugin activated", "bot_user_id", p.botUserID) return nil } // OnDeactivate is invoked when the plugin is deactivated. func (p *Plugin) OnDeactivate() error { if p.backgroundJob != nil { if err := p.backgroundJob.Close(); err != nil { p.API.LogError("Failed to close background job", "err", err) } } return nil } // ExecuteCommand handles slash command execution. func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { locale := p.getUserLocale(args.UserId) config := p.getConfiguration() if teamID := config.getTeamID(); teamID != "" && teamID != args.TeamId { p.API.LogDebug("Command blocked by team restriction", "user_id", args.UserId, "user_team", args.TeamId, "allowed_team", teamID) return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, Text: T(locale, MsgTeamNotAllowed), }, nil } fields := strings.Fields(args.Command) if len(fields) == 0 { return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, Text: T(locale, MsgCommandEmpty), }, nil } trigger := strings.TrimPrefix(fields[0], "/") switch trigger { case "requestbook": response, err := p.handleRequestBook(args, locale) if err != nil { return nil, model.NewAppError("ExecuteCommand", "plugin.command.execute_command.app_error", nil, err.Error(), http.StatusInternalServerError) } return response, nil default: return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, Text: T(locale, MsgCommandUnknown, trigger), }, nil } } // getUserLocale returns the Mattermost locale for the given user, defaulting to "en". func (p *Plugin) getUserLocale(userID string) string { user, appErr := p.API.GetUser(userID) if appErr != nil || user == nil || user.Locale == "" { return "en" } return user.Locale } // OnConfigurationChange is called when the plugin configuration is updated. // It updates the Shelfmark client credentials to match the new configuration. func (p *Plugin) onConfigurationChanged() { config := p.getConfiguration() if p.shelfmarkClient != nil { p.shelfmarkClient.UpdateCredentials(config.shelfmarkCredentials()) } }