mattermost/server/public/plugin/hooks.go
Felipe Martin 1be8a68dd7
feat: pluginapi: filewillbedownloaded / sendtoastmessage (#34596)
* feat: filewillbedonwloaded hook

* feat: error popup

* chore: make generated pluginapi

* tests

* feat: different errors for different download types

* feat: allow toast positions

* fix: avoid using deprecated i18n function

* feat: add plugin API to show toasts

* feat: downloadType parameter

* tests: updated tests

* chore: make check-style

* chore: i18n

* chore: missing fields in tests

* chore: sorted i18n for webapp

* chore: run mmjstool

* test: fixed webapp tests with new changes

* test: missing mocks

* fix: ensure one-file attachments (previews) are handler properly as thumbnails

* chore: lint

* test: added new logic to tests

* chore: lint

* Add SendToastMessage API and FileWillBeDownloaded hook

- Introduced SendToastMessage method for sending toast notifications to users with customizable options.
- Added FileWillBeDownloaded hook to handle file download requests, allowing plugins to control access to files.
- Updated related types and constants for file download handling.
- Enhanced PluginSettings to include HookTimeoutSeconds for better timeout management.

* Update webapp/channels/src/components/single_image_view/single_image_view.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: copilot reviews

* test: head requests

* chore: linted the webapp

* tests: fixed path

* test: fixed mocked args

* allow sending message to a connection directly

* fix: hook thread safety

* chore: formatting

* chore: remove configuration from system console

* chore: release version

* chore: update signature

* chore: update release version

* chore: addressed comments

* fix: update file rejection handling to use 403 Forbidden status and include rejection reason header

* Fix nil pointer panic in runFileWillBeDownloadedHook

The atomic.Value in runFileWillBeDownloadedHook can be nil if no
plugins implement the FileWillBeDownloaded hook. This causes a panic
when trying to assert the nil interface to string.

This fix adds a nil check before the type assertion, defaulting to
an empty string (which allows the download) when no hooks have run.

Fixes:
- TestUploadDataMultipart/success panic
- TestUploadDataMultipart/resume_success panic

* test: move the logout test last

* chore: restored accidential deletion

* chore: lint

* chore: make generated

* refactor: move websocket events to new package

* chore: go vet

* chore: missing mock

* chore: revert incorrect fmt

* chore: import ordering

* chore: npm i18n-extract

* chore: update constants.tsx from master

* chore: make i18n-extract

* revert: conflict merge

* fix: add missing isFileRejected prop to SingleImageView tests

* fix: mock fetch in SingleImageView tests for async thumbnail check

The component now performs an async fetch to check thumbnail availability
before rendering. Tests need to mock fetch and use waitFor to handle
the async state updates.

* refactor: move hook logic to app layer

* chore: update version to 11.5

* Scope file download rejection toast to the requesting connection

Thread the Connection-Id header through RunFileWillBeDownloadedHook and
sendFileDownloadRejectedEvent so the WebSocket event is sent only to the
connection that initiated the download, instead of all connections for
the user.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-16 17:10:39 +01:00

436 lines
20 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"io"
"net/http"
saml2 "github.com/mattermost/gosaml2"
"github.com/mattermost/mattermost/server/public/model"
)
// These assignments are part of the wire protocol used to trigger hook events in plugins.
//
// Feel free to add more, but do not change existing assignments. Follow the naming convention of
// <HookName>ID as the autogenerated glue code depends on that.
const (
OnActivateID = 0
OnDeactivateID = 1
ServeHTTPID = 2
OnConfigurationChangeID = 3
ExecuteCommandID = 4
MessageWillBePostedID = 5
MessageWillBeUpdatedID = 6
MessageHasBeenPostedID = 7
MessageHasBeenUpdatedID = 8
UserHasJoinedChannelID = 9
UserHasLeftChannelID = 10
UserHasJoinedTeamID = 11
UserHasLeftTeamID = 12
ChannelHasBeenCreatedID = 13
FileWillBeUploadedID = 14
UserWillLogInID = 15
UserHasLoggedInID = 16
UserHasBeenCreatedID = 17
ReactionHasBeenAddedID = 18
ReactionHasBeenRemovedID = 19
OnPluginClusterEventID = 20
OnWebSocketConnectID = 21
OnWebSocketDisconnectID = 22
WebSocketMessageHasBeenPostedID = 23
RunDataRetentionID = 24
OnInstallID = 25
OnSendDailyTelemetryID = 26
OnCloudLimitsUpdatedID = 27
deprecatedUserHasPermissionToCollectionID = 28
deprecatedGetAllUserIdsForCollectionID = 29
deprecatedGetAllCollectionIDsForUserID = 30
deprecatedGetTopicRedirectID = 31
deprecatedGetCollectionMetadataByIdsID = 32
deprecatedGetTopicMetadataByIdsID = 33
ConfigurationWillBeSavedID = 34
NotificationWillBePushedID = 35
UserHasBeenDeactivatedID = 36
MessageHasBeenDeletedID = 37
MessagesWillBeConsumedID = 38
ServeMetricsID = 39
OnSharedChannelsSyncMsgID = 40
OnSharedChannelsPingID = 41
PreferencesHaveChangedID = 42
OnSharedChannelsAttachmentSyncMsgID = 43
OnSharedChannelsProfileImageSyncMsgID = 44
GenerateSupportDataID = 45
OnSAMLLoginID = 46
EmailNotificationWillBeSentID = 47
FileWillBeDownloadedID = 48
TotalHooksID = iota
)
const (
// DismissPostError dismisses a pending post when the error is returned from MessageWillBePosted.
DismissPostError = "plugin.message_will_be_posted.dismiss_post"
)
// Hooks describes the methods a plugin may implement to automatically receive the corresponding
// event.
//
// A plugin only need implement the hooks it cares about. The MattermostPlugin provides some
// default implementations for convenience but may be overridden.
type Hooks interface {
// OnActivate is invoked when the plugin is activated. If an error is returned, the plugin
// will be terminated. The plugin will not receive hooks until after OnActivate returns
// without error. OnConfigurationChange will be called once before OnActivate.
//
// Minimum server version: 5.2
OnActivate() error
// Implemented returns a list of hooks that are implemented by the plugin.
// Plugins do not need to provide an implementation. Any given will be ignored.
//
// Minimum server version: 5.2
Implemented() ([]string, error)
// OnDeactivate is invoked when the plugin is deactivated. This is the plugin's last chance to
// use the API, and the plugin will be terminated shortly after this invocation. The plugin
// will stop receiving hooks just prior to this method being called.
//
// Minimum server version: 5.2
OnDeactivate() error
// OnConfigurationChange is invoked when configuration changes may have been made. Any
// returned error is logged, but does not stop the plugin. You must be prepared to handle
// a configuration failure gracefully. It is called once before OnActivate.
//
// Minimum server version: 5.2
OnConfigurationChange() error
// ServeHTTP allows the plugin to implement the http.Handler interface. Requests destined for
// the /plugins/{id} path will be routed to the plugin.
//
// The Mattermost-User-Id header will be present if (and only if) the request is by an
// authenticated user.
//
// Minimum server version: 5.2
ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request)
// ExecuteCommand executes a command that has been previously registered via the RegisterCommand
// API.
//
// Minimum server version: 5.2
ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError)
// UserHasBeenCreated is invoked after a user was created.
//
// Minimum server version: 5.10
UserHasBeenCreated(c *Context, user *model.User)
// UserWillLogIn before the login of the user is returned. Returning a non empty string will reject the login event.
// If you don't need to reject the login event, see UserHasLoggedIn
//
// Minimum server version: 5.2
UserWillLogIn(c *Context, user *model.User) string
// UserHasLoggedIn is invoked after a user has logged in.
//
// Minimum server version: 5.2
UserHasLoggedIn(c *Context, user *model.User)
// MessageWillBePosted is invoked when a message is posted by a user before it is committed
// to the database. If you also want to act on edited posts, see MessageWillBeUpdated.
//
// To reject a post, return an non-empty string describing why the post was rejected.
// To modify the post, return the replacement, non-nil *model.Post and an empty string.
// To allow the post without modification, return a nil *model.Post and an empty string.
// To dismiss the post, return a nil *model.Post and the const DismissPostError string.
//
// If you don't need to modify or reject posts, use MessageHasBeenPosted instead.
//
// Note that this method will be called for posts created by plugins, including the plugin that
// created the post.
//
// Minimum server version: 5.2
MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string)
// MessageWillBeUpdated is invoked when a message is updated by a user before it is committed
// to the database. If you also want to act on new posts, see MessageWillBePosted.
// Return values should be the modified post or nil if rejected and an explanation for the user.
// On rejection, the post will be kept in its previous state.
//
// If you don't need to modify or rejected updated posts, use MessageHasBeenUpdated instead.
//
// Note that this method will be called for posts updated by plugins, including the plugin that
// updated the post.
//
// Minimum server version: 5.2
MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string)
// MessageHasBeenPosted is invoked after the message has been committed to the database.
// If you need to modify or reject the post, see MessageWillBePosted
// Note that this method will be called for posts created by plugins, including the plugin that
// created the post.
//
// Minimum server version: 5.2
MessageHasBeenPosted(c *Context, post *model.Post)
// MessageHasBeenUpdated is invoked after a message is updated and has been updated in the database.
// If you need to modify or reject the post, see MessageWillBeUpdated
// Note that this method will be called for posts created by plugins, including the plugin that
// created the post.
//
// Minimum server version: 5.2
MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post)
// MessagesWillBeConsumed is invoked when a message is requested by a client before it is returned
// to the client
//
// Note that this method will be called for posts created by plugins, including the plugin that
// created the post.
//
// Minimum server version: 9.3
MessagesWillBeConsumed(posts []*model.Post) []*model.Post
// MessageHasBeenDeleted is invoked after the message has been deleted from the database.
// Note that this method will be called for posts deleted by plugins, including the plugin that
// deleted the post.
//
// Minimum server version: 9.1
MessageHasBeenDeleted(c *Context, post *model.Post)
// ChannelHasBeenCreated is invoked after the channel has been committed to the database.
//
// Minimum server version: 5.2
ChannelHasBeenCreated(c *Context, channel *model.Channel)
// UserHasJoinedChannel is invoked after the membership has been committed to the database.
// If actor is not nil, the user was invited to the channel by the actor.
//
// Minimum server version: 5.2
UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User)
// UserHasLeftChannel is invoked after the membership has been removed from the database.
// If actor is not nil, the user was removed from the channel by the actor.
//
// Minimum server version: 5.2
UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User)
// UserHasJoinedTeam is invoked after the membership has been committed to the database.
// If actor is not nil, the user was added to the team by the actor.
//
// Minimum server version: 5.2
UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User)
// UserHasLeftTeam is invoked after the membership has been removed from the database.
// If actor is not nil, the user was removed from the team by the actor.
//
// Minimum server version: 5.2
UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User)
// FileWillBeUploaded is invoked when a file is uploaded, but before it is committed to backing store.
// Read from file to retrieve the body of the uploaded file.
//
// To reject a file upload, return an non-empty string describing why the file was rejected.
// To modify the file, write to the output and/or return a non-nil *model.FileInfo, as well as an empty string.
// To allow the file without modification, do not write to the output and return a nil *model.FileInfo and an empty string.
//
// Note that this method will be called for files uploaded by plugins, including the plugin that uploaded the post.
// FileInfo.Size will be automatically set properly if you modify the file.
//
// Minimum server version: 5.2
FileWillBeUploaded(c *Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string)
// FileWillBeDownloaded is invoked when a file is requested for download, but before it is sent to the client.
//
// To reject a file download, return an non-empty string describing why the file was rejected.
// To allow the download, return an empty string.
//
// The downloadType parameter indicates the type of file access and can be one of:
// - model.FileDownloadTypeFile: Full file download
// - model.FileDownloadTypeThumbnail: Thumbnail request
// - model.FileDownloadTypePreview: Preview image request
// - model.FileDownloadTypePublic: Public link access (userID will be empty string in this case)
//
// Minimum server version: 11.5
FileWillBeDownloaded(c *Context, fileInfo *model.FileInfo, userID string, downloadType model.FileDownloadType) string
// ReactionHasBeenAdded is invoked after the reaction has been committed to the database.
//
// Note that this method will be called for reactions added by plugins, including the plugin that
// added the reaction.
//
// Minimum server version: 5.30
ReactionHasBeenAdded(c *Context, reaction *model.Reaction)
// ReactionHasBeenRemoved is invoked after the removal of the reaction has been committed to the database.
//
// Note that this method will be called for reactions removed by plugins, including the plugin that
// removed the reaction.
//
// Minimum server version: 5.30
ReactionHasBeenRemoved(c *Context, reaction *model.Reaction)
// OnPluginClusterEvent is invoked when an intra-cluster plugin event is received.
//
// This is used to allow communication between multiple instances of the same plugin
// that are running on separate nodes of the same High-Availability cluster.
// This hook receives events sent by a call to PublishPluginClusterEvent.
//
// Minimum server version: 5.36
OnPluginClusterEvent(c *Context, ev model.PluginClusterEvent)
// OnWebSocketConnect is invoked when a new websocket connection is opened.
//
// This is used to track which users have connections opened with the Mattermost
// websocket.
//
// Minimum server version: 6.0
OnWebSocketConnect(webConnID, userID string)
// OnWebSocketDisconnect is invoked when a websocket connection is closed.
//
// This is used to track which users have connections opened with the Mattermost
// websocket.
//
// Minimum server version: 6.0
OnWebSocketDisconnect(webConnID, userID string)
// WebSocketMessageHasBeenPosted is invoked when a websocket message is received.
//
// Minimum server version: 6.0
WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest)
// RunDataRetention is invoked during a DataRetentionJob.
//
// Minimum server version: 6.4
RunDataRetention(nowTime, batchSize int64) (int64, error)
// OnInstall is invoked after the installation of a plugin as part of the onboarding.
// It's called on every installation, not only once.
//
// In the future, other plugin installation methods will trigger this hook, e.g. an installation via the Marketplace.
//
// Minimum server version: 6.5
OnInstall(c *Context, event model.OnInstallEvent) error
// OnSendDailyTelemetry is invoked when the server send the daily telemetry data.
//
// Minimum server version: 6.5
OnSendDailyTelemetry()
// OnCloudLimitsUpdated is invoked product limits change, for example when plan tiers change
//
// Minimum server version: 7.0
OnCloudLimitsUpdated(limits *model.ProductLimits)
// ConfigurationWillBeSaved is invoked before saving the configuration to the
// backing store.
// An error can be returned to reject the operation. Additionally, a new
// config object can be returned to be stored in place of the provided one.
// Minimum server version: 8.0
ConfigurationWillBeSaved(newCfg *model.Config) (*model.Config, error)
// EmailNotificationWillBeSent is invoked before an email notification is sent to a user.
// This allows plugins to customize the email notification content including subject,
// title, subtitle, message content, buttons, and other email properties.
//
// To reject an email notification, return an non-empty string describing why the notification was rejected.
// To modify the notification, return the replacement, non-nil *model.EmailNotificationContent and an empty string.
// To allow the notification without modification, return a nil *model.EmailNotificationContent and an empty string.
//
// Note that core identifiers (PostId, ChannelId, TeamId, SenderId, RecipientId, RootId) and
// context fields (ChannelType, IsDirectMessage, etc.) are immutable and changes to them will be ignored.
// Only customizable content fields can be modified.
//
// Minimum server version: 11.00
EmailNotificationWillBeSent(emailNotification *model.EmailNotification) (*model.EmailNotificationContent, string)
// NotificationWillBePushed is invoked before a push notification is sent to the push
// notification server.
//
// To reject a notification, return an non-empty string describing why the notification was rejected.
// To modify the notification, return the replacement, non-nil *model.PushNotification and an empty string.
// To allow the notification without modification, return a nil *model.PushNotification and an empty string.
//
// Note that this method will be called for push notifications created by plugins, including the plugin that
// created the notification.
//
// Minimum server version: 9.0
NotificationWillBePushed(pushNotification *model.PushNotification, userID string) (*model.PushNotification, string)
// UserHasBeenDeactivated is invoked when a user is deactivated.
//
// Minimum server version: 9.1
UserHasBeenDeactivated(c *Context, user *model.User)
// ServeMetrics allows plugins to expose their own metrics endpoint through
// the server's metrics HTTP listener (e.g. "localhost:8067").
// Requests destined to the /plugins/{id}/metrics path will be routed to the plugin.
//
// Minimum server version: 9.2
ServeMetrics(c *Context, w http.ResponseWriter, r *http.Request)
// OnSharedChannelsSyncMsg is invoked for plugins that wish to receive synchronization messages from the
// Shared Channels service for which they have been invited via InviteRemote. Each SyncMsg may contain
// multiple updates (posts, reactions, attachments, users) for a single channel.
//
// The cursor will be advanced based on the SyncResponse returned.
//
// Minimum server version: 9.5
OnSharedChannelsSyncMsg(msg *model.SyncMsg, rc *model.RemoteCluster) (model.SyncResponse, error)
// OnSharedChannelsPing is invoked for plugins to indicate the health of the plugin and the connection
// to the upstream service (e.g. MS Graph APIs).
//
// Return true to indicate all is well.
//
// Return false to indicate there is a problem with the plugin or connection to upstream service.
// Some number of failed pings will result in the plugin being marked offline and it will stop receiving
// OnSharedChannelsSyncMsg calls until it comes back online. The plugin will also appear offline in the status
// report via the `secure-connection status` slash command.
//
// Minimum server version: 9.5
OnSharedChannelsPing(rc *model.RemoteCluster) bool
// PreferencesHaveChanged is invoked after one or more of a user's preferences have changed.
// Note that this method will be called for preferences changed by plugins, including the plugin that changed
// the preferences.
//
// Minimum server version: 9.5
PreferencesHaveChanged(c *Context, preferences []model.Preference)
// OnSharedChannelsAttachmentSyncMsg is invoked for plugins that wish to receive synchronization messages from the
// Shared Channels service for which they have been invited via InviteRemote. Each call represents one file attachment
// to be synchronized.
//
// The cursor will be advanced based on the timestamp returned if no error is returned.
//
// Minimum server version: 9.5
OnSharedChannelsAttachmentSyncMsg(fi *model.FileInfo, post *model.Post, rc *model.RemoteCluster) error
// OnSharedChannelsProfileImageSyncMsg is invoked for plugins that wish to receive synchronization messages from the
// Shared Channels service for which they have been invited via InviteRemote. Each call represents one user profile
// image that should be synchronized. `App.GetProfileImage` can be used to fetch the image bytes.
//
// The cursor will be advanced based on the timestamp returned if no error is returned.
//
// Minimum server version: 9.5
OnSharedChannelsProfileImageSyncMsg(user *model.User, rc *model.RemoteCluster) error
// GenerateSupportData is invoked when a Support Packet gets generated.
// It allows plugins to include their own content in the Support Packet.
//
// Plugins may specififes a "support_packet" field in the manifest props with a custom text.
// By doing so, the plugin will be included in the Support Packet UI and the user will be able to select it.
// This hook will only be called, if the user selects the plugin in the Support Packet UI.
//
// If no "support_packet" is specified, this hook will always be called.
//
// Minimum server version: 9.8
GenerateSupportData(c *Context) ([]*model.FileData, error)
// OnSAMLLogin is invoked after a successful SAML login.
//
// Minimum server version: 10.7
OnSAMLLogin(c *Context, user *model.User, assertion *saml2.AssertionInfo) error
}