Adds an optional TeamID setting that restricts the slash command, API endpoints, and RHS panel to a specific Mattermost team. When empty, the plugin works on all teams. Server-side enforcement validates team membership on both command execution and book request API calls. The webapp fails closed if the config fetch fails. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
167 lines
5.2 KiB
Go
167 lines
5.2 KiB
Go
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 <code> to specify language.",
|
|
AutoCompleteHint: "[--language <code>] <search query>",
|
|
}); 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())
|
|
}
|
|
}
|