[{"categories":null,"contents":"This tutorial will walk you through the basics of writing a Mattermost plugin with a server component.\nNote that the steps below are intentionally very manual to explain all of the pieces fitting together. In practice, we recommend referencing mattermost-plugin-starter-template for helpful build scripts. Also, the plugin API changed in Mattermost 5.2. Consult the migration document to upgrade older plugins.\nPrerequisites Mattermost plugins extend the server using a Go API. In the future, gRPC may be supported, allowing you to write plugins in any language. For now, you’ll need a functioning Go environment, so follow Go's Getting Started guide if needed.\nYou’ll also need a Mattermost server to install and test the plugin. This server must have Enable set to true in the PluginSettings section of its config file. If you want to upload plugins via the System Console or API, you’ll also need to set EnableUploads to true in the same section.\nBuild the plugin The process that will communicate with the Mattermost server is built using a set of APIs provided by the source code for the Mattermost server.\nDownload the source code for the Mattermost server:\ngo get -u github.com/mattermost/mattermost/server/v8 Define $GOPATH. By default, this is already $HOME/go, but it’s helpful to make this explicit:\nexport GOPATH=$HOME/go Now, create a directory to act as your workspace:\nmkdir -p $GOPATH/src/my-plugin cd $GOPATH/src/my-plugin Create a file named plugin.go with the following contents:\npackage main import ( \"fmt\" \"net/http\" \"github.com/mattermost/mattermost/server/public/plugin\" ) // HelloWorldPlugin implements the interface expected by the Mattermost server to communicate // between the server and plugin processes. type HelloWorldPlugin struct { plugin.MattermostPlugin } // ServeHTTP demonstrates a plugin that handles HTTP requests by greeting the world. func (p *HelloWorldPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, \"Hello, world!\") } // This example demonstrates a plugin that handles HTTP requests which respond by greeting the // world. func main() { plugin.ClientMain(\u0026HelloWorldPlugin{}) } This plugin will register an HTTP handler that will respond with “Hello, world!” when requested.\nBuild the executable that will be distributed with your plugin:\ngo build -o plugin.exe plugin.go Note: Your executable is platform specific! If you’re building the plugin for a server running on a different operating system, you’ll need to use a slightly different command. For example, if you’re developing the plugin from MacOS and deploying to a Linux server, you’ll need to use this command: GOOS=linux GOARCH=amd64 go build -o plugin.exe plugin.go Also note that the “.exe” extension is required if you’d like your plugin to run on Windows, but is otherwise optional. Consider referencing mattermost-plugin-starter-template for helpful build scripts.\nNow, we’ll need to define the required manifest describing your plugin’s entry point. Create a file named plugin.json with the following contents:\n{ \"id\": \"com.mattermost.server-hello-world\", \"name\": \"Hello World\", \"server\": { \"executable\": \"plugin.exe\" } } This manifest gives the server the location of your executable within your plugin bundle. Consult the manifest reference for more details, including how to define a cross-platform bundle by defining multiple executables, and how to define a minimum required server version for your plugin.\nNote that you may also use plugin.yaml to define the manifest.\nBundle the manifest and executable into a tar file:\ntar -czvf plugin.tar.gz plugin.exe plugin.json You should now have a file named plugin.tar.gz in your workspace. Congratulations! This is your first server plugin!\nInstall the plugin Install the plugin in one of the following ways:\nThrough System Console UI:\nLog in to Mattermost as a System Admin. Open the System Console at /admin_console Navigate to Plugins \u003e Plugin Management* and upload the plugin.tar.gz you generated above. Click Enable under the plugin after it has uploaded. Or, manually:\nExtract plugin.tar.gz to a folder with the same name as the plugin id you specified in plugin.json, in this case com.mattermost.server-hello-world/.\nAdd the plugin to the directory set by PluginSettings \u003e Directory in your config.json file. If none is set, defaults to ./plugins relative to your Mattermost installation directory. The resulting directory structure should look something like:\nmattermost/ plugins/ com.mattermost.server-hello-world/ plugin.json plugin.exe Restart the Mattermost server.\nOnce you’ve installed the plugin in one of the ways above, browse to https://\u003cyour-mattermost-server\u003e/plugins/com.mattermost.server-hello-world, and you’ll be greeted by your plugin.\n","permalink":"https://developers.mattermost.com/integrate/plugins/components/server/hello-world/","section":"integrate","subsection":"plugins","tags":null,"title":"Plugin quick start"},{"categories":null,"contents":"This tutorial will walk you through the basics of extending the Mattermost web app.\nNote that the steps below are intentionally very manual to explain all of the pieces fitting together. In practice, we recommend referencing mattermost-plugin-starter-template for helpful build scripts. Also, the plugin API changed in Mattermost 5.2. Consult the migration document to upgrade older plugins.\nPrerequisites Plugins, just like the Mattermost web app itself, are built using ReactJS with Redux. Make sure to install npm to manage your JavaScript dependencies.\nYou’ll also need a Mattermost server to install and test the plugin. This server must have Enable set to true in the PluginSettings section of its config file. If you want to upload plugins via the System Console or API, you’ll also need to set EnableUploads to true in the same section.\nSet up the workspace Create a directory to act as your plugin workspace. With that directory, create and switch to a webapp directory:\nmkdir webapp cd webapp Install the necessary NPM dependencies:\nnpm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader webpack webpack-cli npm install --save react Configure Webpack by creating a webpack.config.js file:\nvar path = require('path'); module.exports = { entry: [ './src/index.jsx', ], resolve: { modules: [ 'src', 'node_modules', ], extensions: ['*', '.js', '.jsx'], }, module: { rules: [ { test: /\\.(js|jsx)$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-react', [ \"@babel/preset-env\", { \"modules\": \"commonjs\", \"targets\": { \"node\": \"current\" } } ] ], }, }, }, ], }, externals: { react: 'React', }, output: { path: path.join(__dirname, '/dist'), publicPath: '/', filename: 'main.js', }, }; Observe that react is specified as an external library. This allows you to test your code locally (e.g. with jest and snapshots) but leverage the version of React shipped with Mattermost to avoid bloating your plugin.\nNow create the entry point file and output directory:\nmkdir src dist touch src/index.jsx Then populate src/index.jsx with the following:\nimport React from 'react'; // Courtesy of https://feathericons.com/ const Icon = () =\u003e \u003ci className='icon fa fa-plug'/\u003e; class HelloWorldPlugin { initialize(registry, store) { registry.registerChannelHeaderButtonAction( // icon - JSX element to use as the button's icon \u003cIcon /\u003e, // action - a function called when the button is clicked, passed the channel and channel member as arguments // null, () =\u003e { alert(\"Hello World!\"); }, // dropdown_text - string or JSX element shown for the dropdown button description \"Hello World\", ); } } window.registerPlugin('com.mattermost.webapp-hello-world', new HelloWorldPlugin()); Generate a minified bundle ready to install as a web app plugin:\n./node_modules/.bin/webpack --mode=production Now, we’ll need to define the required manifest describing your plugin’s entry point. Create a file named plugin.json with the following contents:\n{ \"id\": \"com.mattermost.webapp-hello-world\", \"name\": \"Hello World\", \"webapp\": { \"bundle_path\": \"main.js\" } } This manifest gives the server the location of your components within your plugin bundle. Consult the manifest reference for more details, including how to define a minimum required server version for your plugin.\nNote that you may also use plugin.yaml to define the manifest.\nBundle the manifest and entry point into a tar file:\nmkdir -p com.mattermost.webapp-hello-world cp -r dist/main.js com.mattermost.webapp-hello-world/ cp plugin.json com.mattermost.webapp-hello-world/ tar -czvf plugin.tar.gz com.mattermost.webapp-hello-world You should now have a file named plugin.tar.gz in your workspace. Congratulations! This is your first web app plugin!\nInstall the plugin Install the plugin in one of the following ways:\nThrough System Console UI:\nLog in to Mattermost as a System Admin. Open the System Console at /admin_console Navigate to Plugins \u003e Plugin Management and upload the plugin.tar.gz you generated above. Click Enable under the plugin after it has uploaded. Or, manually:\nExtract plugin.tar.gz to a folder with the same name as the plugin id you specified in plugin.json, in this case com.mattermost.server-hello-world/.\nAdd the plugin to the directory set by PluginSettings \u003e Directory in your config.json file. If none is set, defaults to ./plugins relative to your Mattermost installation directory. The resulting directory structure should look something like:\nmattermost/ plugins/ com.mattermost.webapp-hello-world/ plugin.json main.js Restart the Mattermost server.\nEnable the plugin in System Console \u003e Plugins \u003e Plugin Management.\nNavigate to a regular Mattermost page and observe the new icon in the channel header. Click the icon and observe the alert dialog.\n","permalink":"https://developers.mattermost.com/integrate/plugins/components/webapp/hello-world/","section":"integrate","subsection":"plugins","tags":null,"title":"Web app quick start"},{"categories":null,"contents":"This guide is your reference for all Mattermost contributions. The following is a brief summary to help you find what you’re looking for:\nWhy and how to contribute: How to get started contributing to Mattermost projects.\nCommunity expectations: How to communicate and interact effectively, respectfully, and inclusively with other members of the Mattermost community.\nContributor expectations: What is expected of you throughout the whole contribution process, including guidelines to follow when producing new code and content.\nWhere to find more information: Where to find more information on the contribution process beyond the guidance offered in this guide.\nPlease consult this guide as your official reference when contributing at Mattermost.\n","permalink":"https://developers.mattermost.com/contribute/","section":"contribute","subsection":null,"tags":null,"title":"Contributor guide"},{"categories":null,"contents":"Config module The configuration module in the Common module is responsible for facilitating reading from and writing to external configuration sources. It also consolidates, verifies, and upgrades configuration where applicable.\nFiles We have a few different configuration files in the Desktop App, but the main one is config.json. Most of the user’s configuration from the Settings Window is stored there, as well as any user-configured servers.\nThe application supports different configuration versions and allows for them to be migrating to the version supported by the configuration module via the upgradePreferences module. When no configuration is found, the defaultPreferences object is copied over to the main configuration module.\nWe also support a build configuration in which the packager of the application can pre-define servers and a few other configuration items.\nRegistry We support reading from the Windows registry to allow system administrators to define Group Policy that will pre-define servers and potentially disable user-defined servers and automatic updates as per administrator wishes.\nTemplates for these can be found under resources\\windows\\gpo.\nServer manager The ServerManager is a singleton class that acts as a single source of truth for all server configuration, managing adding/modifying/removing servers and serving the server information to the rest of the application.\nInitialization We populate the ServerManager with all servers provided by the configuration module, marking them as pre-defined when applicable to not allow the user to modify them. Servers are given a unique UUID when the app initialized, and this UUID acts as the global way of identifying the server to the rest of the application.\nAn external call is responsible for populating information about the specific Mattermost server (eg. server version, plugins installed), but the data is stored within the ServerManager.\nModification The ServerManager is the only place that allows the persistent server configuration to be modified. Changes cannot be made directly through the configuration module. Once a server is modified, the ServerManager will update the configuration module with the new changes.\nWhen a server is added or updating, up to two events will be emitted:\nSERVERS_UPDATE: This event is emitted when the ServerManager has new changes. This could be a name, URL or an ordering change. SERVERS_URL_MODIFIED: This event is emitted specifically when a URL has changed, signifying that the application might need to fetch new remote server information or refresh any views associated with the server to reflect the new URL. Lookup We provide a server lookup call that allows for an arbitrary URL to be provided and potentially matched to a server. If found, the UUID will be provided. This function is useful for deep linking or for cross-server linking, when only a URL is available when the request is provided.\n","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/architecture/configuration/","section":"contribute","subsection":"more info","tags":null,"title":"Configuration"},{"categories":null,"contents":"This guide will help you configure your developer environment for the Focalboard Personal Server. For most features, this is the easiest way to get started working against code that ships across editions. For working with Mattermost Boards (Focalboard as a plugin), please refer to the Mattermost Boards Plugin Guide.\nInstall prerequisites All Git (if using Windows, see below) Go Node.js (v10+) npm Windows Install MinGW-w64 via Chocolatey Install Git for Windows and use the git-bash terminal shell Mac Install Xcode (v12+) Install the Xcode Command Line Tools via xcode-select --install Linux sudo apt-get install libgtk-3-dev sudo apt-get install libwebkit2gtk-4.0-dev sudo apt-get install autoconf dh-autoreconf Fork the project repositories Fork the Focalboard GitHub repository and Mattermost GitHub repository. Clone both repositories locally in sibling directories.\nBuild via the terminal To build the server:\nmake prebuild make To run the server:\n./bin/focalboard-server Then navigate your browser to `http://localhost:8000` to access your Focalboard server. The port is configured in config.json.\nOnce the server is running, you can rebuild just the web app via make webapp in a separate terminal window. Reload your browser to see the changes.\nBuild and run standalone desktop apps You can build standalone apps that package the server to run locally against SQLite:\nWindows: Requires Windows 10, Windows 10 SDK 10.0.19041.0, and .NET 4.8 developer pack Open a git-bash prompt. Run make prebuild The above prebuild step needs to be run only when you make changes to or want to install your npm dependencies, etc. Once the prebuild is completed, you can keep repeating the below steps to build the app \u0026 see the changes. Run make win-wpf-app Run cd win-wpf/msix \u0026\u0026 focalboard.exe Mac: Requires macOS 11.3+ and Xcode 13.2.1+ Run make prebuild The above prebuild step needs to be run only when you make changes to or want to install your npm dependencies, etc. Once the prebuild is completed, you can keep repeating the below steps to build the app \u0026 see the changes. Run make mac-app Run open mac/dist/Focalboard.app Linux: Tested on Ubuntu 18.04 Install webgtk dependencies Run sudo apt-get install libgtk-3-dev Run sudo apt-get install libwebkit2gtk-4.0-dev Run make prebuild The above prebuild step needs to be run only when you make changes to or want to install your npm dependencies, etc. Once the prebuild is completed, you can keep repeating the below steps to build the app \u0026 see the changes. Run make linux-app Uncompress linux/dist/focalboard-linux.tar.gz to a directory of your choice Run focalboard-app from the directory you have chosen Docker: To run it locally from offical image: docker run -it -p 80:8000 mattermost/focalboard To build it for your current architecture: docker build -f docker/Dockerfile . To build it for a custom architecture (experimental): docker build -f docker/Dockerfile --platform linux/arm64 . Cross-compilation currently isn’t fully supported, so please build on the appropriate platform. Refer to the GitHub Actions workflows (build-mac.yml, build-win.yml, build-ubuntu.yml) for the detailed list of steps on each platform.\nSet up VS Code Open a VS Code terminal window in the project folder. Run make prebuild to install packages. Do this whenever dependencies change in webapp/package.json. Run cd webapp \u0026\u0026 npm run watchdev to automatically rebuild the web app when files are changed. It also includes source maps from JavaScript to TypeScript. Install the Go and ESLint VS Code extensions (if you haven’t already). Launch the server: Windows: Ctrl+P, type debug, press the Space key, and select Go: Launch Server. Mac: Cmd+P, type debug, press the Space key, and select Go: Launch Server. If you do not see Go: Launch Server as an option, check your ./.vscode/launch.json file and make sure you are not using a VS Code workspace. Navigate a browser to http://localhost:8000 You can now edit the web app code and refresh the browser to see your changes efficiently.\nDebugging the web app: As a starting point, add a breakpoint to the render() function in BoardPage.tsx and refresh the browser to walk through page rendering.\nDebugging the server: As a starting point, add a breakpoint to handleGetBlocks() in server/api/api.go and refresh the browser to see how data is retrieved.\nRebuild translations We use i18n to localize the web app. Localized string generally use intl.formatMessage. When adding or modifying localized strings, run npm run i18n-extract in webapp to rebuild webapp/i18n/en.json.\nTranslated strings are stored in other json files under webapp/i18n, (e.g. es.json for Spanish).\nAccess the database By default, data is stored in a sqlite database focalboard.db. You can view and edit this directly using sqlite3 focalboard.db.\nUnit tests Run make ci, which is similar to the .gitlab-ci.yml workflow and includes:\nServer unit tests: make server-test Web app ESLint: cd webapp; npm run check Web app unit tests: cd webapp; npm run test Web app UI tests: cd webapp; npm run cypress:ci Unit tests for Focalboard are similar to the web app and server testing requirements.\nStaying informed Are you interested in influencing the future of the Focalboard open source project? Please read the Focalboard Contribution Guide. We welcome everyone and appreciate any feedback. ❤️ There are several ways you can get involved:\nChanges: See the CHANGELOG for the latest updates GitHub Discussions: Join the Developer Discussion board Bug Reports: File a bug report Chat: Join the Focalboard community channel ","permalink":"https://developers.mattermost.com/contribute/more-info/focalboard/personal-server-setup-guide/","section":"contribute","subsection":"more info","tags":null,"title":"Personal server setup guide"},{"categories":null,"contents":"Set up your development environment for building, running, and testing the Mattermost Desktop App.\nDependencies macOS Windows Ubuntu Arch Linux Fedora/RedHat/CentOS Install Homebrew: http://brew.sh\nOpen Terminal\nInstall dependencies\nbrew install git python3 Install NVM by following these instructions.\nAfter installing, follow the post-install steps shown by the installer to add the necessary lines to your shell profile (for example ~/.zshrc or ~/.bash_profile). Then open a new terminal and run:\nnvm install --lts Open Terminal\nInstall NodeJS from one of the following sources:\nnvm:\ncurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash nvm install --lts NodeSource:\nsudo apt-get update sudo apt-get install -y ca-certificates curl gnupg sudo mkdir -p /etc/apt/keyrings curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main\" | sudo tee /etc/apt/sources.list.d/nodesource.list sudo apt-get update sudo apt-get install -y nodejs You might need to install curl as well:\nsudo apt install curl Install other dependencies:\nLinux requires the X11 developement libraries and libpng to build native Node modules.\nsudo apt install git python3 make g++ libx11-dev libxtst-dev libpng-dev Notes To build RPMs, you need rpmbuild:\nsudo apt install rpm Install Chocolatey: https://chocolatey.org/install\nInstall Visual Studio Community: https://visualstudio.microsoft.com/vs/community/\nInclude Desktop development with C++ when installing If you are on Windows 11, you may need to install wmic via the system settings \u003e Optional Features.\nOpen PowerShell\nInstall dependencies\nchoco install nvm git python3 Restart PowerShell (to refresh the environment variables)\nRun nvm install lts and nvm use lts to install and use the latest NodeJS LTS version.\nNOTE: We don’t officially support Arch Linux for use with the Mattermost Desktop App. The provided guide is unofficial.\nOpen a terminal\nInstall nvm via\nnvm-sh: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash OR AUR (possibly using a helper): yay -S nvm Install NodeJS via\nnvm install --lts Install other dependencies:\nLinux requires the X11 development libraries and libpng to build native Node modules. Arch requires libffi since it’s not installed by default.\nsudo pacman -S npm git python3 gcc make libx11 libxtst libpng libffi Notes To build RPMs, you need rpmbuild\nsudo pacman -S rpm NOTE: We don’t officially support Fedora/Red Hat/CentOS Linux for use with the Mattermost Desktop App. The provided guide is unofficial.\nOpen Terminal\nInstall NodeJS via nvm:\ncurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash nvm install --lts Install other dependencies:\nLinux requires the X11 developement libraries and libpng to build native Node modules.\nsudo yum install git python3 g++ libX11-devel libXtst-devel libpng-devel Notes To build RPMs, you need rpmbuild:\nsudo dnf install rpm-build Mattermost Server To develop with the Desktop App, we recommend that you set up a Mattermost server specifically for this purpose. This lets you customize it as needed in cases where there are specific integration requirements needed for testing.\nYou can find information on setting that up here:\nDeveloper Setup\nAlternatively, for some changes you may be able to test using an existing Mattermost instance, or one that has been deployed on platforms like Docker, Linux, Kubernetes, Heroku, or others. Please refer to the Mattermost Deployment Guide for more info.\nRepo setup Fork GitHub Repository: https://github.com/mattermost/desktop\nClone from your repo:\ngit clone https://github.com/\u003cYOUR_GITHUB_USERNAME\u003e/desktop.git Open the desktop directory\ncd desktop Install Node Modules\nnpm i Run the application\nnpm run watch ","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/developer-setup/","section":"contribute","subsection":"more info","tags":null,"title":"Developer setup"},{"categories":null,"contents":"Contributions for minor corrections and improvements without a corresponding Help Wanted ticket are welcome. For example, a pull request for a bug or incremental improvement, with less than 20 lines of code change, is usually accepted if the change to existing behaviour is minor.\nAll pull requests submitted without a corresponding ticket will first be reviewed by a core team product manager. Some examples of minor corrections and improvements include:\nFix a formatting error in help text Fix success typo in Makefile Fix broken Cancel button in Edit Webhooks screen Fix Android app crashing when saving user notification settings Fix recent mentions search not working Note: For pull requests greater than 20 lines of code, a Help Wanted ticket should be opened by the core team. This helps ensure that everything going into the project aligns with a unified vision. Core committers who review the PR are entitled to reject it if there isn’t a Help Wanted ticket and feel it significantly changes behavior or user expectations. Note: Please use our translation server to correct errors in translation. The best way to discuss opening a Help Wanted ticket with the core team is by starting a conversation in the feature idea forum or opening an issue in the GitHub repository. Alternatively, don’t hesitate to come chat about it in the Contributors or Developers channels.\n","permalink":"https://developers.mattermost.com/contribute/more-info/getting-started/contributions-without-ticket/","section":"contribute","subsection":"more info","tags":null,"title":"Contributions without ticket"},{"categories":null,"contents":"At times, you may want to build your own Mattermost mobile app. The most common use cases are:\nTo white label the Mattermost mobile app. To use your own deployment of the Mattermost Push Notification Service (always required if you are building your own version of the mobile app). Build preparations 1. Package name and source files Ensure the package ID of the mobile app remains the same as the one in the original mattermost-mobile GitHub repository in com.mattermost.rnbeta. Source files for the main package remain under the android/app/src/main/java/com/mattermost/rnbeta folder. 2. Generate a signing key As Android requires all apps to be digitally signed with a certificate before they can be installed building the Android app for distribution requires the release APK to be signed.\nTo generate the signed key, use keytool which comes with the JDK required to develop the Android app. (see Developer Setup).\n$ keytool -genkey -v -keystore \u003cmy-release-key\u003e.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000 The above command prompts you for passwords for the keystore and key (make sure you use the same password for both), and asks you to provide the Distinguished Name fields for your key. It then generates the keystore as a file called \u003cmy-release-key\u003e.keystore.\nThe keystore contains a single key, valid for 10000 days. The alias is a name that you will use later when signing your app, so remember to take a note of the alias.\nNote: Replace \u003cmy-release-key\u003e with the filename you want to specify. Remember to keep your keystore file private and never commit it to version control. 3. Create a new app in Google Play Create a new application using the Google Play console. If you already have an app registered in the Google Play console you can skip this step.\n4. Set up Gradle variables Now that we have created the keystore file we can tell the build process to use that file:\nCopy or move the my-release-key.keystore file under a directory that you can access. It can be in your home directory or anywhere in the file system.\nEdit the gradle.properties file in your $HOME directory (e.g. $HOME/.gradle/gradle.properties), or create it if one does not exist, and add the following:\nMATTERMOST_RELEASE_STORE_FILE=/full/path/to/directory/containing/my-release-key.keystore MATTERMOST_RELEASE_KEY_ALIAS=my-key-alias MATTERMOST_RELEASE_PASSWORD=***** Note: Replace /full/path/to/directory/containing/my-release-key.keystore with the full path to the actual keystore file and ******** with the actual keystore password. Back up your keystore and don’t forget the password. Important:\nOnce you publish the app on the Play Store, the app needs to be signed with the same key every time you want to distribute a new build. If you lose this key, you will need to republish your app under a different package id (losing all downloads and ratings).\n5. Configure environment variables To make it easier to customize your build, we’ve defined a few environment variables that are going to be used by Fastlane during the build process.\nVariable Description Default Required COMMIT_CHANGES_TO_GIT Should the Fastlane script ensure that there are no changes to Git before building the app and that every change made during the build is committed back to Git. Valid values are: true, false false No BRANCH_TO_BUILD Defines the Git branch that is going to be used for generating the build. Make sure that, if this value is set, the branch it is set to exists. $GIT_BRANCH No GIT_LOCAL_BRANCH Defines the local branch to be created from BRANCH_TO_BUILD to ensure the base branch does not get any new commits on it. Make sure a branch with this name does not yet exist in your local Git repository. build No RESET_GIT_BRANCH Defines if, once the build is done, the branch should be reset to the initial state before building and whether to also delete the branch created to build the app. Valid values are: true, false false No VERSION_NUMBER Set the version of the app at build time to a specific value, rather than using the one set in the project. No INCREMENT_VERSION_NUMBER_MESSAGE Set the commit message when changing the app version number. Bump app version number to No INCREMENT_BUILD_NUMBER Defines if the app build number should be incremented. Valid values are: true, false false No BUILD_NUMBER Set the build number of the app at build time to a specific value, rather than incrementing the last build number. No INCREMENT_BUILD_NUMBER_MESSAGE Set the commit message when changing the app build number. Bump app build number to No ANDROID_BUILD_TASK The build tasks for Android. This is a comma-separated list of tasks that can have two values: ‘assemble’ and ‘bundle’. assemble is used for building APK file and bundle is used for building AAB file. assemble No APP_NAME The name of the app as it is going to be shown on the device home screen. Mattermost Beta Yes APP_SCHEME The URL naming scheme for the app as used in direct deep links to app content from outside the app. mattermost No REPLACE_ASSETS Override the assets as described in White Labeling. Valid values are: true, false false No MAIN_APP_IDENTIFIER The package identifier for the app. Yes BUILD_FOR_RELEASE Defines if the app should be built in release mode. Valid values are: true, false Make sure you set this value to true if you plan to submit this app Google Play or distribute it in any other way. false Yes SEPARATE_APKS Build one APK per achitecture (armeabi-v7a, x86, arm64-v8a and x86_64) as well as a universal APK. The advantage is the size of the APK is reduced by about 4MB. People will download the correct APK from the Play Store based on the CPU architecture of their device. false Yes SUBMIT_ANDROID_TO_GOOGLE_PLAY Should the app be submitted to the Play Store once it finishes building, use along with SUPPLY_TRACK.\nValid values are: true, false false Yes SUPPLY_TRACK The track of the application to use when submitting the app to Google Play Store. Valid values are: alpha, beta, production RIt is not recommended to submit the app to production. First try any of the other tracks and then promote your app using the Google Play console. alpha Yes SUPPLY_PACKAGE_NAME The package Id of your application, make sure it matches MAIN_APP_IDENTIFIER. Yes SUPPLY_JSON_KEY The path to the service account json file used to authenticate with Google.\nSee the Supply documentation to learn more. Yes Note: To configure your variables create the file ./mattermost-mobile/fastlane/.env where .env is the filename. You can find the sample file env_vars_example here. 6. Google services Replace the google-services.json file as instructed in the Android Push Notification Guide before you build the app.\nBuild the mobile app Once all the previous steps are done, execute the following command from within the project’s directory:\n$ npm run build:android This will start the build process following the environment variables you’ve set. Once it finishes, it will create the .apk file(s) with the APP_NAME as the filename in the project’s root directory. If you have not set Fastlane to submit the app to the Play Store, you can use this file to manually publish and distribute the app.\nFrequently Asked Questions How do I update the lock file? We use lockfiles to lock dependencies and make sure the builds are reproductible. If we want to update the lockfile to update all dependencies to the latest, we can run these commands:\ncd android ./gradlew app:dependencies --update-locks \"*:*\" In case we want to regenerate the lockfile from the scratch, we can delete the android/buildscript-gradle.lockfile and then run the following commands:\ncd android ./gradlew app:dependencies --write-locks ","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/build-your-own/android/","section":"contribute","subsection":"more info","tags":null,"title":"Build the Android mobile app"},{"categories":null,"contents":"The following is an overview of the Mobile app repository file structure:\n. . . a a a b d d e f i p s s t t c g h n p s u e o s a o a c h e y i i u d p s i t c l s s t r a s p r t s r e l o s i t c i r t e c h k o a c c c c d h h i i m n p q s s u t b f d n x n l h p e s l u y i c l o o o a e o 1 n a o r u c t t s a o o t a e t _ e b d t i m n n t l o 8 i n t o e r o i s n t n s s e c i e p s t a p k n t a i d r e r l e t i e x i o n o t e b e s g f u i e e s i i r s c t n t n a x a r e i c e n 1 m e e e s e n t s s r c t s s 8 a l - n # # # # n t e s a s n g e f s t s t e a i i C G A R s i s s l o i i n e o e e n r t d a n c H r c s l u o t # # # # e b i d N F i P A C a a a O a n I c s t s S t d t p i t c r w i e v l s h o o o c e a p e i r n i n e s d k s f c e c ' f i o i f s l c d s f o o e c i r s w c r c h o i v a t d p c a r o e t o r e s d i b e o e u t u x i o s t l e d b d n u e s t i p i h l e o e d n n d a t e a p h n p p e c p s i a e p s p ","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/developer-setup/structure/","section":"contribute","subsection":"more info","tags":null,"title":"Folder structure"},{"categories":null,"contents":"If plugin functionalities don’t cover your use cases, you have the freedom to customize and build your own version of the mattermost-server project.\nBefore proceeding with the steps below, make sure you have completed the mattermost-server setup process.\nCustomize the project according to your requirements.\nBuild binary files for Mattermost server.\nmake build Assemble essential files.\nmake package Transfer desired .tar.gz file to server for deployment.\n","permalink":"https://developers.mattermost.com/integrate/customization/customization/server-build/","section":"integrate","subsection":null,"tags":null,"title":"Server build (Team Edition)"},{"categories":null,"contents":"In both Mattermost Team and Enterprise editions, you have the freedom to alter how content is displayed by being able to change the contents of localized files and email templates.\nBefore proceeding with the steps below, make sure you have completed the mattermost-server and mattermost-webapp setup process.\ni18n files The i18n files define many of the contents seen in email notifications and responses from the server.\nEdit contents of files in the mattermost/server/i18n directory according to your requirements.\nOnce you’re ready, create a tarball or zip the files within the i18n directory.\ncd i18n tar -cvf i18n.tar * Before replacing the files, stop your Mattermost instance if it’s currently running.\nsudo service mattermost stop In your Mattermost deployment, back up and remove the files inside i18n that will no longer be in use.\ncd ~/mattermost/i18n tar -cvf i18n-yyyy-MM-dd.tar * rm -f *.json Transfer your custom i18n.tar file to the deployment server and extract it in the i18n directory.\nmv i18n.tar mattermost/i18n cd ~/mattermost/i18n tar -xvf i18n.tar Restart your Mattermost instance.\nsudo service mattermost start Email templates Similarly to the i18n files, email templates can be edited and applied onto a running Mattermost instance in the following manner:\nEdit the html files inside of the templates directory. In this example we’ll assume the post_body_full.html file has been customized.\nStop your Mattermost instance if it’s currently running.\nsudo service mattermost stop (optional) Rename the template file to be replaced to keep it as a backup.\ncd ~/mattermost/templates mv post_body_full.html post_body_full_yyyy_MM_dd.html Transfer your custom post_body_full.html file to the deployment server and place it inside templates.\nmv ./post_body_full.html ~/mattermost/templates/ Restart your Mattermost instance.\nsudo service mattermost start ","permalink":"https://developers.mattermost.com/integrate/customization/customization/server-files/","section":"integrate","subsection":null,"tags":null,"title":"Server files"},{"categories":null,"contents":"With every Mattermost mobile app release, we publish the Android unsigned apk in in the GitHub Releases page. This guide describes the steps needed to modify and sign the app, so it can be distributed and installed on Android devices.\nPrerequisites Apktool is a tool for reverse engineering Android apk files. XMLStarlet is a set of command line utilities (tools) which can be used to transform, query, validate, and edit XML documents and files using a simple set of shell commands in the same way it is done for plain text files using UNIX grep, sed, awk, diff, patch, join, etc., commands. JQ is like sed for JSON data - you can use it to slice, filter, map, and transform structured data with the same ease that sed, awk, and grep let you work with text. Android SDK as described in the Developer Setup. Set up keys and Google Services as described in steps 2, 3, 4, and 6 of the Build your own App guide. sign-android script to sign the Android app. Sign tool Usage: sign-android \u003cunsigned apk file\u003e [-e|--extract path] [-p|--package-id packageID] [-g|--google-services path] [-d|--display-name displayName] outputApk Usage: sign-android -h|--help Options: -e, --extract path\t(Optional) Path to extract the unsigned APK file. By default the path of the unsigned APK is used. -p, --package-id packageID\t(Optional) Specify the unique Android application ID. -g, --google-services path\t(Optional) Path to the google-services.json file. Will setup the Firebase to receive Push Notifications. Warning: will apply only if packageID is set. -d, --display-name displayName\t(Optional) Specify new application display name. By default \"Mattermost\" is used. -h, --help\tDisplay help message. Sign the Mattermost Android app Now that all requirements are met, it’s time to sign the Mattermost app for Android. Most of the options of the signing tool are optional but you should use your own package identifier, google services settings, and change the display name.\nCreate a folder that will serve as your working directory to store all the needed files.\nDownload the sign-android script and save it in your working directory.\nDownload the Android unsigned build and save it in your working directory.\nOpen a terminal to your working directory and make sure the sign-android script is executable.\n$ ls -la total 49756 drwxr-xr-x 4 user staff 128 Oct 2 08:12 . drwx------@ 59 user staff 1888 Oct 1 14:12 .. -rw-r--r-- 1 user staff 50685064 Sep 29 10:58 Mattermost-unsigned.apk -rw-r--r-- 1 user staff 2597 Oct 2 08:19 google-services.json -rwxr-xr-x 1 user staff 7005 Sep 30 12:47 sign-android Sign the app\n$ ./sign-android Mattermost-unsigned.apk -p com.example.test -g google-services.json -d \"My App\" MyApp-signed.apk Once the code sign is complete you should have a signed APK in the working directory with the name MyApp-signed.apk.\nNote: The app name can be anything but be sure to use double quotes if the name includes white spaces. If you are using a Google Services JSON file, you need to specify a package identifier that has a corresponding client in the JSON configuration file. ","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/unsigned/android/","section":"contribute","subsection":"more info","tags":null,"title":"Sign unsigned Android builds"},{"categories":null,"contents":"Customizations to the Mattermost Web App can be performed in cases where you need to customize branding, alter localization strings, or fulfill security requirements that are not immediately offered out-of-the-box.\nCustomization steps With that in mind, customizing and deploying your Mattermost Web App can be done in a few steps:\nFork the mattermost repository and then clone your fork in your local environment.\ngit clone https://github.com/\u003cyourgithubusername\u003e/mattermost Create a separate branch for your customized version, as it’s not recommended to perform them in the master branch (more details about that in the next section regarding rebasing).\ngit checkout -b custom_branch Perform customization tasks by replacing image assets, changing strings, altering the UI, and whatever else may be necessary. Be mindful not to violate any of the guidelines on trademark use during this process.\nOnce customization has been completed, build the files that will be used in your deployment and generate mattermost-webapp.tar.gz containing them.\nmake dist In a Mattermost deployment, rename client (assuming Mattermost was deployed in the $HOME directory).\ncd ~/mattermost mv client client-original Transfer compressed dist files inside Mattermost’s client directory, and decompress it.\ntar -xvf mattermost-webapp.tar.gz Copy the products subdirectory from the original deployment into the customized one.\ncp -R client-original/products client/products Rebasing to latest version Challenges arise when creating a separate custom branch from an active open-source project like mattermost. As the project gets new commits and pull requests on a daily basis, your custom webapp can quickly become outdated.\nTo deal with that, you’ll need to leverage Git’s interactive rebasing functionality in the following way:\nAdd an upstream with the original mattermost repository.\ngit remote add upstream https://github.com/mattermost/mattermost.git Update master branch to latest upstream commit.\ngit checkout master git pull upstream master Perform interactive rebase.\ngit checkout custom_branch git rebase -i master Use git rebase --continue after resolving any conflicts that arise during rebase process\nPush new version back to remote (Note: Rebasing requires that you override previous remote version with a force push. Be sure you’ve tested that your rebase was successful before completing this last command).\ngit push -f origin custom_branch ","permalink":"https://developers.mattermost.com/integrate/customization/customization/webapp/","section":"integrate","subsection":null,"tags":null,"title":"Web app"},{"categories":null,"contents":"This site is for developers who want to contribute code to the core Mattermost project. If you’re looking for other ways to contribute, head over to our website. Before getting started, it’s a good idea to review our guide on integrating and extending Mattermost because you might be able to build the improvements you want without needing to contribute them upstream.\nTechnical overview The Mattermost core repositories include:\nServer - Highly-scalable Mattermost installation written in Go Web App - JavaScript client app built on React and Redux Mobile Apps - JavaScript client apps for Android and iOS built on React Native Desktop App - An Electron wrapper around the web app project that runs on Windows, Linux, and macOS Core Plugins - A core set of officially-maintained plugins that provide a variety of improvements to Mattermost. Core Integrations - Major Mattermost features including Focalboard and Playbooks. Improvements to Mattermost may require you to contribute to multiple projects; if you’re unsure where to start, the server repository is generally the best way to get introduced to the codebase.\nHow to contribute code If you’re looking for an existing issue to help with, check out the help wanted tickets on GitHub. If you see any that you’re interested in working on, comment on it to let everyone know you’re working on it. If there’s no ticket for what you want to contribute, see our guide on contributing without a ticket.\nOnce you’ve created some code that you want to contribute, follow our pull request checklist to submit your contribution for review, and one of our core committers will reach out with any feedback, questions, or requests they have.\nHow to get help with your contribution Our contributor community is segmented into guilds that focus on specific components within the Mattermost ecosystem. Each guild has a leader and a channel on our community chat server where you can ask questions about your contribution.\n","permalink":"https://developers.mattermost.com/contribute/more-info/getting-started/","section":"contribute","subsection":"more info","tags":null,"title":"Contribute code"},{"categories":null,"contents":"The Mattermost web app is written in JavaScript using React and Redux.\nRepository It is located in the webapp directory of the main Mattermost repository.\nhttps://github.com/mattermost/mattermost/tree/master/webapp\nHelp Wanted Find help wanted tickets here.\nPackage structure The web app is set up as a monorepo which has the code broken up into multiple packages. The main packages in the web app are:\nchannels - The main web app which contains Channels, the System Console, login/signup pages, and most of the core infrastructure for the app. src/. Key folders include: actions - Contains Redux actions which make up much of the view logic for the web app components - Contains UI components and views written using React i18n - Contains the localization files for the web app packages/mattermost-redux - Contains most of the Redux logic used for handling data from the server plugins - Contains the plugin framework, utility functions and components reducers - Contains Redux reducers used for view state selectors - Contains Redux selectors used for view state tests - Contains setup code and mocks used for unit testing utils - Contains many widely-used utility functions platform - Packages used by the web app and related projects client - The JavaScript client for Mattermost’s REST API, available on NPM as @mattermost/client components - A work-in-progress package containing UI components designed to be used by different parts of Mattermost types - The TypeScript types used by Mattermost, available on NPM as @mattermost/types Important libraries and technologies React - React is a user interface library used for React apps. Its key feature is that it uses a variation of JavaScript called JSX to declaratively define interfaces using HTML-like syntax. Redux - Redux is a state management library used for JavaScript apps. Its key features are a centralized data store for the entire app and a pattern for predictably modifying and displaying that application state. Notably, we’re not using Redux Toolkit since a large portion of our Redux code predates its existence. Redux Thunk - Redux Thunk is a middleware for Redux that’s used to write async actions and logic that interacts more closely with the Redux store. React Redux - React Redux is the library used to connect React components to a Redux store. Legacy Notes Note that the webapp was previously located at https://github.com/mattermost/mattermost-webapp/. You may find additional history in this repository that was not migrated back to https://github.com/mattermost/mattermost when forming the monorepo.\n","permalink":"https://developers.mattermost.com/contribute/more-info/webapp/","section":"contribute","subsection":"more info","tags":null,"title":"Web app"},{"categories":null,"contents":"Electron The Desktop App, like all Electron apps, is broken into two pieces: the main process and the renderer process.\nThe main process is a NodeJS process that has access to operating system functions, and governs the creation and management of several renderer processes. The renderer processes are Chromium instances that perform different functions. In our app, each Mattermost server is its own renderer process. In order to facilitate communication between the two processes, there’s a communication layer in which information can be sent between. We expose ONLY the communication API to the renderer process so that we don’t allow any malicious server to wreak havoc on a user’s computer.\nYou can read more about the Process Model here.\nDirectory structure The directory structure is broken down into a few pieces to best organize the code:\nMattermost Desktop ├── docs/ - Documentation for working on the Desktop App ├── e2e/ - E2E tests │ ├── modules/ - Setup code for the E2E tests │ └── specs/ - E2E tests themselves ├── resources/ - Assets such as images or sound files that the Desktop App uses ├── scripts/ - Automated scripts used for building or packaging the Desktop App └── src/ - Application source code ├── assets/ - Assets such as images or sound files that the Desktop App uses ├── common/ - Common objects and utility functions that aren't specifically tied to Electron ├── main/ - The majority of the main process code, including setup for the Electron app ├── renderer/ - The web code for all of the main application wrapper, modals. and server dropdown views that are used by the renderer process └── types/ - Common types for use between all of the individual modules ","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/architecture/","section":"contribute","subsection":"more info","tags":null,"title":"Architecture"},{"categories":null,"contents":"The following instructions apply to the mobile apps for iOS and Android built in React Native. Download the iOS version here and the Android version here. Source code can be found at the GitHub Mattermost Mobile app repository.\nIf you run into any issues getting your environment set up, check the Troubleshooting section of the product docs for common solutions.\niOS mobile app A macOS computer is required to build the Mattermost iOS mobile app. Android mobile app Version 17 of the Java SE Development Kit (JDK) is required to develop the Mattermost Android mobile app. You can download the latest OpenJDK release of Java from Oracle, for free, under an open source license. Environment setup The following instructions apply to both iOS and Android mobile apps. On macOS, we recommend using Homebrew as a package manager.\nInstall NodeJS and NPM We recommend using NodeJS v22 and npm v10. Many of our team use nvm to manage npm and NodeJS versions.\nmacOSLinux To install NVM, follow these instructions.\nAfter installing, follow the post-install steps shown by the installer to add the necessary lines to your shell profile (for example ~/.zshrc or ~/.bash_profile). Then open a new terminal and run:\nnvm install --lts There are three available options for installing NodeJS on Linux:\nUsing NVM by following the instructions here. Install using your distribution’s package manager. Download and install the package from the NodeJS website. Note: The version of NodeJS that your distribution’s package manager supports may not be the recommended version to build Mattermost mobile apps. Please make sure that NodeJS installed by the package manager is at the recommended version. Install Watchman Watchman is a file watching program. When a file changes, Watchman triggers an action, such as re-running a build command if a source file has changed.\nThe minimum required version of Watchman is 4.9.0.\nmacOSLinux To install Watchman using Homebrew, open a terminal and execute:\nbrew install watchman Download the latest package from here.\nInotify limits Note that you need to increase your inotify limits for Watchman to work properly. Install react-native-cli tools npm -g install react-native-cli Install Git macOSLinux To install Git using Homebrew, open a terminal and execute:\nbrew install git Some distributions come with Git preinstalled but you’ll most likely have to install it yourself. For most distributions the package is simply called git. Additional setup for iOS (macOS) Install XCode Install Xcode to build and run the app on iOS. The minimum required version is 11.0.\nInstall Ruby A version of Ruby is automatically installed on macOS, but Mattermost React Native app development requires Ruby 3.2.0. You can check the current version of Ruby by running the following command.\nruby --version If it isn’t, we recommend using Ruby Version Manager or your preferred package manager to install the required version. The steps below are for using RVM.\nInstall the GPG keys for RVM using the command found here. If you don’t have the gpg command, you can install it using Homebrew by running. brew install gnupg Install the stable version of RVM using the following command. \\curl -sSL https://get.rvm.io | bash -s stable --ruby To load RVM, either open a new terminal or run the following command. source ~/.rvm/scripts/rvm Install the required version of Ruby rvm install 3.2.0 (Optional) If you don’t need to use a different version of Ruby for anything else, you’ll want to change the default version of Ruby. Without this, you’ll need to run rvm use 3.2.0 any time you want to work on the mobile app. rvm alias create default 3.2.0 Additional setup for Android Download and install Android Studio or Android SDK CLI tools Download and install the Android Studio app or the Android SDK command line tools\nDefault paths This documentation assumes you chose the default path for your Android SDK installation. If you chose a different path, adjust the environment variables below accordingly. Environment variables Make sure you have the following environment variables configured for your platform:\nAll platformsmacOSLinux Set ANDROID_HOME to where Android SDK is located (likely /Users/\u003cusername\u003e/Library/Android/sdk or /home/\u003cusername\u003e/Android/Sdk) Make sure your PATH includes ANDROID_HOME/tools and ANDROID_HOME/platform-tools On Mac, this usually requires adding the following lines to your ~/.bash_profile file:\nexport ANDROID_HOME=$HOME/Library/Android/sdk export PATH=$ANDROID_HOME/emulator:$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools:$PATH Then reload your bash configuration:\nsource ~/.bash_profile Note: Depending on the shell you’re using, this might need to be put into a different file such as ~/.zshrc. Adjust this accordingly. On Linux the home folder is located under /home/\u003cusername\u003e which results in a slightly different path:\nexport ANDROID_HOME=/home/\u003cusername\u003e/Android/Sdk export PATH=$ANDROID_HOME/platform-tools:$PATH export PATH=$ANDROID_HOME/tools:$PATH Then reload your configuration\nsource ~/.bash_profile Note: Depending on the shell you’re using, this might need to be put into a different file such as ~/.zshrc. Adjust this accordingly. Install the SDKs and SDK tools In the SDK Manager using Android Studio or the Android SDK command line tool, ensure the following are installed:\nSDK Tools (you may have to select Show Package Details to expand packages):\nAndroid SDK Build-Tools 31 Android Emulator Android SDK Platform-Tools Android SDK Tools Google Play services Intel x86 Emulator Accelerator (HAXM installer) Support Repository Android Support Repository Google Repository SDK Platforms (you may have to select Show Package Details to expand packages)\nAndroid 12 or above Google APIs SDK Platform Android SDK Platform 31 or above Intel or Google Play Intel x86 Atom_64 System Image Any other API version that you want to test Obtain the source code In order to develop and build the Mattermost mobile apps, you’ll need to get a copy of the source code. Forking the mattermost-mobile repository will also make it easy to contribute your work back to the project in the future.\nFork the mattermost-mobile repository on GitHub.\nClone your fork locally:\na. Open a terminal\nb. Change to a directory you want to hold your local copy\nc. Run git clone https://github.com/\u003cusername\u003e/mattermost-mobile.git if you want to use HTTPS, or git clone git@github.com:\u003cusername\u003e/mattermost-mobile.git if you want to use SSH\nNote: \u003cusername\u003e refers to the username or organization in GitHub that forked the repository Change the directory to mattermost-mobile.\ncd mattermost-mobile Install the project dependencies with npm install\n","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/developer-setup/","section":"contribute","subsection":"more info","tags":null,"title":"Developer setup"},{"categories":null,"contents":"Push notifications on Android are managed and dispatched using Firebase Cloud Messaging (FCM)\nCreate a Firebase project within the Firebase Console.\nClick Add Project Enter the project name, project ID and Country\nClick CREATE PROJECT\nOnce the project is created you’ll be redirected to the Firebase project dashboard\nClick Add Firebase to your Android App\nEnter the package ID of your custom Mattermost app as the Android package name.\nEnter an App nickname so you can identify it with ease\nClick REGISTER APP\nOnce the app has been registered, download the google-services.json file which will be used later\nClick CONTINUE and then FINISH Now that you have created the Firebase project and the app and downloaded the google-services.json file, you need to make some changes in the project.\nReplace android/app/google-services.json with the one you downloaded earlier At this point, you can build the Mattermost app for Android and setup the Mattermost Push Notification Service.\n","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/push-notifications/android/","section":"contribute","subsection":"more info","tags":null,"title":"Android push notifications"},{"categories":null,"contents":"Set up your development environment for building, running, and testing Mattermost.\nNote: If you’re migrating from before the monorepo see the migration notes. If you’re developing plugins, see the plugin developer setup documentation. If you are forking Mattermost to create a derivative version, you must comply with the AGPLv2 license in both source code and compiled versions and replace the Mattermost name and logo from the system, among other requirements, per the Mattermost trademark policy. Prerequisites for Windows If you’re using Windows, we recommend using the Windows Subsystem for Linux (WSL) for Mattermost development. Go and Node must be run from within WSL, so you’ll need to install them in WSL even if you already have the Windows versions of them installed.\nInstall WSL by running the following command as an administrator in PowerShell: wsl --install Install Docker Desktop for Windows on your Windows machine. Alternatively, you can also install docker engine directly on your linux distribution. Perform the rest of the operations (except Docker installation) within the WSL environment and not in Windows. Setup the Mattermost Server Note: The web app isn’t exposed directly, it’s exposed via the server. So if both server and web app are running, you can open localhost:8065, the server’s port to access the web app. Install make.\nOn Ubuntu, you can install build essential tools which will also take care of installing the make:\nsudo apt install build-essential Install and run Docker. If you don’t want to use Docker, you can follow this guide.\nWhen running docker commands under WSL2, if you receive the error The command 'docker' could not be found in this WSL 2 distro. you may need to toggle the Use the WSL 2 based engine off and on within Docker Settings after installation. Make sure that Docker has virtual file share access to the directory that you will clone the repository in Install Go.\nVersion 1.21 or higher is required. Increase the number of available file descriptors. Update your shell’s initialization script (e.g. .bashrc or .zshrc), and add the following:\nulimit -n 8096 If you don’t have it already, install libpng with your preferred package manager.\nIf you are on ARM based Mac, you’ll need to install Rosetta to make libpng work. Rosetta can be installed by the following command-\nsoftwareupdate --install-rosetta Fork https://github.com/mattermost/mattermost.\nClone the Mattermost source code from your fork:\ngit clone https://github.com/YOUR_GITHUB_USERNAME/mattermost.git Install NVM and use it to install the required version of Node.js:\nInstall NVM by following these instructions.\nThen, use NVM to install the correct version of Node.js for the Mattermost web app (this should be run within the webapp directory):\nnvm install Start the server:\ncd server make run-server Test your environment to ensure that the server is running:\ncurl http://localhost:8065/api/v4/system/ping If successful, the curl step will return a JSON object:\n{\"AndroidLatestVersion\":\"\",\"AndroidMinVersion\":\"\",\"DesktopLatestVersion\":\"\",\"DesktopMinVersion\":\"\",\"IosLatestVersion\":\"\",\"IosMinVersion\":\"\",\"status\":\"OK\"} Set up up your admin user using mmctl:\nbin/mmctl user create --local --email ADMIN_EMAIL --username ADMIN_USERNAME --password ADMIN_PASSWORD --system-admin Optionally, you can also populate the database with random sample data as well:\nbin/mmctl sampledata Start the web app:\ncd webapp make run Open the web app by going to http://localhost:8065 in your browser or by adding it to the Mattermost desktop app.\nStop the server:\nmake stop-server The stop-server make target does not stop all the docker containers started by run-server. To stop the running docker containers:\nmake stop-docker Set your options:\nSome behaviors can be customized such as running the server in the foreground as described in the config.mk file in the server directory. See that file for details.\nBuild the Mattermost Server The make package command will package the application and place it under the ./dist directory. You can distribute the .tar.gz file if you wish the run the application elsewhere. Note that you would need to run make build before this to build the binaries.\nDevelop Mattermost without Docker Install make.\nOn Ubuntu, you can install build essential tools which will also take care of installing the make: sudo apt install build-essential Copy the file server/config.mk as server/config.override.mk and set MM_NO_DOCKER to true in the copy.\nInstall PostgreSQL\nRun psql postgres. Then create mmuser by running CREATE ROLE mmuser WITH LOGIN PASSWORD 'mostest';\nModify the role to give rights to create a database by running ALTER ROLE mmuser CREATEDB;\nConfirm the role rights by running \\du\nBefore creating the database, exit by running \\q\nLogin again via mmuser by running psql postgres -U mmuser\nCreate the database by running CREATE DATABASE mattermost_test; and exit again with \\q\nLogin again with psql postgres and run GRANT ALL PRIVILEGES ON DATABASE mattermost_test TO mmuser; to give all rights to mmuser\nInstall Go.\nIncrease the number of available file descriptors. Update your shell’s initialization script (e.g. .bashrc or .zshrc), and add the following:\nulimit -n 8096 If you don’t have it already, install libpng with your preferred package manager.\nIf you are on ARM based Mac, you’ll need to install Rosetta to make libpng work. Rosetta can be installed by the following command- softwareupdate --install-rosetta Fork https://github.com/mattermost/mattermost.\nClone the Mattermost source code from your fork:\ngit clone https://github.com/YOUR_GITHUB_USERNAME/mattermost.git cd mattermost Install NVM and use it to install the required version of Node.js:\nFirst, install NVM by following these instructions. Then, use NVM to install the correct version of Node.js for the Mattermost web app (this should be run within the webapp directory): cd webapp nvm install cd .. NOTE: If you get zsh: command not found: nvm when running nvm install, you will need to add the following to your ~/.zshrc file: export NVM_DIR=\"$HOME/.nvm\" [ -s \"$NVM_DIR/nvm.sh\" ] \u0026\u0026 \\. \"$NVM_DIR/nvm.sh\" # This loads nvm [ -s \"$NVM_DIR/bash_completion\" ] \u0026\u0026 \\. \"$NVM_DIR/bash_completion\" # This loads nvm bash_completion Start the server:\ncd server make run-server Test your environment to ensure that the server is running by running the following in a different terminal session:\ncurl -s http://localhost:8065/api/v4/system/ping | jq . If successful, the curl step will return a JSON object:\n{\"AndroidLatestVersion\":\"\",\"AndroidMinVersion\":\"\",\"DesktopLatestVersion\":\"\",\"DesktopMinVersion\":\"\",\"IosLatestVersion\":\"\",\"IosMinVersion\":\"\",\"status\":\"OK\"} Alternately, you can enter http://localhost:8065/api/v4/system/ping in a web browser.\nSet up up your admin user using mmctl:\nbin/mmctl user create --local --email ADMIN_EMAIL --username ADMIN_USERNAME --password ADMIN_PASSWORD --system-admin Note: ADMIN_PASSWORD must be 8 characters or more.\nOptionally, you can also populate the database with random sample data as well:\nbin/mmctl --local sampledata Start the web app (in another terminal window):\ncd PATH_TO_MATTERMOST_REPO/webapp make run Open the web app by going to http://localhost:8065 in your browser or by adding it to the Mattermost desktop app.\nStop the server:\ncd PATH_TO_MATTERMOST_REPO/server make stop-server Set your options: Some behaviors can be customized such as running the server in the foreground as described in the config.mk file in the server directory. See that file for details.\n","permalink":"https://developers.mattermost.com/contribute/developer-setup/","section":"contribute","subsection":null,"tags":null,"title":"Developer setup"},{"categories":null,"contents":"By default, only a small number of required Docker services are started to support basic development:\nENABLED_DOCKER_SERVICES=\"postgres mysql inbucket\" But there are many additional services ready to work with your local environment. Note that some services will require a Mattermost Enterprise license.\nENABLED_DOCKER_SERVICES=\"postgres mysql inbucket minio openldap dejavu keycloak elasticsearch grafana prometheus promtail loki\" To customize which services are started, either export the above environment variable or copy config.mk as config.override.mk to tune appropriately.\npostgres From https://www.postgresql.org/:\nThe official site for PostgreSQL, the world’s most advanced open source database.\nThis is the default and recommended database to use with Mattermost. No additional configuration should be required, but the following settings apply to a Mattermost instance using Postgres:\nMM_SQLSETTINGS_DRIVERNAME=postgres MM_SQLSETTINGS_DATASOURCE=postgres://mmuser:mostest@localhost:5432/mattermost_test?sslmode=disable\\u0026connect_timeout=10 mysql From https://dev.mysql.com/doc/refman/8.3/en/introduction.html:\nThe MySQL software delivers a very fast, multithreaded, multi-user, and robust SQL (Structured Query Language) database server.\nThis is an alternate database supported by Mattermost, but not recommended for new deployments.\nTo use with Mattermost, be sure to configure the following settings:\nMM_SQLSETTINGS_DRIVERNAME=mysql MM_SQLSETTINGS_DATASOURCE=mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8\\u0026readTimeout=30s\\u0026writeTimeout=30s inbucket From https://inbucket.org/about/:\nInbucket is an email testing application; it will accept messages for any email address and make them available to view via a web interface. If you’ve ever used mailinator.com, you already have a good idea of what Inbucket does. The benefit of Inbucket is that it is an application instead of a hosted service; you may run it on your own private network, or even your desktop.\nUse this during development to “receive” email confirmations, password resets, or message notifications.\nTo use with Mattermost, be sure to configure the following settings:\nMM_EMAILSETTINGS_ENABLESMTPAUTH=false MM_EMAILSETTINGS_SMTPUSERNAME= MM_EMAILSETTINGS_SMTPPASSWORD= MM_EMAILSETTINGS_SMTPSERVER=localhost MM_EMAILSETTINGS_SMTPPORT=10025 When running, access the web interface at http://localhost:9001/. grafana From https://grafana.com/docs/\nCollect, correlate, and visualize data with beautiful dashboards using our open source data visualization and monitoring solution.\nGrafana is where all the metrics and logs collected by Prometheus, Loki and promtail come together. Panels visualize the data and are grouped into dashboards. The home dashboard links out to various performance dashboards, lists which Docker services are currently online, has quick links to various filtered log views, and panels showing the most recent Mattermost and Docker container logs.\nWhen running, access the web interface at http://localhost:3000. prometheus From https://prometheus.io/docs/introduction/overview/:\nPrometheus collects and stores its metrics as time series data, i.e. metrics information is stored with the timestamp at which it was recorded, alongside optional key-value pairs called labels.\nMattermost exposes metrics at http://localhost:8067/metrics which are scraped periodically by Prometheus to form a time series database. While you can access Prometheus directly to view and graph this collected data, typically this is used in tandem with Grafana for a rich dashboard experience.\nTo use with Mattermost, be sure to install a Mattermost enterprise license and configure the following settings:\nMM_METRICSSETTINGS_ENABLE=true When running, access the web interface at http://localhost:9090/. promtail From https://grafana.com/docs/loki/latest/send-data/promtail/:\nPromtail is an agent which ships the contents of local logs to a private Grafana Loki instance or Grafana Cloud. It is usually deployed to every machine that runs applications which need to be monitored.\nTo use with Mattermost, be sure to enable file logs, with the containing directory automatically mounted as a volume for promtail to scrape and relay to Loki. Promtail is automatically configured to scrape all Docker container logs for use with Loki and Grafana.\nMM_LOGSETTINGS_ENABLEFILE=true MM_LOGSETTINGS_FILELEVEL=debug MM_LOGSETTINGS_FILEJSON=true MM_LOGSETTINGS_FILELOCATION=logs loki From https://grafana.com/oss/loki/:\nLoki is a log aggregation system designed to store and query logs from all your applications and infrastructure.\nJust as Prometheus is for metrics, think of Loki being for logs. Combined with promtail scraping the logs from Mattermost and all these supporting Docker containers, and Grafana for the frontend, Loki effectively provides a powerful user interface for slicing and dicing your developer logs.\nkeycloak From https://www.keycloak.org/documentation:\nKeycloak is an open source identity and access management solution.\nKeycloak can be used as a SAML identity provider with your local setup. See the setup instructions here.\nOther Docker services Other Docker services supported by the development environment include:\nminio openldap dejavu elasticsearch ","permalink":"https://developers.mattermost.com/contribute/developer-setup/docker/","section":"contribute","subsection":null,"tags":null,"title":"Docker Services"},{"categories":null,"contents":"There are many reasons you might be motivated to contribute to Mattermost:\nYou’ve found a bug You want to help with content You want to make something more inclusive or accessible You have a feature idea You’re looking to practice your skills or give back to the community You want to help with product translation You want to help test new features You’ve found a bug If the bug fix that you’re proposing would be larger than 20 lines of code, create a GitHub issue.\nYou can speed up the process by asking about the issue in the Contributors or Developers channels on the Mattermost Community Server.\nHere’s a good example of a contribution that is small enough to not need a ticket while still being incredibly helpful.\nIf you’ve volunteered to take the ticket once it’s active, or if your fix is too small to warrant the ticket, fork the applicable repository, and start making your changes.\nCreate a PR on the master branch of the applicable repository. If there is an associated Jira or GitHub issue, your PR should begin with the issue ID (e.g. [MM-394] or [GH-394]). Our GitHub PR template will walk you through the creation of your PR.\nIf you’re a community contributor, the team at Mattermost will handle most of the review process for you. You can wait for a reviewer to be assigned and for them to review your work.\nGenerally the process works like you’d expect: they’ll make suggestions, you’ll implement them, and together you’ll discuss changes in PR comments. If you’re having trouble getting a reviewer to be assigned to your PR, take a look at GitHub’s suggested reviewers and reach out to them in the Community workspace. Here is an overview of how our review process works listed in the order in which things happen. If you’re a core contributor, you can manage the process yourself.\nLabels will be added to your PR, such as 1: UX Review, 2: Dev Review, and 3: QA Review, as applicable. See the list of labels for details.\nA milestone or a cherry-pick label may be added to your PR. Many PRs don’t need this step, and will simply ship once merged. However, if there are upcoming milestones, some reviewers are going to prioritize reviews attached to those milestones. Adding a milestone is mandatory for bug fixes that must be cherry-picked.\nIn many cases, you’ll know whose code you’re changing and can assign the appropriate Product Manager, Designer, or Technical Writer to review your PR from a UI/UX perspective. When in doubt, somebody is better than nobody, as reviewers can always reassign. Once reviewers approve your PR, the 1: UX Review label will be removed.\nRequest two core committers to review your PR — one with experience solving that particular type of issue or in that particular language, and another with expertise in the domain of your changes. Don’t request the same people over and over again unless they’re the only people with the necessary expertise. Once they give their approval, the 2: Dev Review label will be removed.\nRequest a QA tester from the team that owns the code that you’re changing. They’ll decide the necessary scope of testing. Make sure you’ve defined what the expected results are so they know when their tests are successful. Once they give their approval, the 3: QA Review label will be removed. The QA tester may add the QA Review Done label as well. Tip: We recommend that you generally avoid doing QA testing yourself, unless it’s obvious that it’s not necessary. The best way to help Mattermost QA is to clearly explain your changes and expected outcomes in the PR. Once all outstanding requests are completed, and all involved have approved of the PR, the 4: Reviews Complete label will be added which signals Mattermost core committers to merge the PR into master, and delete the old branch. If your pull request depends on other pull requests, note this as a PR comment so that the Do Not Merge/Awaiting PR label can be added to avoid merging prematurely.\nIf this is a cherry-picked PR, automated processes should handle everything from here on out. If the automated cherry-pick fails, the PR will be cherry-picked manually back to the appropriate releases. If the release branches have not been cut yet, labels are left as-is and the PR is cherry-picked once the branch has been cut. The release manager will provide reminders to finish cherry-picks. The CherryPick/Done label is applied the end of a cherry-pick process.\nUpdate the appropriate Jira ticket so everybody knows where the project stands. Resolve the ticket for QA from the Ready for QA button, and include QA test steps, or note No Testing Required if no QA testing is needed.\nIf you need to test your changes on a test server, an appropriate label can be added to the PR. Request this label as a comment in your PR to create a test server. After about three to five minutes, a test server is created, and a bot will send a link and credentials in the form of a comment on the PR. The test server is destroyed when the label is removed.\nOnce you address suggestions a reviewer has made, re-request a review from them. Their initial review was technically completed, so it’s no longer in a reviewer’s queue until you re-request.\nTip: Give reviewers time — they may have long queues and different schedules. If you’ve been assigned a reviewer but haven’t heard from them in five business days, you can politely bring focus back to your PR by mentioning them in a PR comment. Mattermost has a system to categorize the inactivity of contributions. Invalid PRs don’t need to go through this cycle at the Community Coordinator’s discretion.\nAfter ten days of inactivity: A contribution becomes stale, and a bot will add the lifecycle/1:stale label to the PR. It’s the job of the Community Coordinator to nudge the right people to get a stale PR active again, even if that means clarifying requests so the contributor has more information to work with.\nAfter 20 days of inactivity: A contribution becomes inactive, and a bot will add the lifecycle/2:inactive label to the PR.\nThe Community Coordinator warns everybody involved how much time they have before the contribution is closed and again tries to reach out to the blocking party to help.\nThe Community Coordinator also ensures that it’s not the reviewers taking the PR to this point — contributions should only ever be inactive because of no response from the contributor.\nWhen contributions are inactive, but there’s a good reason (for example, when the team is actively discussing a major design decision but they haven’t decided on anything yet), lifecycle/frozen would be a better label.\nInactive contributions are eligible to be picked up by another community member.\nAfter 30 days of inactivity: A contribution becomes orphaned, the lifecycle/3:orphaned label is added to the now-closed PR. The associated Help Wanted ticket is given back its Up For Grabs status so others can pick up the issue.\nYou want to help with content Good product and developer documentation content is as important as good code! If you notice and fix a content error in the documentation, in a repository README, or in another open source article describing Mattermost, we consider you to be as valued a member of our contributor community as those who contribute to core code.\nIf you see a problem with Mattermost developer or product documentation, you have a few options:\nIf you have time to fix the mistake and it only affects a single page, navigate to the applicable page and select Edit in GitHub at the top right. You’ll be walked through the process of creating a fork so that you can then follow the steps under the section titled “You’ve found a bug” in this guide.\nIf you don’t have time to fix the mistake, copy the file path you’re on, and create a GitHub issue about the problem you found on the applicable repository. Make sure to include the file path and fill out the issue template completely to maximize clarity.\nIf you’re not up for creating a GitHub issue right now, that’s alright too! In the bottom-right corner of every product documentation page is the question “Did you find what you were looking for?” Use this to quickly provide direct feedback about any page you’re viewing.\nIf you want to fix a larger problem that affects multiple pages or the structure of the docs, you should first report it as an issue on the appropriate GitHub repository, and follow the steps under “You’ve found a bug”. The developer and product documentation repositories contain instructions on how to build and modify the sites locally so you can test larger changes more efficiently.\nFind a list of the Mattermost documentation specific repos on the Contributor expectations page of this guide.\nTip: The best place to discuss problems with the writing team is in the Documentation Working Group channel where you can ping our technical writers with the group @docsteam. If you’d like to contribute to our blog, website, or social media content, you also have a few options:\nYou can get paid to write technical content for developers through the Mattermost community writing program.\nIf you see a problem with any webpages, blog posts, or other content on Mattermost.com, you can notify us via the Content channel on the Mattermost Community Server.\nShare your contributor or user experience! Mention us when you promote your work within our community, and we’ll amplify the message through Mattermost social media platforms.\nWant to lead a social community? We can provide advice and resources to help you in the Community channel on the Mattermost Community Server.\nYou want to make something more inclusive or accessible Accessibility is one of the most overlooked yet most important features of modern software development, and we’re eager to improve on the accessibility of the features in our open source project and its documentation.\nProblems with the accessibility or the inclusivity of both features in the codebase, as well as problems with pieces of content describing Mattermost, are bugs, so we treat them as such in our development process.\nWhen you contribute a change that incorporates an adjustment based on the principles of accessibility and inclusivity, please circle back to this guide to back you up in the PR, ticket, or post.\nYou have a feature idea Thank you for your enthusiasm! You can act on feature ideas in a few ways:\nTake a look at our product roadmap. There’s a chance we might already be building the thing you want.\nProvide input on features we’re considering to let us know what matters the most to you.\nParticipate in a survey to help us better understand how to meet the needs of our users.\nDiscuss your idea with our community in the Feature Proposals channel.\nBuild an app! Mattermost has a rich framework full of tools to help you add the features you want that don’t quite work as core additions to Mattermost.\nWebhooks are the easiest type of app to create and enable you to integrate with the vast majority of modern web tools.\nSlash commands are apps that respond to triggers sent as messages inside Mattermost.\nplugins enable you to alter every component of the Mattermost experience.\nYou’re looking to practice your skills or give back to the community We love developers who are passionate about open-source!\nIf you’re looking to tackle an interesting problem, we’ve got you covered! Feel free to check out the help wanted tickets on GitHub. To take one on, just comment on the issue, and follow the process outlined in You’ve found a bug of this guide. You can find a list of the Mattermost repositories on the Contributor expectations page of this guide.\nYou want to help with product translation We’re honored that you’d like to help people use Mattermost in their native language, so we treat all translators as full-fledged contributors alongside engineers and authors.\nEach localization community is going to have specific guidelines on how to maintain Mattermost’s distinctive voice across language barriers. Read these guides thoroughly before starting to translate German, French, or Dutch.\nTo get started:\nJoin Mattermost translation server and the localization community channel on the Mattermost Community Server.\nIf your preferred language already exists on the translation server, you can start making translations or translation suggestions immediately there on the translation server. Don’t try to do this manually through GitHub. If your language is absent from the translation server, ask on the localization community channel for it to be added. Visit the Mattermost Handbook to learn more about getting started with product translation at Mattermost. Each language in whole is assigned one quality level, and with each release cycle, it can be upgraded or downgraded if necessary.\nOfficial — 100% of the language’s translations are verified both by a Mattermost functionality expert and by an expert speaker of the target language. Officially supported languages have at least one official reviewer who updates all of the strings that have changed in the English source text, and they have successfully kept all of the translated strings updated since the last release cycle. The language has been in use for at least three full release cycles.\nBeta — At least 90% of the language’s translations are verified by at least one Mattermost functionality expert who is fluent in the target language. If a language is at risk for ongoing maintenance, Mattermost can raise the threshold closer to 100%. Up to 10% of the translations may not be synchronized with the latest English source text.\nAlpha — The language does not meet the qualifications for the Beta level.\nEvery Monday, PRs gets opened for all updates. These PRs will be checked for unexpected character insertions and security problems. Reviewers should always “merge commit” or “rebase and merge” into the PR, but never, ever “squash and commit”. Once approved, PRs are merged into the proper repositories.\nEvery contribution will be written with the ICU syntax. Please read this guide so you can get familiar with how it works, and focus especially on how plural terms are handled since that topic comes up quite often. See the Mattermost Handbook’s Localization page for best practice recommendations on working with ICU syntax.\nDon’t hesitate to use tools like the Online ICU Message Editor, which can help you see how your string will look in context.\nIf you’re not sure how to translate a technical term, you can search for it elsewhere in your language on the translation server, check how Microsoft has translated it, and feel free to ask for additional string context in the localization channel.\nYou want to help test new features The QA team keeps all contributions up to Mattermost’s high standards. That big responsibility earns QA reviewers the same status as all other contributors.\nIf you’d like to earn some prizes, join our periodic bug bashes run on the QA Contributors channel on the Mattermost Community Server.\nStandalone exploratory testing is highly encouraged too! Remember to report your findings in the QA Contributors channel.\nTest-writing is also a valuable part of the development process, and is a great way to start contributing to the Mattermost project.\nGet started by learning about our testing ethos, the categories of tests that exist, situations where tests should and should not be made, and how to write tests on the Test guidelines page. ","permalink":"https://developers.mattermost.com/contribute/why-contribute/","section":"contribute","subsection":"contribute","tags":null,"title":"Why and how to contribute"},{"categories":null,"contents":"Generate APNs Auth Key To deliver push notifications on iOS, you need to authenticate with Apple Push Notification service (APNs).\nMattermost recommends using token-based authentication with an APNs Auth Key (.p8) instead of certificates.\nPrerequisites Apple Developer Program account Registered iOS app Bundle ID with Push Notifications capability enabled 1. Create an APNs Auth Key Sign in to Apple Developer: Keys. Click + to register a new key. Enter a Key Name to easily identify it later (e.g., Mattermost Push Proxy). Enable APNs by checking the Apple Push Notifications service (APNs) box and click Configure to configure the key. On the Configure Key screen: Select an Environment: Sandbox, Production, or Sandbox \u0026 Production. Choose a Key Restriction: Team Scoped (All Topics) or Topic Specific. If you select Topic Specific, add the topics (App IDs) you want to associate. Click Save, then Continue. Review the Key details and click Register. Download the generated file AuthKey_XXXXXXXXXX.p8 and store it securely. You can only download the file once.\nNote the following values: Key ID (from the Keys list) Team ID (from your Apple Developer Membership) Bundle ID (your app identifier, used as the APNs topic) 2. Next Steps Once you’ve generated your APNs Auth Key and collected the Key ID, Team ID, and Bundle ID, continue to the Push Notification Service setup page to configure the Mattermost Push Notification Service (MPNS).\n","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/push-notifications/ios/","section":"contribute","subsection":"more info","tags":null,"title":"iOS push notifications"},{"categories":null,"contents":"Handling Flaky Tests A flaky test is one that exhibits both passing and failing results when run multiple times without any code changes. When our automation detects a flaky test on your PR:\nCheck if the Test is Newly Introduced\nReview your PR changes to determine if the flaky test was introduced by your changes If the test is new, fix the flakiness in your PR before merging For Existing Flaky Tests\nCreate a JIRA ticket titled “Flaky Test: {TestName}”, e.g. “Flaky Test: TestGetMattermostLog”\nCopy the test failure message into the JIRA ticket description\nAdd the flaky-test and triage-global labels\nCreate a PR to skip the test by adding:\nt.Skip(\"https://mattermost.atlassian.net/browse/MM-XXXXX\") where MM-XXXXX is your JIRA ticket number\nLink the JIRA ticket in the skip message for tracking\nThis process helps us track and systematically address flaky tests while preventing them from blocking development work.\nWriting Parallel Tests Leveraging parallel tests can drastically reduce execution time for entire test packages, such as api4 and app, which are notably heavy with hundreds of tests. However, careful implementation is essential to ensure reliability and prevent flakiness. Follow these guidelines when writing parallel tests:\nEnabling Parallel Tests In api4, app, platform, email, jobs packages:\nfunc TestExample(t *testing.T) { mainHelper.Parallel(t) ... } // OR func TestExample(t *testing.T) { th := Setup(t) th.Parallel(t) ... } // OR func TestExample(t *testing.T) { if mainHelper.Options.RunParallel { t.Parallel() } ... } If sqlstore package:\nfunc TestExample(t *testing.T) { if enableFullyParallelTests { t.Parallel() } ... } To enable parallel execution, you should set the ENABLE_FULLY_PARALLEL_TESTS environment variable. Example:\nENABLE_FULLY_PARALLEL_TESTS=true go test -v ./api4/... When to Use Parallel Tests Generally Safe: Tests with dedicated setup functions that ensure independence from other tests. Subtests: Only safe if each subtest features its own setup function, ensuring they are decoupled and independent of execution order. Unsafe: When a subtest depends on state changes made by another subtest, thus coupling their execution order. Common Issues That Break Parallel Safety Global State Avoid reliance on global variables and registrations such as:\nLicenseValidator platform.RegisterMetricsInterface platform.PurgeLinkCache model.BuildEnterpriseReady jobs.DefaultWatcherPollingInterval Filesystem Operations Avoid using os.Chdir (or t.Chdir) and relative paths tied to the test executable, as they may introduce inconsistencies when tests run in parallel. When possible, rely on temporary directories such as th.tempWorkspace which are dedicated to the test.\nEnvironment Variables Using os.Setenv for feature flags and other settings can cause interference between parallel tests. Instead, use the configuration API:\n// UNSAFE for parallel tests: os.Setenv(\"MM_FEATUREFLAGS_CUSTOMFEATURE\", \"true\") defer os.Unsetenv(\"MM_FEATUREFLAGS_CUSTOMFEATURE\") // SAFE for parallel tests: th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.CustomFeature = true }) Process-Level Methods Be cautious with methods affecting the entire process, such as pprof.StartCPUProfile, which can introduce contention between tests.\n","permalink":"https://developers.mattermost.com/contribute/more-info/server/tests/","section":"contribute","subsection":"more info","tags":null,"title":"Tests"},{"categories":null,"contents":"The Desktop App exercises relatively strict control over the user’s ability to navigate through the web. This is done for a few reasons:\nSecurity: Since we expose certain Electron (and therefore NodeJS) APIs to the front-end application, we want to be in control of what scripts are run in the front-end. We make a concerted effort to lock down the exposed APIs to only what is necessary; however, to avoid any privacy or security breaches, it’s best to avoid allowing the user to navigate to any page that isn’t explicitly trusted. User Experience: Our application is ONLY designed to work with the Mattermost Web App and thus allowing the user to navigate to other places that are not the Web App is not a supported use case, and could create some undesirable effects. Internal navigation The Mattermost Web App is self-contained, with the majority of links provided by react-router and thus most navigation is handled by that module. However, in the Desktop App, we have a major feature that allows users to navigate between distinct tabs bound to the same server. There are two ways that this style of navigation happens in the Web App:\nA user clicks on a link provided by the react-router Link component The application calls browserHistory.push directly within the Web App based on the user action Both of these methods will make use of the browserHistory module within the Web App. When one of the above methods is used, normally the Web App would update the browser’s URL and change the state of the page. In the Desktop App, we instead send the arguments of the call to browserHistory.push up to the Electron Main Process. The information is received at the method WindowManager.handleBrowserHistoryPush, where we perform the following actions:\nClean the path name by removing any part of the server’s subpath pathname. When the arguments are sent up to the Desktop App, it includes the subpath of the server hosting it. As an example, if the server URL is http://server-1.com/mattermost, any path that is received will start with /mattermost and we will need to remove that component. The same would be true for any other path following the origin http://server-1.com. Retrieve the view matching the path name After removing the leading subpath (if applicable), we check to see if a portion of the path matches one of the other tabs, signally that we will need to switch to that tab. For server http://server-1.com/mattermost, if the pathname is /mattermost/boards/board1, we would get the Boards view matching the server. Display the correct view and send the cleaned path to its renderer process We then explicitly display the new view if it’s not currently in focus. If it’s closed, we open it and load the corresponding URL with the provided path. Exception: If we’re redirecting to the root of the application and the user is not logged in, it will generate an unnecessary refresh. In this case, we do not send the path name down. External navigation For the cases where a user wants to navigate away from the Web App to an external site, we generally want to direct the user outside of the Desktop App and have them open their default web browser and use the external site in that application.\nIn order to achieve this, we need to explicitly handle every other link and method of navigation that is available to an Electron renderer process. Fortunately, Electron provides a few listeners that help us with that:\nwill-navigate is an event that fires when the URL is changed for a given renderer process. Attaching a listener for this event allows us to prevent the navigation if desired. NOTE: The event will not fire for in-page navigations or updating window.location.hash. did-start-navigation is another renderer process event that will fire once the page has started navigating. We can use this event to perform any actions when a certain URL is visited. new-window is an event that will fire when the user tries to open a new window or tab. This commonly will fire when the user clicks on a link marked target=_blank. We attach this listener using the setWindowOpenHandler and will allow us to allow or deny the opening as we desire. In our application, we define all of these listeners in the webContentEvents module, and we attach them whenever a new webContents object is create to make sure that all renderer processes are correctly secured and set up correctly.\nNew window handling Our new window handler will deny the opening of a new Electron window if any of the following cases are true:\nMalformed URL: Depending on the case, it will outright ignore it (if the URL could not be parsed), or it will open the user’s default browser if it is somehow invalid in another way. Untrusted Protocol: If the URL does not match an allowed protocol (allowed protocols include http, https, and any other protocol that was explicitly allowed by the user). In this case, it will ask the user whether the protocol should be allowed, and if so will open the URL in the user’s default application that corresponds to that protocol. Unknown Site: If the URL does not match the root of a configured server, it will always try to open the link in the user’s default browser. If the URL DOES match the root of a configured server, we still will deny the window opening for a few cases: If the URL matches the public files route (/api/v4/public/files/*) If the URL matches the image proxy route (/api/v4/image/*) If the URL matches the help route (/help/*) For these cases, we will open the link in the user’s browser. Deep Link Case: If the URL doesn’t match any of the above routes, but is still a valid configured server, we will generally treat is as the deep link cause, and will instead attempt to show the correct tab as well as navigate to the corresponding URL within the app. There are two cases where we do allow the application to open a new window:\nIf the URL matches the devtools: protocol, so that we can open the Chrome Developer Tools. If the URL is a valid configured server URL that corresponds to the plugins route (/plugins/*). In these cases we allow a single popup per tab to be opened for certain plugins to do things like OAuth (e.g. GitHub or JIRA). Any other case will be automatically denied for security reasons.\nLinks within the same window By default, the Mattermost Web App marks any link external to its application as target=_blank, so that the application doesn’t try to open it in the same window. Any other links should therefore be internal to the application.\nWe deny any sort of in-window navigation with the following exceptions: if the link is a mailto: link (which always opens the default mail program), OR if we are in the custom login flow.\nCustom login flow In order to facilitate logging into to the app using an external provider (e.g. Okta) in the same way that one would in the browser, we add an exception to the navigation flow that bypasses the will-navigate check.\nWhen a user clicks on a login link that redirects them to a matching URL scheme (listed here), we will activate the custom login flow. The URL MUST still be internal to the application before we activate this flow, or any URL matching this pattern would allow the app to circumvent the navigation protection.\nWhile the current window is in the custom login flow, all links that emit the will-navigate event will be allowed. Anything that opens a new window will still be restricted based on the rules for new windows. We leave the custom login flow once the app has navigated back to an URL internal to the application\n","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/architecture/navigation/","section":"contribute","subsection":"more info","tags":null,"title":"Navigation"},{"categories":null,"contents":" Important: From Mattermost v7.11, Mattermost Boards is a core part of the product that cannot be disabled or built separately. Developers should read the updated Developer Guide for details. In Mattermost v7.10 and earlier releases, Mattermost Boards is the Mattermost plugin version of Focalboard that combines project management tools with messaging and collaboration for teams of all sizes. It is installed and enabled by default in Mattermost v6.0 and later. For working with Focalboard as a standalone application, please refer to the Personal Server Setup Guide.\nBuild the plugin Fork the Focalboard repository and clone it locally. Clone Mattermost in a sibling directory. Define an environment variable EXCLUDE_ENTERPRISE with a value of 1. To install the dependencies: cd mattermost-plugin/webapp npm install --no-optional cd ../.. make prebuild To build the plugin: make webapp cd mattermost-plugin make dist Refer to the dev-release.yml workflow for the up-to-date commands that are run as part of CI.\nUpload and install the plugin Enable custom plugins by setting PluginSettings.EnableUploads to true and set FileSettings.MaxFileSize to a number larger than the size of the packed.tar.gz plugin file in bytes (e.g., 524288000) in the Mattermost config.json file. Navigate to System Console \u003e Plugins \u003e Management and upload the packed .tar.gz file from your mattermost-plugin/dist directory. Enable the plugin. Deploy the plugin to a local Mattermost server Instead of following the steps above, you can also set up a mattermost-server in local mode and automatically deploy mattermost-plugin via make deploy.\nFollow the steps in the mattermost-webapp developer setup guide and then: Open a new terminal window. In this terminal window, add an environmental variable to your bash via MM_SERVICESETTINGS_SITEURL='http://localhost:8065' (docs) Build the web app via make build Follow the steps in the mattermost-server developer setup guide and then: Make sure Docker is running. Run make config-reset to generate the config/config.json file: Edit config/config.json: Set ServiceSettings \u003e SiteURL to http://localhost:8065 (docs) Set ServiceSettings \u003e EnableLocalMode to true (docs) Set PluginSettings \u003e EnableUploads to true (docs) In this terminal window, add an environmental variable to your bash via MM_SERVICESETTINGS_SITEURL='http://localhost:8065' (docs) Build and run the server via make run-server Follow the steps above to install the dependencies. Run make deploy in the mattermost-plugin folder to automatically deploy your plugin to your local Mattermost server. ","permalink":"https://developers.mattermost.com/contribute/more-info/focalboard/mattermost-boards-setup-guide/","section":"contribute","subsection":"more info","tags":null,"title":"Mattermost Boards plugin guide"},{"categories":null,"contents":"Build Here’s a list of all the commands used by the Desktop App. These can all be found in package.json, and should be run using npm, using the following syntax: npm run \u003ccommand\u003e.\nTesting and Verification check - Runs ESLint, checks types, validates the build config and runs the unit tests check-build-config - Builds and validates the build config check-types - Runs the TypeScript compiler against the code to check the types for errors lint:js - Runs ESLint against the code and displays results lint:js-quiet - Same as above, but with the –quiet option fix:js - Save as above, but attempts to fix some of the issues test - Builds and runs all of the automated tests for the Desktop App test:e2e - Builds and runs the E2E tests for the Desktop App test:e2e:no-rebuild - Runs the E2E tests without rebuilding the entire app test:e2e:run - Runs the E2E tests without building them test:e2e:send-report - Uploads E2E results test:unit - Runs the unit tests for the main module test:unit-coverage - Runs the unit tests and displays a coverage breakdown Building and Running build - An amalgam of the following build commands, used to build the Desktop App: build:main - Builds the source code used by the Electron Main process build:renderer - Builds the source code used by the Electron Renderer process build:preload - Builds the source code used by the preload scripts run in the preload context of the Electron Renderer process build-prod - Builds the app in production mode build-prod-mas - Builds the app in production mode for Mac App Store distribution build-prod-upgrade - Builds the app in production mode with auto-update functionality build-test- Builds the app for E2E testing build-test:e2e - Builds only the E2E tests and not the app build-test:robotjs - Builds the RobotJS test module for the current Electron version start - Runs the Desktop App using the current code built in the dist/ folder restart - Re-runs the build process and then starts the app (amalgam of build and start) watch - Runs the app, but watches for code changes and re-compiles on the fly when a file is changed Packaging package - Builds and creates distributable packages for all OSes package:windows - Builds and creates distributable packages for Windows package:windows-zip - Builds and create distributable ZIP packages for Windows package:windows-installers - Builds and creates distributable MSI and EXE packages for Windows package:mac - Builds and creates distributable packages for macOS package:mac-with-universal - Same as above, but includes a universal binary package:mas - Builds and creates distributable packages for Mac App Store package:mas-dev - Same as above, but builds the development version for testing package:linux - Builds and creates distributable packages for Linux package:linux-tar - Builds and creates distributable .tar.gz packagesfor Linux package:linux-pkg - Builds and creates distributable .deb packages for Ubuntu/Debian and .rpm for Red Hat/Fedora package:linux-appImage - Builds and creates distributable .AppImage packages for Linux Workspace Utility clean - Removes all installed Node modules and built code clean-install - Same as above, but then runs npm install to reinstall the Node modules clean-dist - Only removes the built code prune - Runs ts-prune to display unused code i18n-extract - Scrape the codebase and adds missing translations to the translation file create-linux-dev-shortcut: Creates a shortcut for Linux developers to ensure deep linking works CLI options Some useful CLI options the desktop app uses are shown below. You can also display these options by running: npm run start help.\n--version, -v: Prints the application version --dataDir, -d: Set the path to where user data is stored --disableDevMode, -p: Disable development mode to allow for testing as if it was Production Environment variables Some common environment variables that are used include:\nNODE_ENV: Defines the Node environment PRODUCTION: Used for Production mode DEVELOPMENT: Development mode TEST: Used when running automated tests MM_DEBUG_MODALS: Used for debugging modals, set to 1 to show Developer Tools when a modal is opened ","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/build-commands/","section":"contribute","subsection":"more info","tags":null,"title":"Build and CLI commands"},{"categories":null,"contents":"At times, you may want to build your own Mattermost mobile app. The most common use cases are:\nTo white label the Mattermost mobile app. To use your own deployment of the Mattermost Push Notification Service (always required if you are building your own version of the mobile app). Build preparations 1. Prerequisites The Mattermost mobile app for iOS needs to be built on a macOS computer with Xcode and the Xcode command line tools installed.\n$ xcode-select --install 2. Bundle ID and entitlements Follow the steps 1, 2, and 3 for Run on iOS Devices in the Developer Setup. After configuring the app in the previous step, ensure the bundle ID for each target of the mobile app remains the same as the one in the original mattermost-mobile GitHub repository (com.mattermost.rnbeta, com.mattermost.rnbeta.MattermostShare, and com.mattermost.rnbeta.NotificationService). 3. Code sign Apple requires all apps to be digitally signed with a certificate before they can be installed.\nThe build script will make use of Match to sync your provisioning profiles (the profiles will be created for you if needed). The provisioning profiles will be created based on the environment variables.\n4. Configure environment variables To make it easier to customize your build, we’ve defined a few environment variables that are going to be used by Fastlane during the build process.\nVariable Description Default Required COMMIT_CHANGES_TO_GIT Should the fastlane script ensure that there are no changes to Git before building the app and that every change made during the build is committed back to Git.\nValid values are: true, false false No BRANCH_TO_BUILD Defines the Git branch that is going to be used for generating the build. Make sure that, if this value is set, the branch it is set to exists. $GIT_BRANCH No GIT_LOCAL_BRANCH Defines the local branch to be created from BRANCH_TO_BUILD to ensure the base branch does not get any new commits on it.\nMake sure a branch with this name does not yet exist in your local git. build No RESET_GIT_BRANCH Defines if, once the build is done, the branch should be reset to the initial state before building and whether to also delete the branch created to build the app. Valid values are: true, false false No VERSION_NUMBER Set the version of the app at build time to a specific value, rather than using the one set in the project. No INCREMENT_VERSION_NUMBER_MESSAGE Set the commit message when changing the app version number. Bump app version number to No INCREMENT_BUILD_NUMBER Defines if the app build number should be incremented.\nValid values are: true, false false No BUILD_NUMBER Set the build number of the app at build time to a specific value, rather than incrementing the last build number. No INCREMENT_BUILD_NUMBER_MESSAGE Set the commit message when changing the app build number. Bump app build number to No APP_NAME The name of the app as it is going to be shown in the device home screen. Mattermost Beta Yes APP_SCHEME The URL naming scheme for the app as used in direct deep links to app content from outside the app. mattermost No REPLACE_ASSETS Override the assets as described in White Labeling.\nValid values are: true, false false No MAIN_APP_IDENTIFIER The bundle identifier for the app. Yes BUILD_FOR_RELEASE Defines if the app should be built in release mode. Valid values are: true, false Make sure you set this value to true if you plan to submit this app to TestFlight, the Apple App Store or distribute it in any other way. false Yes NOTIFICATION_SERVICE_IDENTIFIER The bundle identifier for the notification service extension. Yes EXTENSION_APP_IDENTIFIER The bundle identifier for the share extension. Yes FASTLANE_TEAM_ID The ID of your Apple Developer Portal Team. Yes IOS_ICLOUD_CONTAINER The iOS iCloud container identifier used to support iCloud storage. Yes IOS_APP_GROUP The iOS App Group identifier used to share data between the app and the share extension. Yes SYNC_PROVISIONING_PROFILES Should we run match to sync the provisioning profiles. Note: Not syncing the provisioning profiles, will cause the singing to fail. Valid values are: true, false false Yes MATCH_USERNAME Your Apple ID Username. Yes MATCH_PASSWORD Your Apple ID Password. Yes MATCH_KEYCHAIN_PASSWORD Your Mac user password used to install the certificates in the build computer KeyChain. No MATCH_GIT_URL URL to the Git repo containing all the certificates. Make sure this Git repo is set to private. Remember this repo will be used to sync the provisioning profiles and other certificates. Yes MATCH_APP_IDENTIFIER The Bundle Identifiers for the app (comma-separated).\nList the identifiers for each target of the app. for example:\ncom.mattermost.rnbeta, com.mattermost.rnbeta.MattermostShare, com.mattermost.rnbeta.NotificationService Yes MATCH_TYPE Define the provisioning profile type to sync. Valid values are: appstore, adhoc, development, enterprise Make sure you set this value to the same type as the IOS_BUILD_EXPORT_METHOD as you want to have the same provisioning profiles installed in the machine so they are found when signing the app. adhoc Yes SUBMIT_IOS_TO_TESTFLIGHT Submit the app to TestFlight once the build finishes. Valid values are: true, false false No PILOT_USERNAME Your Apple ID Username used to deploy the app to TestFlight. No PILOT_SKIP_WAITING_FOR_BUILD_PROCESSING Do not wait until TestFlight finishes processing the app.\nValid values are: true, false true No Note: To configure your variables create the file ./mattermost-mobile/fastlane/.env where .env is the filename. You can find the sample file env_vars_example here. Build the mobile app Once all the previous steps are complete, execute the following command from within the project’s directory:\n$ npm run build:ios This will start the build process following the environment variables you’ve set. Once it finishes, it will create an .ipa file with the APP_NAME as the filename in the project’s root directory. If you have not set Fastlane to submit the app to TestFlight, you can use this file to manually publish and distribute the app.\nTroubleshooting I keep receiving Invalid username and password combination. but the user and password are correct Apple IDs must be lowercase. A username like “Example@icloud.com” may not work properly, while “example@icloud.com” will. Also ensure you have recently changed your Apple ID password. “Old” passwords may be blocked by Apple when not connecting through a browser, so Apple may block Fastlane. Resetting your password may solve this issue.\n","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/build-your-own/ios/","section":"contribute","subsection":"more info","tags":null,"title":"Build the iOS mobile app"},{"categories":null,"contents":"You can build the app from source and distribute it within your team or company either using the App Stores, Enterprise App Stores or EMM providers, or another way of your choosing.\nAt Mattermost, we build and deploy the Apps using a CI pipeline. The pipeline has different jobs and steps that run on specific contexts based on what we want to accomplish. You can check it out here.\nAs an alternative we’ve also created a set of scripts to help automate build tasks. Learn more about the scripts by reviewing the package.json file.\nNote: By using the scripts, Fastlane and other dependencies will be installed in your system. Build the Android app Build the iOS app Push notifications with your own mobile app When building your own Mattermost mobile app, you will also need to host the Mattermost Push Notification Service in order to receive push notifications.\nSee Setup Push Notifications for more details.\n","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/build-your-own/","section":"contribute","subsection":"more info","tags":null,"title":"Build your own mobile app"},{"categories":null,"contents":"What is SSL Pinning? SSL (Secure Sockets Layer) pinning is a technique used in mobile app development to ensure that the app communicates only with a server that has a specific certificate. This is done by embedding the server’s certificate in the app itself and then validating the server’s certificate against this embedded certificate during communication. If the server’s certificate does not match the pinned certificate, the connection is rejected.\nAdvantages of SSL Pinning Increased Security: Protects against man-in-the-middle (MITM) attacks by ensuring that the app only communicates with trusted servers. Trustworthiness: Guarantees that the data sent and received is from the expected server. Prevention of Certificate Spoofing: Ensures that the server’s certificate is exactly what is expected, preventing spoofing attempts. Disadvantages of SSL Pinning Certificate Management: Requires regular updates to the app when the server’s certificate is renewed or changed. Deployment Complexity: Coordination between development and deployment teams is necessary to avoid disruptions during certificate rotations. Maintenance Overhead: Adds additional steps and complexity to the app’s maintenance process. Important Note: SSL pinning requires that both development and deployment teams understand and follow best practices for cryptographic key management, certificate rotation, and incident response.\nCoordinating the timing of certificate updates and app updates is crucial to minimize impact on end-users. When the server’s SSL certificate is renewed or rotated, the hardcoded public key in the app no longer matches the server’s new certificate, leading to connection failures until the app is updated with the new key. Both development and deployment teams need to align on a deployment window that considers factors like user downtime, mobile app store review timelines, and peak usage times, as well as a coordinated rollback plan. Steps to Enable SSL Pinning in Your Mobile App 1. Obtain the Certificate from the Server Use openssl to retrieve the certificate from your server and save it to a file. This can be done with the following command:\nopenssl s_client -connect yourserver.com:443 -showcerts \u003c /dev/null | openssl x509 -outform DER -out yourserver.cer Alternatively, to save it in PEM format:\nopenssl s_client -connect yourserver.com:443 -showcerts \u003c /dev/null | openssl x509 -outform PEM -out yourserver.crt 2. Naming the Certificate Files Name the certificate files using the domain name as the filename with either .cer or .crt as the extension. For example, if your server’s domain is example.com, your files should be named example.com.cer and/or example.com.crt.\nOptionally you can have both file types (.cer and .crt) to match the server trust and to ensure continued app functionality during certificate rotations. Coordinate certificate rotations with the deployment teams to avoid disruptions between the app and the server.\n3. Copy the Certificate Files to the Assets Folder Place the certificate files in the assets/certs folder of your project. This is necessary for the app to access and use the certificates during runtime.\n4. Build Your App Follow the instructions in the Build the iOS app or Build the Android app sections to build your app with SSL pinning enabled.\n","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/build-your-own/ssl-pinning/","section":"contribute","subsection":"more info","tags":null,"title":"Enable SSL Pinning certificates"},{"categories":null,"contents":"We provide a set of scripts to help you run the app for the different platforms that are executed with npm:\nnpm start: Start the React Native packager. The packager has to be running in order to build the JavaScript code that powers the app. npm run android: Compile and run the mobile app on Android. npm run ios: Compile and run the mobile app on iOS. Note: To speed up development, only compile and run the apps in the following cases:\nYou have not deployed the app to a device or simulator with the npm run \u003cplatform\u003e command. There have been changes in the native code. A new library has been added or updated that has native code. If none of the above cases apply, you could just simply start the React Native packager with npm start and launch the app you have already deployed to the device or simulator.\nThe above commands are shortcuts for the react-native CLI. You can append -- --help to the above commands to see available options, for example:\nnpm run android -- --help Make sure you are adding -- before the options you want to include or run the react-native CLI directly:\nnpx react-native run-android --help Run on a device By default, running the app will launch an Android emulator (if you created one) or an iOS simulator.\nIf you want to test the performance of the app or if you want to make a contribution it is always a good idea to run the app on an actual device. This will let you ensure that the app is working correctly and in a performant way before submitting a pull request.\nAndroidiOS To be able to run the app on an Android device you’ll need to follow these steps:\nEnable debugging over USB\nMost Android devices can only install and run apps downloaded from Google Play by default. In order to be able to install the Mattermost Mobile app in the device during development you will need to enable USB Debugging on your device in the Developer options menu by going to Settings \u003e About phone and then tap the Build number row at the bottom seven times, then go back to Settings \u003e Developer options and enable USB debugging.\nPlug in your device via USB\nPlug in your Android device in any available USB port in your development machine (try to avoid hubs and plug it directly into your computer) and check that your device is properly connecting to ADB (Android Debug Bridge) by running adb devices.\n$ adb devices List of devices attached 42006fb3e4fb25b8 device If you see device in the right column that means that the device is connected. You can have multiple devices attached and the app will be deployed to all of them.\nCompile and run\nWith your device connected to the USB port execute the following in your command prompt to install and launch the app on the device:\nnpm run android Note: If you don’t see a bar at the top loading the JavaScript code then it’s possible that the device is not connected to the development server. See Using adb reverse. To be able to run the app on an iOS device you’ll need to have Xcode installed on a Mac computer and follow this steps:\nGet an Apple Developer account\nThe apps that run on an iOS device must be signed. To sign it, you’ll need a set of provisioning profiles. If you already have an Apple Developer account enrolled in the Apple Developer program you can skip this step. If you don’t have an account yet you’ll need to create one and enroll in the Apple Developer Program.\nOpen the project in Xcode\nNavigate to the ios folder in your mattermost-mobile project, then open the file Mattermost.xcworkspace in Xcode.\nConfigure code signing and capabilities\nSelect the Mattermost project in the Xcode Project Navigator, then select the Mattermost target. Look for the Signing \u0026 Capabilities tab.\nGo to the Signing section and make sure your Apple developer account or team is selected under the Team dropdown and change the Bundle Identifier. Xcode will register your provisioning profiles in your account for the Bundle Identifier you’ve entered if it doesn’t exist. Go to the App Groups section and change the App Groups. Xcode will register your AppGroupId and update the provision profile. Go to the iCloud section and change the Containers. Xcode will register your iCloud container and update the provision profile. Go to the Keychain Sharing section and change the Keychain Groups. Xcode will register your Keychain access groups and update the provision profile. Important: Repeat the steps for the MattermostShare and NotificationService targets. Each target must use a different Bundle Identifier. Compile and run\nPlug in your iOS device in any available USB port in your development computer.\nIf everything is set up correctly, your device will be listed as the build target in the Xcode toolbar, and it will also appear in the Devices Pane (⇧⌘2). You can press the Build and run button (⌘R) or select Run from the Product menu to run the app.\nAs an alternative you can select the targeted device by opening the Product menu in Xcode menu bar, then go to Destination and look for your device to select from the list.\nNote: If you run into any issues, please take a look at Apple’s Launching Your App on a Device documentation. If the app fails to build, go to the Product menu and select Clean Build Folder before trying to build the app again. Also, be sure that your iOS device is trusted so app deployments can proceed. ","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/developer-setup/run/","section":"contribute","subsection":"more info","tags":null,"title":"Run the mobile app"},{"categories":null,"contents":"With every Mattermost mobile app release, we publish the iOS unsigned ipa in in the GitHub Releases page, this guide describes the steps needed to modify and sign the app, so it can be distributed and installed on iOS devices.\nRequisites macOS with Xcode installed. The minimum required version is 11.0. Install the Xcode command line tools: $ xcode-select --install Set up your Certificate and Provisioning profiles as described in steps 1 and 2 for Run on iOS Devices in the Developer Setup. sign-ios script to sign the iOS app. Sign Tool Usage: sign-ios \u003cunsigned ipa file\u003e [-a|--app provisioning] [-n|--notification provisioning] [-s|--share provisioning] [-c|--certificate certificateName] [-g|--app-group-id appGroupId] [-d|--display-name displayName] outputIpa Usage: sign-ios -h|--help Options: -a, --app provisioning\tProvisioning profile for the main application. -a xxx.mobileprovision -n, --notification provisioning\tProvisioning profile for the notification extension. -n xxx.mobileprovision -s, --share provisioning\tProvisioning profile for the share extension. -s xxx.mobileprovision -d, --display-name displayName\t(Optional) Specify new application display name. By default \"Mattermost\" is used. Warning: will apply for all nested apps and extensions. -g, --app-group-id appGroupId\tSpecify the app group identifier to use (AppGroupId). Warning: will apply for all nested apps and extensions. -v, --verbose\tVerbose output. -h, --help\tDisplay help message. Sign the Mattermost iOS app Now that all requisites are met, it’s time to sign the Mattermost app for iOS. Most of the options of the signing tool are mandatory and you should be using your own provisioning profiles, certificate, also you could change the app display name.\nCreate a folder that will serve as your working directory to store all the needed files. Download your Apple Distribution certificate from the Apple Developer portal and save it in your working directory. Install the previously downloaded certificate into your macOS Keychain. Learn more. Download your Provisioning profiles from the Apple Developer portal and save it in your working directory. Download the sign-ios script and save it in your working directory. Download the iOS unsigned build and save it in your working directory. Open a terminal to your working directory and make sure the sign-ios script is executable. $ ls -la total 81472 drwxr-xr-x 7 user staff 224 Oct 11 10:54 . drwxr-xr-x 8 user staff 256 Oct 11 10:49 .. -rw-r--r--@ 1 user staff 75261811 Oct 2 12:44 Mattermost-unsigned.ipa -rw-r--r--@ 1 user staff 10746 Oct 2 10:30 app.mobileprovision -rw-r--r--@ 1 user staff 9963 Oct 2 10:30 noti.mobileprovision -rw-r--r--@ 1 user staff 10763 Oct 2 10:30 share.mobileprovision -rwxr-xr-x 1 user staff 38581 Oct 11 10:54 sign-ios Sign the app $ ./sign-ios Mattermost-unsigned.ipa -c \"Apple Distribution: XXXXXX. (XXXXXXXXXX)\" -a app.mobileprovision -n noti.mobileprovision -s share.mobileprovision -g group.com.mattermost -d \"My App Display Name\" MyApp-signed.ipa Once the code sign is complete you should have a signed IPA in the working directory with the name MyApp-signed.ipa.\nNote: The app name can be anything but be sure to use double quotes if the name includes white spaces. The name of the certificate should match the name in the macOS Keychain. ","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/unsigned/ios/","section":"contribute","subsection":"more info","tags":null,"title":"Sign unsigned iOS builds"},{"categories":null,"contents":"The server is the highly scalable backbone of the Mattermost project. Written in Go, it compiles to a single, standalone binary. It’s generally stateless except for the WebSocket connections and some in-memory caches.\nCommunication with Mattermost clients and integrations mainly occurs through the RESTful JSON web API and WebSocket connections primarily used for event delivery.\nData storage is done with MySQL or PostgreSQL for non-binary data. Files are stored locally, on network drives or in a service such as S3 or Minio.\nRepository https://github.com/mattermost/mattermost\nServer packages The server consists of several different Go packages:\napi4 - Version 4 of the RESTful JSON Web Service app - Logic layer for getting, modifying, and interacting with models cmd - Command line interface einterfaces - Interfaces for Enterprise Edition features jobs - Job server and scheduling model - Definitions and helper functions for data models store - Storage layer for interacting with caches and databases utils - Utility functions for various tasks web - Serves static pages ","permalink":"https://developers.mattermost.com/contribute/more-info/server/","section":"contribute","subsection":"more info","tags":null,"title":"Server"},{"categories":null,"contents":"Thanks for your interest in contributing to Mattermost! Come join our Contributors community channel on the community server, where you can discuss questions with community members and the Mattermost core team.\nTo help with translations, see the localization process.\nFollow this checklist for submitting a pull request (PR):\nYou’ve signed the Contributor License Agreement, so you can be added to the Mattermost Approved Contributor List.\nIf you’ve included your mailing address in the signed Contributor License Agreement, you may receive a Limited Edition Mattermost Mug as a thank you gift after your first pull request is merged. You have claimed the ticket that you wish to work on by asking for an assignment from the Mattermost team.\nTickets are assigned on a first-come-first-serve basis. Your ticket is a Help Wanted GitHub issue for the Mattermost project you’re contributing to.\nIf not, follow the process here. Your code is thoroughly tested, including appropriate unit, end-to-end, and integration tests for webapp.\nIf applicable, user interface strings are included in localization files:\nmattermost/server/en.json mattermost/webapp/channels/src/i18n/en.json mattermost-mobile/assets/base/i18n/en.json 5.1. In the webapp/channels repository run npm run i18n-extract to generate the new/updated strings.\nThe PR is submitted against the Mattermost master branch from your fork.\nThe PR title begins with the Jira or GitHub ticket ID (e.g. [MM-394] or [GH-394]) and summary template is filled out.\nIf your PR adds or changes a RESTful API endpoint, please update the API documentation.\nIf your PR adds a new plugin API method or hook, please add an example to the Plugin Starter Template.\nIf QA review is applicable, your PR includes test steps or expected results.\nIf the PR adds a substantial feature, a feature flag is included. Please see criteria here.\nYour PR includes basic documentation about the change/addition you’re submitting. View our guidelines for more information about submitting documentation and the review process.\nOnce submitted, the automated build process must pass in order for the PR to be accepted. Any errors or failures need to be addressed in order for the PR to be accepted. Next, the PR goes through code review. To learn about the review process for each project, read the CONTRIBUTING.md file of that GitHub repository.\nThat’s all! If you have any feedback about this checklist, let us know in the Contributors channel.\n","permalink":"https://developers.mattermost.com/contribute/more-info/getting-started/contribution-checklist/","section":"contribute","subsection":"more info","tags":null,"title":"Contribution checklist"},{"categories":null,"contents":"We are a welcoming and open community and we’re excited to have you join us!\nTo be an open, safe, and welcoming community, we strive to be inclusive, collaborative, considerate, and respectful. We all abide by the Mattermost Code of Conduct (CoC), and by joining our contributor community, you agree to abide by it as well.\nLearn more about our company values, and how to become a successful member of the Mattermost community by learning and following our standard operational guidelines below. Please read these sections below carefully and let us know if you have any questions or concerns.\nWe are inclusive We welcome all people, but not all behavior.\nWe are a diverse community who celebrate both our differences and the things that connect us. We treat each other with respect, and aim to treat others better than they wish to be treated.\nWe try our best to be clear and respectful. We remember that others may not communicate in the same language with the same fluency. We recognize that communication can be challenging, especially among a diverse group of people communicating in many different languages and coming from many different cultures and backgrounds.\nWe all try to be mindful of our differences when we communicate and collaborate. We’re aware that misunderstandings can happen. We try to resolve them by being respectful, understanding, and by using clear and simple language.\nWe are collaborative We ask questions and consult others. We work together and help each other. We aim for clarity. We are considerate We have patience with each other. We understand that no one has all the answers, nor are they expected to. We are respectful We offer thanks and we’re grateful. We may occasionally disagree, but we resolve these disagreements in respectful ways, take breaks if things get heated, reassess, and consult others where appropriate. We aim to be self-aware and we take responsibility for our impact through our words and actions. We understand and acknowledge that intent doesn’t equal impact. We can have the best of intentions, but still cause negative impact to others by our words and actions. This can happen to all of us, so we practice openness and grace. Attribution This document was heavily inspired by and adapted from the work of the Drupal Community and its Code of Conduct and its Values and Principles documents.\n","permalink":"https://developers.mattermost.com/contribute/good-decisions/","section":"contribute","subsection":"community expectations","tags":null,"title":"Community expectations"},{"categories":null,"contents":"If you are transitioning from the non-monorepo mattermost-server to the monorepo, the easiest way to do so is to move the old mattermost server folder to something like mattermost-server-old then re-clone mattermost-server. Then:\nCopy over your old config\ncd server cp ../../mattermost-server-old/config/config.json ./config/ Copy over your developer config override\ncd server cp ../../mattermost-server-old/config.override.mk ./ Update your development Docker containers for the new location of the server folder:\ncd server make update-docker ","permalink":"https://developers.mattermost.com/contribute/monorepo-migration-notes/","section":"contribute","subsection":null,"tags":null,"title":"Monorepo migration notes"},{"categories":null,"contents":"This page describes the process to follow when someone notices a mistake in a merged pull request (PR).\nA contributor (either staff or community member) submits a PR, it is reviewed and merged into the codebase. Sometime later, the community notices a mistake with the PR. Question is, what should we, as a community, do? That depends on the scope of the changes in the PR that was merged.\nLow impact issues A low impact PR might mean that it affected:\nSome non-critical functionality. It doesn’t affect users in a substantial way. If this is the case, do the following:\nCapture details in an issue. Mark it according to its priority. Would be best to assign it to the person who introduced the issue in the first place. High impact issues A high impact PR represents something that has or will result in a customer incident.\nIf this is the case, there are two scenarios:\nThe feature introduced in the PR is handled by a feature flag. The feature introduced in the PR is not handled by a feature flag. For scenario 1, if it’s not affecting other functionality, turn that feature flag off to disable the feature.\nFor scenario 2:\nRevert the changes introduced in the original PR. Notify the person who worked on the PR so they can work on a proper fix for their PR. Reintroduce the change through the regular PR cycle. ","permalink":"https://developers.mattermost.com/contribute/good-decisions/fixing-pr-mistakes/","section":"contribute","subsection":"community expectations","tags":null,"title":"When a merged PR results in a bug"},{"categories":null,"contents":"There are several renderer processes that make up the internal interface of the Desktop App. These are all represented by singleton objects that reside in the Main Module. These classes are in charge of holding the corresponding BrowserWindow or BrowserView object, initializing any handlers specific to that view, and exposing any special functionality that other modules may need to either read or affect the view.\nAs all of these views only load trusted scripts in the renderer process, all of these views are given full access to the desktopAPI module, allowing them to perform basically any action that we allow for in the Desktop App via the IPC layer.\nWindows These are the internally-managed windows acting as the main user interface points for the user. Each of these views are represented by a BrowserWindow object.\nMain window This is the primary view that encapsulates the core of the Desktop App interface. Most BrowserView objects are rendered using this window as their parent, and are affected by the behavior of this window. Most other controls, including the tray icon and taskbar/dock icon, interact with this window, and most of their functionality is tied to it as well.\nThis window is managed by the MainWindow module located at main/windows/mainWindow.\nHooks init(): Creates the BrowserWindow object for the Main Window and adds all appropriate listeners. get(): Returns the BrowserWindow object for the Main Window. This is directly exposed as there are many different functions affecting the behavior of the window, and thus the encapsulating module often needs to pass that control to other modules. If true is passed as an argument, init() will be called if the window does not exist, otherwise undefined is returned. getBounds(): Returns the current size and location of the BrowserWindow, used for resize functionality, and to ensure that child windows/views are positioned correctly. focusThreeDotMenu(): Sends a message to the Main process that focuses the view and highlights and focuses the 3-dot menu on Windows/Linux. This is used when the ALT key is pressed as a shortcut to focus the menu. Settings window This window is created when the user opens Preferences from the File menu. It contains an interface where the user can change settings specific to the Desktop App client that do not affect their Mattermost servers. This window is a child window of the Main Window and will close/hide when the Main Window is closed/hidden.\nThis window is managed by the SettingsWindow module located at main/windows/settingsWindow.\nHooks show(): Shows the Settings Window if it exists and will create it if does not. When the window is closed, the BrowserWindow object is dereferenced. get(): Retrieves the Settings Window BrowserWindow object if it exists and returns undefined if it does not. Views These are the internally managed views that are rendered on top of existing windows, adding additional functionality. Each of these views are represented by a BrowserView object.\nMost of these views exist as they act as augments to the existing interface and must be rendered over top of the external sandbox Mattermost BrowserViews.\nLoading screen This is a BrowserView that renders over top of external Mattermost views that are loading. It is a cosmetic view that avoids the user having a white screen while the application is loading. The view is ephemeral should only be visible while the current external Mattermost view is loading.\nThis view is managed by the LoadingScreen module located at main/views/loadingScreen. Its parent is the Main Window.\nHooks show(): Displays the Loading Screen over top of any other BrowserView currently rendered in the Main Window and begins the animation. fade(): Starts the process of removing the Loading Screen. First a signal is sent to the renderer to fade the screen and stop the animation. When that finishes, the view is removed from the window. setBounds(): Calls when the Main Window resizes while the Loading Screen is still visible and the view needs to change its size as well. setDarkMode(): Calls when the application’s dark mode flag is changed, to ensure a consistent color scheme. isHidden(): Helper method to check whether the view is hidden or not. ","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/architecture/internal-views/","section":"contribute","subsection":"more info","tags":null,"title":"Internal views"},{"categories":null,"contents":"Overview This document aims to do an analysis of the types of schema migrations we do in Mattermost and ways to make them non-blocking so as to improve the Mattermost upgrade experience. Historically, we have never put a lot of thought to the migration process. Developers would simply add a DDL statement and call it a day. But that causes a significant impact to large customers for whom downtime is not an option. This causes them to push back on upgrading their Mattermost version for a long time (sometimes for several years). This in turn, has some cascading effects like customers not able to get new features, performance improvements etc.\nWe want to improve the situation and make upgrades a worry-free experience for our customers. It definitely comes at a cost of writing more code and delaying some features to avoid breaking changes. This document will aim to uncover all those cases and provide best practices to follow so that we can hit the right balance.\nGoals We have two overarching goals and a third auxiliary goal.\nSchema migrations should ALWAYS be backwards compatible until the last ESR. Schema migrations should NEVER lock the entire table. Reduce migration time where possible. We want to strictly follow this as much as possible (even at the cost of slower feature development).\nBackground Schema changes are always made synchronously when Mattermost starts up. This means the application won’t be ready to serve requests until all schema changes are applied. In most cases, the new application won’t be able to work until those schema changes are in place.\nIn a high availability environment, multiple instances will try to run migrations. To prevent this, a lock table is used in the migration system. Until migrations are completed, none of the instances will start. Once the lock is released by a node, another instance will obtain the lock, and check the migrations table. Since the previous node already applied the migrations, the remaining nodes won’t re-apply the migrations.\nFrom Mattermost release v6.4, we have started using a schema-based migration system. We are now creating SQL statement files to run migrations. A developer must create migration files for each database driver. Since we want our migrations to be reversible, the developer must create one up script along with a down script. For instance, a single migration would have the following files:\n000066_upgrade_posts_v6.0.down.sql 000066_upgrade_posts_v6.0.up.sql A file naming convention is used to determine the order in which the migrations should be applied that appends up|down.sql suffix to the migration name. We were using a database version before the new migration system which is why the versions exist in the migration file name in the example. Going forward, using version identifiers for future next migration files is not mandatory. A developer can add any information to the name if they think it’s going to be helpful.\nWe are using morph for the migration engine. The tool has a library and also a CLI. Mattermost server imports the library to have programmatic access to morph functions. A developer can use the morph CLI tool to test whether their migrations are working properly. Please follow instructions in the morph documentation to use the morph CLI tool.\nAnalysis A rough analysis of our past schema migrations shows the following (some very early migrations were skipped which would be considered as base Mattermost):\nCREATE INDEX - 489 ALTER TABLE - 195 ADD COLUMN - 113 ALTER COLUMN - 51 DROP COLUMN - 25 ADD CONSTRAINT - 6 DROP INDEX - 124 CREATE TABLE - 60 UPDATE - 19 DELETE - 2 We will go through each of these migration types and discuss how we can make it non-blocking. This is a lengthy document, so for those wanting to directly look at the executive summary, we present it right now. And then expand on each section in detail later.\nConclusions Operation Table rewrite Concurrent DML allowed CREATE INDEX NO YES DROP INDEX NO YES ADD COLUMN NO YES1 ALTER COLUMN YES NO DROP COLUMN YES YES1 ADD FK CONSTRAINT NO YES (only selects)2 ADD UNIQUE CONSTRAINT NO YES Note: Technically it takes an ACCESS EXCLUSIVE LOCK, however it is only to add/remove the metadata. The command returns instantly. Adding FK constraint takes a SHARE ROW EXCLUSIVE lock. Recommendations Try to avoid FK constraints. Strongly avoid trying to alter column types. However, if you MUST do it, take a look into the following sections.\nDetails CREATE INDEX CREATE INDEX CONCURRENTLY does not take any locks.\nALTER TABLE ADD COLUMN Adding nullable columns happens in constant time from version 10. And from version 11 onwards, adding non-null columns with a default value also happens in constant time.\nThe catch here is to be able to handle denormalization optimizations which typically adds a new column but needs to backfill that with data before using the column. Take a look at the next section on how to achieve that.\nALTER TABLE ALTER COLUMN This takes an exclusive lock. We strongly recommend you avoid doing this.\nTo give some context, we have this particular migration ALTER TABLE posts ALTER COLUMN props TYPE jsonb USING props::jsonb; which has caused us more pain than it was worth. Several large customers have faced problems with this migration where in some cases, it has been observed to take 8+ hrs. Therefore, we strongly suggest to avoid making any ALTER COLUMN changes until absolutely unavoidable (for example, security issues).\nHowever, if you MUST do this, then see the example later.\nALTER TABLE DROP COLUMN Only a metadata lock is taken. No table rewrite takes place. The space is just marked as unused and later taken up by future DB writes.\nALTER TABLE ADD CONSTRAINT Relatively rare, but out of those 6 cases, 2 are adding unique constraints. For example:\nALTER TABLE oauthaccessdata ADD CONSTRAINT oauthaccessdata_clientid_userid_key UNIQUE (clientid, userid); This can be improved by first adding the index concurrently, and then attaching the index to the constraint. See example later.\nAdding a foreign key in PostgreSQL takes a share row exclusive lock, which means only SELECT queries are allowed. It is possible to bypass the table scanning by adding a “NOT VALID” suffix, but then it defeats the purpose of having a foreign key. We recommend against doing it.\nDROP INDEX DROP INDEX CONCURRENTLY does not take any locks.\nCREATE TABLE Does not lock any existing data so no issues.\nUPDATE An analysis shows that UPDATE statements roughly fall into one of these three categories:\nData migrations: UPDATE channelmembers SET MentionCountRoot = .. UPDATE Channels SET TotalMsgCountRoot = .. UPDATE ChannelMembers CM SET MsgCountRoot .. In these cases, rather than operating on the entire table, we need to operate on batches at a time. See the example later on how to achieve that.\nChanging NULL columns to NON-NULL UPDATE Channels SET LastRootPostAt=0 WHERE LastRootPostAt IS NULL; UPDATE OAuthApps SET MattermostAppID = '' WHERE MattermostAppID IS NULL; This is possible to handle from the code itself using a COALESCE function. It makes the code more complicated, but it’s a cost we have to pay to reduce migration overhead.\nDenormalization optimizations: UPDATE threads SET threaddeleteat = posts.deleteat FROM posts WHERE threads.threaddeleteat IS NULL AND posts.id = threads.postid; UPDATE reactions SET channelid = COALESCE((select channelid from posts where posts.id = reactions.postid), '') WHERE channelid=''; UPDATE threads SET threadteamid = channels.teamid FROM channels WHERE threads.threadteamid IS NULL AND channels.id = threads.channelid; UPDATE fileinfo SET channelid = posts.channelid FROM posts WHERE fileinfo.channelid IS NULL AND fileinfo.postid = posts.id; We can take the same approach as in data migrations.\nDELETE So far, there have been only a handful of DELETE statements in schema migrations. And mostly they have been for security issues. The general recommendation is to avoid running a full-blown DELETE statement that operates on the entire table, but rather operate on batches so as to avoid taking a lock on the entire table. This could either be done in a job since there is no new code waiting for this to be executed. (See above)\nExamples How do I change a column type if I MUST Follow this long-winded procedure:\nCreate a new column. Migrate existing data. From next ESR, start using the new column. Next ESR, drop the old column. For example, let’s say the next upcoming version is 8.4, and the next ESR is 8.6. So step 1 and 2, goes in 8.4. And in 8.7 onwards, we add the code to start using the new column, which will eventually be part of 8.12 (ESR after that). And then from 8.13 onwards, we can drop the column.\nThe following diagram should explain things better:\nThe reasoning behind this is some customers will only upgrade from ESR to ESR. So we need to ensure backwards compatibility with the previous version.\nFollowing shows an example where we are adding a channel_count column to the status table. This is not exactly altering a column, but the idea remains the same, and you can extend this to fit your use-case.\nALTER TABLE status ADD COLUMN channel_count integer;\nOur next objective is to migrate existing data. We do this in a 2-phase approach where we set up triggers to migrate all new data and in the background migrate old data in batches.\nCREATE OR REPLACE FUNCTION public.update_status_channel_count() RETURNS trigger LANGUAGE plpgsql AS $function$ DECLARE member_count integer; BEGIN select count(*) into member_count from channelmembers where userid=NEW.userid; NEW.channel_count := member_count; RETURN NEW; END $function$ CREATE TRIGGER tr_update_status_channel_count BEFORE INSERT OR UPDATE ON status FOR EACH ROW EXECUTE PROCEDURE update_status_channel_count(); After this is taken care of, we need to create a job, which will migrate existing data in batches.\nUPDATE status s SET channel_count=(SELECT count(*) FROM channelmembers cm WHERE cm.userid=s.userid) WHERE channel_count IS NOT NULL AND s.userid in (SELECT userid FROM status WHERE userid \u003e '' ORDER BY userid ASC limit 10); Then store the user id offset in the job metadata.\nUPDATE status s SET channel_count=(SELECT count(*) FROM channelmembers cm WHERE cm.userid=s.userid) WHERE channel_count IS NOT NULL AND s.userid in (SELECT userid FROM status WHERE userid \u003e \u003coffset\u003e ORDER BY userid ASC limit 10); At this point, when the job finishes, the new column would be ready to use. And the triggers would take care of always keeping the data up to date.\nNow we can start using the new column from the next ESR version. But we cannot yet drop the existing column because of backwards compatibility guarantees. The old column would still be in use by older app nodes in the cluster during upgrade. We also would want to drop the trigger since its use is finished. DROP TRIGGER tr_update_status_channel_count on status And in the ESR after that, now we can finally drop the old column. This deliberately skips renaming the column for simplicity. Depending on your use-case, you can do that if you want to. It is a fast operation that does not rebuild the table, so there are no issues.\nHow do I add a unique constraint to a table CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS oauthaccessdata_clientid_userid_key on oauthaccessdata(clientid, userid); ALTER TABLE oauthaccessdata ADD UNIQUE USING INDEX oauthaccessdata_clientid_userid_key; -- This is instantaneous How do I run UPDATE statements in data migrations/denormalizations The idea would be to run the UPDATE statements in batches so as to avoid taking a large lock. This is similar to the second part in the column change example.\nFollowing is an example where I update the channels table in batches setting a new column.\nCREATE OR REPLACE FUNCTION public.update_in_batches() RETURNS INTEGER LANGUAGE plpgsql AS $function$ DECLARE id_offset text := ''; rows_updated integer; BEGIN LOOP WITH table_holder AS ( SELECT id FROM channels WHERE id \u003e id_offset ORDER BY id ASC limit 100 ) UPDATE channels c SET new='improved' WHERE c.id in (SELECT id FROM table_holder); -- change this query to whatever your requirement is GET DIAGNOSTICS rows_updated = ROW_COUNT; -- We have to run the select query again -- becaue \"select into\" isn't allowed inside a CTE -- and without CTE, we have to use a temp table (because you can't select into a table) -- and with a temp table, you run into max_locks_inside_transaction limit. -- Probably there is a better way but keeping things simple for now. select id into id_offset from (select id from channels where id \u003e id_offset ORDER BY id ASC limit 100) as temp order by id desc limit 1; EXIT WHEN rows_updated = 0; END LOOP; return 1; END $function$; FAQ I need to make a schema change. What do I do? Add the appropriate SQL script file containing the statements you want to run into the migrations directory. This directory is located in {project_dir}/db/migrations/{driver_name}/. Run make migrations-extract to add your new migrations to the db/migrations/migrations.list file. This will ensure that there will be merge conflicts in case there is a conflict on migration sequence numbers with the master branch. Since we don’t want to have a collision on version numbers of the migration files, the developer should merge the upstream branch to the feature branch just before merging so that we can be sure that there are no versioning issues. In case of a version number collision, the build process will fail and main branch will be broken until it gets fixed. When you run the mattermost/server binary, the tool will automatically apply the migration if it’s required. The migration name will be saved in the db_migrations table. Lastly, please also measure the time taken for the migration with an eye towards resource usage. Please use the DB dumps from the ~developers-performance channel in our Community server. You will find the links in the channel header. In your PR, make sure to add release notes following the Developer Schema Migration Template. My migration has failed. What do I do? If you think your migration is applied, and you want to revert changes, you can run the down script to roll back in a clean way. You can use morph CLI to apply down migrations. Before rolling down the script, check the db_migrations table whether the migration is applied or not. If it’s applied you can revert it using morph CLI command. An example command would look like morph apply down --driver {your-driver} --dsn \"{your-dsn}\" --path {path-to-your-driver-specific-migration-files} --number 1 If the migration has been shipped in a release and you want to apply fixes, instead of changing the existing script, you should add a new one so that db_migrations will stay consistent. You can edit the existing migration to be a no-op for future releases in this case. GLOSSARY DDL - short form of Data Definition Language, which deals with database schema changes. For example: CREATE TABLE, ALTER TABLE etc. DML - short form of Data Manipulation Language, which deals with SQL queries that read/update/delete tables. For example: SELECT, UPDATE, INSERT etc. FK - Foreign Key ","permalink":"https://developers.mattermost.com/contribute/more-info/server/schema-migration-guide/","section":"contribute","subsection":"more info","tags":null,"title":"DB migration guide"},{"categories":null,"contents":"Golang (“go”) is a more opinionated language than many others when it comes to coding style. The compiler enforces some basic stylistic elements, such as the removal of unused variables and imports. Many others are enforced by the gofmt tool, such as usage of white-space, semicolons, indentation, and alignment. The gofmt tool is run over all code in the Mattermost Server CI pipeline. Any code which is not consistent with the formatting enforced by gofmt will not be accepted into the repository.\nDespite this, there are still many areas of coding style which are not dictated by these tools. Rather than reinventing the wheel, we are adopting Effective Go as a basis for our style guide. On top of that, we also follow the guidelines laid out by the Go project at CodeReviewComments.\nHowever, at present, some of the guidelines from these sources come into conflict with existing patterns that are present in our codebase which cannot immediately be corrected due to the need to maintain backward compatibility.\nThis document, which should be read in conjunction with Effective Go and CodeReviewComments, outlines the small number of exceptions we make to maintain backward compatibility, as well as a number of additional stylistic rules we have adopted on top of those external recommendations.\nApplication of guidelines The following guidelines should be applied to both new and existing code. However, this does not mean that a developer is required to fix any surrounding code that contravenes the rules in the style guide. It’s encouraged to keep fixing things as you go, but it’s not compulsory to do so. Reviewers should refrain from asking for stylistic changes in surrounding code if the submitter has not included them in their pull request.\nGuidelines Project layout When creating a new Go module, please follow the standardized guidelines of the Go team.\nThis blog article provides additional guidance on package names.\nFollowing are some of the anti-patterns to keep in mind:\nDon’t use the pkg pattern. This is a common standard used by many projects. But it goes against the Go philosophy of naming packages that signify what they contain. From https://blog.golang.org/package-names: A package’s name provides context for its contents, making it easier for clients to understand what the package is for and how to use it.\nThe pkg directory was used long ago in the Go project when there weren’t any well-defined standards. But it was later removed to just have normal packages.\nSame for util or misc packages. Don’t use them. Instead break out related util functionalities into its own package or if it’s not used by a lot of packages, make it part of the original package itself.\nDon’t have too many small packages with only one file. It’s usually a sign of splitting packages without giving thought into them. Look at the API boundaries and group the packages into bigger chunks.\nFunctional Default to sync instead of async Always prefer synchronous functions by default. Async calls are hard to get right. They have no control over goroutine lifetimes and introduce data races. If you think something needs to be asynchronous, measure it and prove it. Ask these questions:\nDoes it improve performance? If so, by how much? What’s the tradeoff of the happy path vs. slow path? How do I propagate errors? What about back-pressure? What should be my concurrency model? Do not create one-off goroutines without knowing when/how they exit. They cause problems that are hard to debug, and can often cause performance degradation rather than an improvement. Have a look at:\nhttps://go.dev/wiki/CodeReviewComments#goroutine-lifetimes https://go.dev/wiki/CodeReviewComments#synchronous-functions Pointers to slices Do not use pointers to slices. Slices are already reference types which point to an underlying array. If you want a function to modify a slice, then return that slice from the function, rather than passing a pointer.\nAvoid creating more ToJSON methods Do not create new ToJSON methods for model structs. Instead, just use json.Marshal at the call site. This has two major benefits:\nIt avoids bugs due to the suppression of the JSON error which happens with ToJSON methods (we’ve had a number of bugs caused by this). It’s a common pattern to pass the output to something (like a network call) which accepts a byte-slice, leading to a double conversion from byte-slice to string to a byte-slice again if ToJSON methods are used. Interfaces Return structs, accept interfaces. Interface names should end with “-er”. This is not a strict rule. Just a guideline which indicates the fact that interface functionalities are designed around the concept of “doing” something. Try not to define interfaces on the implementer side of an API “for mocking”; instead, design the API so that it can be tested using the public API of the real implementation. As an example, if you’re trying to integrate with a third-party service, it’s tempting to create an interface and use that in the code so that it can be easily mocked in the test. This is an anti-pattern and masks real bugs. Instead, you should try to use the real implementation via a docker container or if that’s not feasible, mock the network response coming from the external process.\nAnother common pattern is to preemptively declare the interface in the source package itself, so that the consumer can just directly import the interface. Instead, try to declare the interface in the package which is going to consume the functionality. Often, different packages have non-overlapping set of functionalities to consume. If you do find several consumers of the package, remember that interfaces can be composed. So define small chunks of functionalities in different interfaces, and let consumers compose them as needed. Take a look at the set of interfaces in the io package.\nThese are just guidelines and not strict rules. Understand your use case and apply them appropriately.\nStylistic CamelCase variables/constants We use CamelCase names like WebsocketEventPostEdited, not WEBSOCKET_EVENT_POST_EDITED.\nEmpty string check Use foo == \"\" to check for empty strings, not len(foo) == 0.\nReduce indentation If there are multiple return statements in an if-else statement, remove the else block and outdent it.\nThis is an example from mlog/human/parser.go:\n// Look for an initial \"{\" if token, err := dec.Token(); err != nil { return result, err } else { d, ok := token.(json.Delim) if !ok || d != '{' { return result, fmt.Errorf(\"input is not a JSON object, found: %v\", token) } } This can be simplified to:\n// Look for an initial \"{\" if token, err := dec.Token(); err != nil { return result, err } d, ok := token.(json.Delim) if !ok || d != '{' { return result, fmt.Errorf(\"input is not a JSON object, found: %v\", token) } Initialisms Use userID rather than userId. Same for abbreviations; HTTP is preferred over Http or http.\nReceiver names The name of a method’s receiver should be a reflection of its identity; often a one or two letter abbreviation of its type suffices (such as “c” or “cl” for “Client”). Don’t use generic names such as “me”, “this”, or “self” identifiers typical of object-oriented languages that give the variable a special meaning.\nError variable names The name of any error variable must be err or prefixed with err. The name of any *model.AppError variable must be appErr or prefixed with appErr. This allows us to avoid confusion about how to handle different kind of errors inside Mattermost. If you are storing the error value from a function that returns an error type in its signature, it’s considered an error, regardless of whether the function, internally, is returning a *model.AppError instance.\nFor example, when the function signature returns an error, we use the err variable name:\nfunc MyFunction() error { return model.NewAppError(...) } func OtherFunction() { ... err := MyFunction() ... } When the function signature returns an *model.AppError, we use the appErr variable name:\nfunc MyFunction() *model.AppError { return model.NewAppError(...) } func OtherFunction() { ... appErr := MyFunction() ... } Errors Always add proper context to errors.\nInclude the user input for validation errors. For example:\nreturn fmt.Errorf(\"invalid export type, must be one of: csv, actiance, globalrelay\") does not include what was the input value entered by the user. A better way might be:\n- return fmt.Errorf(\"invalid export type, must be one of: csv, actiance, globalrelay\") + return fmt.Errorf(\"invalid export type: %s, must be one of: csv, actiance, globalrelay\", exportType) Another example:\nif r.Start \u003e r.End { -\treturn fmt.Errorf(\"report timestamps are erroneous\") + return fmt.Errorf(\"report timestamps are erroneous: start_timestamp %f is greater than end_timestamp %f\", r.Start, r.End) } Return errors instead of boolean for ID validation errors A validation function can check various properties of an object. But if we simply return true or false, and then log that object is invalid, the user will have no idea why it is invalid or how to fix it.\nFor example:\n- func IsValidId(value string) bool { + func IsValidId(value string) error { if len(value) != 26 { -\treturn false +\treturn fmt.Errorf(\"Invalid length. Found: %d; expected: %d\", len(value), 26) } for _, r := range value { if !unicode.IsLetter(r) \u0026\u0026 !unicode.IsNumber(r) { -\treturn false +\treturn fmt.Errorf(\"Rune %c in %s is not an unicode letter or number\", r, value) } } -\treturn true + return nil } Logging Log messages should be annotated with contextual information in the form of key-value pairs to make it easier to identify the context they originated from. The keys should use snake_case. Refer to the corresponding JSON struct tags for key names.\nfunc (a *App) SendNotifications(...) { .. _, err := a.sendOutOfChannelMentions(c, sender, post, channel, ...) if err != nil { c.Logger().Error( \"Failed to send warning for out of channel mentions\", mlog.String(\"user_id\", sender.Id), mlog.String(\"post_id\", post.Id), mlog.Err(err), ) } .. } Avoid double-logging Double-logging is when you immediately log something after an error, and also return an error at the same time. This creates two log lines with the same error and is confusing to the admin. The best practice is to always pass the error upwards adding context and log it in the upper-most layer correct. Logging the error at the lowest layer does not give any additional context as to from where it was called, and what path did the code take to reach there.\nFor example:\nif err != nil { -\tmlog.Error(\"Failed to generate SQL query\", -\tmlog.String(\"user_id\", userID), -\tmlog.Int(\"timestamp\", int(syncTime)), -\tmlog.Err(err), -\t) -\treturn errors.Wrap(err, \"failed to generate SQL query\") +\treturn errors.Wrapf(err, \"failed to generate SQL query: user_id: %s, timestamp: %d\", userID, int(syncTime)) } Log levels The purpose of logging is to provide observability - it enables the application communicate back to the administrator about what is happening. To communicate effectively logs should be meaningful and concise. To achieve this, log lines should conform to one of the definitions below:\nCritical: This log-level represents the most severe situations when the service is entirely unable to continue operating. After emitting a critical log line, it is expected that the service will terminate.\nFor example, the code block below demonstrates a critical situation where the server startup routine fails, meaning the service is unable to start and must terminate.\nfunc runServer(..) { .. server, err := app.NewServer(options...) if err != nil { mlog.Critical(err.Error()) return } .. } Error: This log-level is used when something unexpected has happened to the service, but it does not result in a total loss of service. Log lines using the error level must be actionable, so that the system administrator can investigate and resolve the incident. The error log level may indicate a loss of service for an individual user or request or it may indicate a total failure of a non-critical subsystem within the service.\nFor example, the error log level is used in the code snippet below as it represents a partial failure of one non-critical subsystem of the service. Administrator intervention is required to resolve this situation, but the rest of the service is able to continue operating in the meantime.\nfunc (a *App) SyncPlugins(..) { .. reader, appErr := a.FileReader(plugin.path) if appErr != nil { mlog.Error(\"Failed to open plugin bundle from file store.\", mlog.String(\"bundle\", plugin.path), mlog.Err(appErr)) return } .. } Warn: This log level is used to indicate that something unexpected has happened, but the server is able to continue operating and it has not suffered any loss of functionality as a consequence of this failure. System administrators may wish to investigate the cause of log lines at this level, but the need is typically less pressing than for those at error level. System administrators may also wish to monitor the rate of occurrence of individual log-lines at this level as this may be indicative of a wider problem. Log lines at the warning level should be as detailed as possible, since these are often the least clear-cut category of message.\nFor example, the warning log level may be used to indicate that something went wrong but the overall operation was still able to complete successfully.\nfunc (a *App) UpdateUserRoles(..) { .. if result := \u003c-schan; result.NErr != nil { // soft error since the user roles were still updated mlog.Warn(\"Error during updating user roles\", mlog.Err(result.NErr)) } a.InvalidateCacheForUser(userId) .. } Info: This log level should be used to record normal, expected application behavior, even if it results in an error for the end user. They are not actionable individually, but the significant changes in the frequency of occurrence of individual log lines at this level may be indicative of a possible problem.\nFor example, the info log level may be used to communicate to administrators that certain subsystems within the service have been started or stopped.\nfunc (s *Schedulers) Start(..) { s.startOnce.Do(func() { mlog.Info(\"Starting schedulers.\") .. }) .. } Debug: This log-level is used for diagnostic information which may be used to debug issues but is not necessary for normal production system logging, nor actionable by system administrators.\nfunc (worker *Worker) Run() { mlog.Debug(\"Worker started\", mlog.String(\"worker\", worker.name)) .. Performance sensitive areas Any PR that can potentially have a performance impact on the mattermost/server codebase is encouraged to have a performance review. For more information, please see this link. The following is a brief list of indicators that should to undergo a performance review:\nNew features that might require benchmarks and/or are missing load-test coverage. PRs touching performance of critical parts of the codebase (e.g. Hub/WebConn). PRs adding or updating SQL queries. Creating goroutines. Doing potentially expensive allocations: bytes.Buffer and []byte: Use of Buffer.Grow, Buffer.ReadFrom, ioutil.ReadAll. Creating big slices and maps without capacity when size is known in advance. Recursion, unbounded, and deeply nested for loops. Use of locks and/or other synchronization primitives. Regular expressions, especially when creating regexp.MustCompile dynamically every time. Use of the reflect package. Propose a new rule To propose a new rule, follow the process below:\nAdd it to the agenda in the Server Guild meeting, and propose it. If it gets accepted, create a go-vet rule (if possible), or a golangci-lint rule to prevent new regressions from creeping in. Fix all existing issues. Add it to this guide. ","permalink":"https://developers.mattermost.com/contribute/more-info/server/style-guide/","section":"contribute","subsection":"more info","tags":null,"title":"Golang style guide"},{"categories":null,"contents":"What are feature flags Feature flag is a software development technique that turns functionality on and off without deploying new code. Feature flags allow us to be more confident in shipping features continuously to Mattermost Cloud. Feature flags also allow us to control which features are enabled on a cluster level.\nHow to use feature flags When to use There are no hard rules on when a feature flag should be used. It is left up to the best judgement of the responsible engineers to determine if a feature flag is required. The following are guidelines designed to help the determination:\nAny “substantial” feature should have a flag Features that are probably substantial: Features with new UI or changes to existing UI Features with a risk of regression Features that are probably not substantial: Small bug fixes Refactoring Changes that are not user facing and can be completely verified by unit and E2E testing. In all cases, ask yourself: Why do I need to add a feature flag? If I don’t add one, what options do I have to control the impact on user experience (e.g. a config setting or System Console setting)?\nAdd the feature flag in code Add the new flag to the feature flag struct located in model/feature_flags.go. Set a default value in the SetDefaults function in the same file. Use the feature flag in code as you would use a regular configuration setting. In tests, manipulate the configuration value to test value changes, such as activation and deactivation of the feature flag. Code may be merged regardless of setup in the management system. In this case it will always take the default value supplied in the SetDefaults function. Create a removal ticket for the feature flag. All feature flags should be removed as soon as they have been verified by Cloud. The ticket should encompass removal of the supporting code and archiving in the management system. Feature flag code guidelines A ticket should be created when a feature flag is added to remove the feature flag as soon as it isn’t required anymore. Tests should be written to verify the feature flag works as expected. Note that in cases where there may be a migration or new data, off to on and on to off should both be tested. Log messages by the feature should include the feature flag tag, with the feature flag name as a value, in order to ease debugging. Changing Feature Flag Values Self Hosted (and local development) Feature flag values can be changed via environment variables. The environment variable set follows the pattern MM_FEATUREFLAGS_\u003cname\u003e where \u003cname\u003e is the uppercase key of the feature flag you added to model/feature_flags.go\nCloud Feature flag adjustments (ie, turning something on or off) in the Mattermost Cloud environment are owned and controlled by the Cloud team. To change the value for a feature flag, please open a ticket.\nTimelines for rollouts Typically feature flag will initially disable the feature. It’s a good idea to test the feature during a safe time or on a subset of instances. Each team can decide what’s best and there’s no need to request the flag value changes from the Cloud team. If you think there might be a performance impact there’s no harm in communicating your plan beforehand.\nNote: The steps below are an initial guideline and will be iterated on over time.\n1st week after feature is merged (T-30): 10% rollout; only to test servers, no rollout to customers. 2nd week (T-22): 50% rollout; rollout to some customers (excluding big customers and newly signed-up customers); no major bugs in test servers. 3rd week (T-15): 100% rollout; no major bugs from customers or test servers. End of 3rd week (T-8): Remove flag. Feature is production ready and not experimental. For smaller, non-risky features, the above process can be more fast tracked as needed, such as starting with a 10% rollout to test servers, then 100%. Features have to soak on Cloud for at least two weeks for testing. Focus is on severity and number of bugs found; if there are major bugs found at any stage, the feature flag can be turned off to roll back the feature.\nWhen the feature is rolled out to customers, logs will show if there are crashes, and normally users will report feedback on the feature (e.g. bugs).\nSelf-hosted releases For self-hosted releases, typically a flagged feature will be released in an enabled state. That said, you can release a feature to self-hosted disabled, it's not unprecedented.\nTests Tests should be written to verify all states of the feature flag. Tests should cover any migrations that may take place in both directions (i.e., from “off” to “on” and from “on” to “off”). Ideally E2E tests should be written before the feature is merged, or at least before the feature flag is removed.\nExamples of feature flags Some examples are here.\nFAQ What are the expected values for boolean feature flags?\nNormally true or false, but this may not always equate to enabled/disabled. A feature flag that introduces three new sorting algorithms can also be written: “selection” (default, the existing strategy in production) “bubble” “quick” Is it possible to use a plugin feature flag such as PluginIncidentManagement to “prepackage” a plugin only on Cloud by only setting a plugin version to that flag on Cloud? Can self-hosted customers manually set that flag to install the said plugin?\nYes. If you leave the default \"\" then nothing will happen for self-hosted installations. How do feature flags work on webapp?\nTo add a feature flag that affects frontend, the following is needed: PR to server code to add the new feature flag. PR to redux to update the types. PR to webapp to actually use the feature flag. How do feature flags work on mobile?\nTo add a feature flag that affects mobile, the following is needed: PR to server code to add the new feature flag. PR to mobile to update the types and to actually use the feature flag. What is the environment variable to set a feature flag?\nIt is MM_FEATUREFLAGS_\u003cmyflag\u003e. Can plugins use feature flags to enable small features aside of the version forcing feature flag?\nYes. You can create feature flags as if they were added for the core product, and they’ll get included in the plugin through the config. Do feature flag changes require the server to be restarted?\nFeature flags don’t require a server restart unless the feature being flagged requires a restart itself. For features that are requested by self-hosted customers, why do we have to deploy to Cloud first, rather than having the customer who has the test case test it?\nCloud is the way to validate the stability of the feature before it goes to self-hosted customers. In exceptional cases we can let the self-hosted customer know that they can use environment variables to enable the feature flag (but specify that the feature is experimental). How does the current process take into account bugs that may arise on self-hosted specifically?\nThe process hasn’t changed much from the old release process: Features can still be tested on self-hosted servers once they have been rolled out to Cloud. The primary goal is that bugs are first identified on Cloud servers. How can self-hosted installations set feature flags?\nSelf-hosted installations can set environment variables to set feature flag values. However, users should recognize that the feature is still considered “experimental” and should not be enabled on production servers. ","permalink":"https://developers.mattermost.com/contribute/more-info/server/feature-flags/","section":"contribute","subsection":"more info","tags":null,"title":"Feature flags"},{"categories":null,"contents":"Debug the main process The simplest way to debug the main process is to simply insert logging statements wherever needed and have the application output logs of whatever is necessary.\nFor already built applications (or bugs that only appear in the packaged version of the application), you can view the Logs by going to Help \u003e Show Logs in the 3-dot menu, which will open a file manager window showing the location of the log file.\nIf you’d like to make use of better debugging tools, you can use the Chrome Dev Tools or the debugger in VSCode by following the steps here: https://www.electronjs.org/docs/latest/tutorial/debugging-main-process\nDebug the renderer process The renderer processes are controller by Chrome instances, so each of them will have their own Developer Tools instance.\nYou can access these instances by going to the View \u003e Developer Tools menu (under the 3-dot menu on Windows/Linux, and in the top bar on macOS) and selecting:\nDeveloper Tools for Application Wrapper for anything involving the top bar. Developer Tools for Current Tab for anything involving the Mattermost view or the preload script. Note: For this one, make sure you’re currently on the tab where you want to load the Developer Tools. You can have instances open for tabs you aren’t currently viewing, but to open them in the first place requires it to be opened. Developer Tools for Call Widget if you are using Mattermost Calls and the calls widget is currently open. There are other BrowserViews that are governed seperately from the main application wrapper, including:\nDropdown Menu You can open this one by adding a line in the main/teamDropdownView.ts file. In the constructor, at the end, add: this.view.webContents.openDevTools({mode: 'detach'}); Modals You can open these by setting an environment variable when running the Desktop App called MM_DEBUG_MODALS. // macOS/Linux export MM_DEBUG_MODALS=1 // Windows PowerShell $env:MM_DEBUG_MODALS = 1 URL View You can open this one by adding a line in the main/viewManager.ts file. In the function showURLView, at the end, add: urlView.webContents.openDevTools({mode: 'detach'}); Note: This view is ephemeral and based on whether a link is hovered with the mouse, so it might be best to use some logging instead here. Debug the Mattermost Server/webapp Some issues are only reproducible on the Desktop App, though the code that is causing the issue may not live in the Desktop App.\nHere are some ways of determining whether this is true:\nDoes the issue reproduce on the browser? Specifically Chrome? Does the issue surround a piece of code on the server/webapp that only applies to the Desktop App? You can check this by seeing if there is a call to isDesktopApp in the webapp. If you have determined that the issue doesn’t apply to the Desktop App code base directly, you can file a ticket in the appropriate repository, such as the server and web app repository.\nIf you are having trouble determining where the issue lies, feel free to post in the Developers: Desktop App on Mattermost Community, or you can file a ticket in the server and web app repository and it will be triaged and transferred to the appropriate location.\n","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/debugging/","section":"contribute","subsection":"more info","tags":null,"title":"Debug the desktop app"},{"categories":null,"contents":"The Desktop App uses npm to manage its dependencies.\nWe usually try to keep each major dependency version locked such that we don’t accidentally introduce any bugs or breaking changes by upgrading.\nAll dependencies are locked using a package-lock.json file to ensure that we don’t change the package versions used to build unless explicitly upgrading the package. Thus if a PR contains changes to package-lock.json without explicitly changing a dependency, we will usually ask the contributor to revert those changes.\nElectron The most important dependency for the Desktop App is Electron, and is usually the library that can have the most impact on how the Desktop App works. The Electron dependency also contains the Chromium driver.\nGenerally we try to use the latest possible version of Electron where applicable to ensure we have the latest security fixes and are using the latest possible version of Chromium to maintain compatibility with the Web App.\nUpgrading For patch releases of the Desktop App, we will generally upgrade Electron to the latest patch version.\nFor major and minor version releases of the Desktop App, we will upgrade to the latest major version.\nThis will usually require QA testing to ensure that nothing has broken between versions. Bug fixes Sometimes, it’s necessary to upgrade the Electron version in order to resolve a bug in the app caused by the framework. If this is the case, please change the dependency according to the above guidelines, and the PR will be merged and released as per the same guidelines.\nOther dependencies We try to keep the majority of dependencies up-to-date as much as possible, with a few exceptions:\nReact: We generally keep the same version of react in the Desktop App for as long as possible, unless an upgrade or a new feature is required. Since the Desktop App doesn’t rely too heavily on react, it’s better for us to avoid introducing potential breaking changes unless something urgently needs to change. Webpack: Upgrading webpack requires us to change our configuration significantly, so we generally keep it the same unless we need to make a change. ","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/dependencies/","section":"contribute","subsection":"more info","tags":null,"title":"Dependencies"},{"categories":null,"contents":"We run automated style and type-checking against every new PR that is created and the new code must pass before it can be merged.\nIn some rare cases you can override these, but this is strongly discouraged.\nLinter We make use of eslint to enforce good coding style in the Desktop App.\nYou can run the linter using the following command:\nnpm run lint:js Outside of the linter, we generally allow for a loose coding style, although the reviewer of the PR has the final say.\nType checker We make use of TypeScript in our application to help reduce errors when coding.\nYou can run the type checker by running the following command:\nnpm run check-types Submitting great PRs Jesse Hallam has written an excellent blog post entitled “Submitting Great PRs” that can be found here\n","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/style-and-code-quality/","section":"contribute","subsection":"more info","tags":null,"title":"Style and code quality"},{"categories":null,"contents":"We’ve made it easy to white label the mobile app and to replace and override the assets used, however, you have to Build Your Own App from source.\nIf you look at the Project Folder Structure, you’ll see that there is an assets folder containing a base folder with assets provided by Mattermost. These include localization files and images as well as a release folder that optionally contains the icons and the splash screen of the app when building in release mode.\nTo replace these with your own assets, create a sub-directory called override in the assets folder. The assets that you add using the same directory structure and file names as in the base directory, will be used instead of the original ones.\nLocalization strings To replace these with your own assets, create a sub-directory called override in the assets folder. Using the same directory structure and file names as in the base directory, you can add assets to the override folder to be used instead.\nFor example, to override assets/base/images/logo.png you would replace your own logo.png file in assets/override/images/logo.png.\nImages To replace an image, copy the image to assets/override/images/ with the same location and file name as in the base folder.\nNote: Make sure the images have the same height, width, and DPI as the images that you are overriding. App splash screen and launch icons In the assets directory you will find a folder named assets/base/release which contains an icons folder and a splash_screen folder under each platform directory.\nCopy the full release directory under assets/override/release and then replace each image with the same name. Make sure you replace all the icon images for the platform you are building the app - the same applies to the splash screen.\nThe splash screen’s background color is white by default and the image is centered. If you need to change the color or the layout to improve the experience of your new splash screen make sure that you also override the file launch_screen.xml for Android and LaunchScreen.storyboard for iOS. Both can be found under assets/base/release/splash_screen/\u003cplatform\u003e/.\nSplash screen and launch icons assets are replaced at build time when the environment variable REPLACE_ASSETS is set to true (default is false).\nNote: Make sure the images have the same height, width, and DPI as the images that you are overriding. Configuration The config.json file handles custom configuration for the app for settings that cannot be controlled by the Mattermost server. Like with localization strings, create a config.json file under assets/override and just include the keys and values that you wish to change that are present in assets/base/config.json.\nFor example, if you want the app to automatically provide a server URL and skip the screen to input it, you would add the following to assets/override/config.json:\n{ \"DefaultServerUrl\": \"http://192.168.0.13:8065\", \"AutoSelectServerUrl\": true } Note: The above key/value pairs are taken from the original config.json file. Since we don’t need to change anything else, we only included these two settings. ","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/build-your-own/white-label/","section":"contribute","subsection":"more info","tags":null,"title":"White label"},{"categories":null,"contents":"This process describes how inactive contributions are managed at Mattermost, inspired by the Kubernetes project:\nAfter 10 days of inactivity, a contribution becomes stale and a bot will add the lifecycle/1:stale label to the contribution.\nIf action is required from submitter, Community Coordinator asks if the team can help clarify previous feedback or provide guidance on next steps. If action is required from reviewers, Community Coordinator asks reviewers to share feedback or help answer questions. The Coordinator will follow up with reviewers until a response is received. After 20 days of inactivity, a contribution becomes inactive.\nCommunity Coordinator asks the submitter if the team can help with questions. They acknowledge that after another 30 days of inactivity the contribution will be closed. They also add a lifecycle/2:inactive label to the contribution. Note: Contributions should never become orphaned because of reviewers. The Coordinator will be responsible for receiving a response from the reviewers during the stale period, which may be that the maintainers aren’t able to accept the contribution in its current form. After 30 days of inactivity, a contribution becomes orphaned.\nCommunity Coordinator notes that the contribution has been inactive for 60 days, thanks for the contribution and closes the contribution. They also add an lifecycle/3:orphaned label to the contribution, and adds an Up For Grabs label to the associated help wanted ticket, if appropriate. Exceptions:\nIf the contribution is inactive but shouldn’t be closed, the Coordinator adds a lifecycle/frozen label to the contribution. An example of this is when a design decision is being discussed but no decision has been arrived at yet. Once the contribution reaches the lifecycle/2:inactive state, it is eligible to be assumed by another community member interested in working on the ticket. Invalid PRs may be closed immediately without advancing through this lifecycle, especially if the contributor is unresponsive. ","permalink":"https://developers.mattermost.com/contribute/more-info/getting-started/inactive-contributions/","section":"contribute","subsection":"more info","tags":null,"title":"Inactive contributions"},{"categories":null,"contents":"If you haven’t set up your developer environment, please do so before continuing with this section.\nJoin the Developers community channel to ask questions from community members and the Mattermost core team.\nWorkflow Here’s a general workflow for a Mattermost developer working on the mattermost repository:\nMaking code changes Review the repository structure to familiarize yourself with the project:\n./server/channels/api4/ holds all API and application related code. ./server/public/model/ holds all data model definitions and the Go driver. ./server/channels/store/ holds all database querying code. ./server/channels/utils/ holds all utilities, such as the mail utility. ./server/i18n/ holds all localization files for the server. On your fork, create a feature branch for your changes. Name it MM-$NUMBER_$DESCRIPTION where $NUMBER is the Jira ticket number you are working on and $DESCRIPTION is a short description of your changes. Example branch names are MM-18150_plugin-panic-log and MM-22037_uppercase-email.\nMake the code changes required to complete your ticket.\nRunning and writing tests Ensure that unit tests are written or modified where appropriate. For the server repository in general, Mattermost follows the opinionated way of testing in Go. You can learn more about this process in DigitalOcean's How To Write Unit Tests in Go tutorial. Test files must always end with _test.go, and should be located in the same folder where the code they are checking lives. For example, check out download.go and download_test.go, which are both located in the app folder. Please also use testify for new tests.\nIf you made changes to the store, run make store-mocks and make store-layers to update test mocks and timing layer.\nTo test your changes, run make run-server from the root directory of the server repository. This will start up the server at http://localhost:8065. To get changes to the server it must be restarted with make restart-server. If you want to test with the web app, you may also run make run which will start the server and a watcher for changes to the web app.\nOnce everything works to meet the ticket requirements, stop Mattermost by running make stop in the server repository, then run make check-style to check your syntax.\nRun the tests using one or more of the following options:\nRun make test to run all the tests in the project. This may take a long time and provides very little feedback while it’s running. Run individual tests by name executing go test -run \"TestName\" ./\u003cdirectory\u003e. Run all the tests in a package where changes were made executing go test app. Create a draft PR with your changes and let our CI servers run the tests for you. Running every single unit test takes a lot of time while making changes, so you can run a subset of the server-side unit tests by using the following:\ngo test -v -run='\u003ctest name or regex\u003e' ./\u003cpackage containing test\u003e For example, if you want to run TestUpdatePost in app/post_test.go, you would execute the following:\ngo test -v -run='TestUpdatePost' ./app If you added or changed any localization strings you will need to run make i18n-extract to generate the new/updated strings.\nTesting email notifications When Docker starts, the SMTP server is available on port 2500. A username and password are not required. You can access the Inbucket webmail on port 9000. For additional information on configuring an SMTP email server, including troubleshooting steps, see the SMTP email setup page in the Mattermost user documentation. Testing with GitLab Omnibus To test a locally compiled version of Mattermost with GitLab Omnibus, replace the following GitLab files: The compiled mattermost binary in /opt/gitlab/embedded/bin/mattermost. The assets (templates, i18n, fonts, webapp) in /opt/gitlab/embedded/service/mattermost. Creating a pull request (PR) Commit your changes, push your branch, and create a pull request. Once a PR is submitted it’s best practice to avoid rebasing on the base branch or force-pushing. Jesse, a developer at Mattermost, mentions this in his blog article Submitting Great PRs. When the PR is merged, all the PR’s commits are automatically squashed into one commit, so you don’t need to worry about having multiple commits on the PR. That’s it! Rejoice that you’ve helped make Mattermost better. Useful Server makefile commands Some useful make commands include:\nmake run runs the server, creates a symlink for your mattermost-webapp folder, and starts a watcher for the web app. make stop stops the server and the web app watcher. make run-server runs only the server and not the client. make debug-server will run the server in the delve debugger. make stop-server stops only the server. make update-docker stops and updates your Docker images. This is needed if any changes are made to docker-compose.yaml. make clean-docker stops and removes your Docker images and is a good way to wipe your database. make clean cleans your local environment of temporary files. make config-reset resets the config/config.json file to the default. make nuke wipes your local environment back to a completely fresh start. make package creates packages for distributing your builds and puts them in the ./dist directory. You will first need to run make build and make build-client. If you would like to run the development environment without Docker you can set the MM_NO_DOCKER environment variable. If you do this, you will need to set up your own database and any of the other services needed to run Mattermost.\nUseful Mattermost and mmctl commands During development you may want to reset the database and generate random data for testing your changes. For this purpose, Mattermost has the following commands in the Mattermost CLI:\nFirst, install the server with go install ./cmd/mattermost in the server repository.\nYou can reset your database to the initial state using:\nmattermost db reset The following commands need to be run via the mmctl tool.\nYou can generate random data to populate the Mattermost database using:\nmmctl sampledata Create an account using the following command:\nmmctl user create --email user@example.com --username test1 --password mypassword Optionally, you can assign that account System Admin rights with the following command:\nmmctl user create --email user@example.com --username test1 --password mypassword --system-admin Customize your workflow Makefile variables You can customize variables of the Makefile by creating a config.override.mk file or setting environment variables. To get started, you can copy the config.mk file to config.override.mk and change the values in your newly copied file.\nDocker-compose configurations If you create a docker-compose.override.yaml file at the root of the project, it will be automatically loaded by all the Makefile tasks using docker-compose, allowing you to define your own services or change the configuration of the ones Mattermost provides.\n","permalink":"https://developers.mattermost.com/contribute/more-info/server/developer-workflow/","section":"contribute","subsection":"more info","tags":null,"title":"Server workflow"},{"categories":null,"contents":"This page contains most of the information required for a developer to work with the Mattermost web app. Note that everything in this document will refer to working in the webapp directory of the main Mattermost repository unless otherwise stated.\nWorkflow If you haven’t done so already, set up your developer environment.\nOn your fork, create a feature branch for your changes. Name it MM-$NUMBER_$DESCRIPTION where $NUMBER is the Jira ticket number you are working on and $DESCRIPTION is a short description of your changes. Example branch names are MM-18150_plugin-panic-log and MM-22037_uppercase-email. You can also use the name GH-$NUMBER_$DESCRIPTION for tickets come from GitHub Issues.\nMake the code changes required to complete your ticket, making sure to write or modify unit tests where appropriate. Use make test to run the unit tests.\nTo run your changes locally, you’ll need to run both the client and server. The server and client can either be run together or separately as follows:\nYou can run both together by using make run from the server directory. Both server and web app will be run together and can be stopped by using make stop. If you run into problems getting the server running this way, you may want to consider running them separately in case the output from one is hiding errors from the other.\nYou can run the server independently by running make run-server from its directory and, using another terminal, you can run the web app by running make run from the web app directory. Each can be stopped by running make stop-server or make stop from their respective directories.\nOnce you’ve done either of those, your server will be available at http://localhost:8065 by default. Changes to the web app will be built automatically, but changes to the server will only be applied if you restart the server by running make restart-server from the server directory.\nIf you added or changed any translatable text, you will need to update the English translation files to make them available to translators for other languages. You can do that by navigating to channels and running make i18n-extract to update src/i18n/en.json.\nRemember to double check that any newly added strings have the correct values in case they weren’t detected correctly. Generally, only en.json should be modified directly from this repository. Other languages’ translation files are updated using Weblate. Before submitting a PR, make sure to check your coding style and run the automated tests on your changes. These are checked automatically by CI, but they should be run manually before submitting changes to ensure the review process goes smoothly.\nTo check the code style and run the linter, run make check-style. If any problems are encountered, they may be able to be automatically fixed by using make fix-style. To run the type checker, use make check-types. To run the unit tests, run make test. Commit your changes, push your branch and create a pull request.\nRespond to feedback on your pull request and make changes as necessary by committing to your branch and pushing it. Your branch should be kept roughly up to date by merging master into it periodically. This can either be done using `git merge` or, as long as there are no conflicts, by commenting /update-branch on the PR.\nThat’s it! Rejoice that you’ve helped make Mattermost better.\nUseful Mattermost commands During development you may want to reset the database and generate random data for testing your changes. See the corresponding section of the server developer workflow for how to do that.\n","permalink":"https://developers.mattermost.com/contribute/more-info/webapp/developer-workflow/","section":"contribute","subsection":"more info","tags":null,"title":"Web app workflow"},{"categories":null,"contents":"Now that the app can receive push notifications, we need to make sure that the Mattermost Push Notification Service is able to send the notification to the device. This guide will focus on installing and configuring the push notification service.\nRequirements A Linux or FreeBSD box server with at least 1GB of memory. A copy of the Mattermost Push Notification Service. Custom Android and/or iOS Mattermost mobile apps. An APNs Auth Key (.p8) obtained by following the iOS Push Notifications guide. A Firebase Cloud Messaging Server key obtained by following the Android Push Notifications guide. Install and upgrade For the sake of making this guide simple we located the files at /home/ubuntu/mattermost-push-proxy. We’ve also elected to run the Push Notification Service as the ubuntu account for simplicity. We recommend setting up and running the service under a mattermost-push-proxy user account with limited permissions.\nDownload the latest Mattermost Push Notification Service:\nwget https://github.com/mattermost/mattermost-push-proxy/releases/download/vX.X.X/mattermost-push-proxy-linux-amd64.tar.gz\nor\nwget https://github.com/mattermost/mattermost-push-proxy/releases/download/vX.X.X/mattermost-push-proxy-freebsd-amd64.tar.gz\nIn this command, vX.X.X refers to the release version you want to download. See Mattermost Push Notification Service releases.\nIf you’re upgrading a previous version of the Mattermost Push Notification Service make sure to back up your mattermost-push-proxy.json file before continuing.\nUnzip the downloaded Mattermost Push Notification Service using:\ntar -xvzf mattermost-push-proxy-linux-amd64.tar.gz\nor\ntar -xvzf mattermost-push-proxy-freebsd-amd64.tar.gz\nConfigure the Mattermost Push Notification service by editing the mattermost-push-proxy.json file at /home/ubuntu/mattermost-push-proxy/config. Follow the steps in the Android and iOS sections to replace the values in the config file.\nCreate a systemd unit file to manage the Mattermost Push Notification Services with systemd and log all output of the service to /var/log/syslog by running this command as root.\necho \"[Unit] Description=Mattermost Push Notification Service [Service] Type=oneshot ExecStart=/bin/sh -c '/home/ubuntu/mattermost-push-proxy/bin/mattermost-push-proxy | logger' WorkingDirectory=/home/ubuntu/mattermost-push-proxy [Install] WantedBy=multi-user.target\" \u003e\u003e /etc/systemd/system/mattermost-push-proxy.service To route the traffic through a separate proxy server, add Environment=\"HTTP_PROXY=\u003chttp://server\u003e\" under the [Service] section of the file. If you have an HTTPS server, then use HTTPS_PROXY. If you set both then HTTPS_PROXY will take higher priority than HTTP_PROXY.\nStart the service with sudo systemctl start mattermost-push-proxy or restart with sudo systemctl restart mattermost-push-proxy. Use sudo systemctl enable mattermost-push-proxy to have systemd start the service on boot.\nSet up Mattermost push notification service to send Android push notifications Go to the Firebase Console and select the project you’ve created. Once in the dashboard, go to the project settings and select Service Accounts. Click on Generate new private key and store the downloaded file. Open the mattermost-push-proxy.json file in the mattermost-push-proxy/config directory and look for the “ServiceFileLocation” entry under “AndroidPushSettings”. Paste the location of the file as its value.\n\"ServiceFileLocation\": \"/path/to/downloaded_file\" Set up Mattermost push notification service to send iOS push notifications Instead of certificates, we now recommend using an APNs Auth Key (.p8) to authenticate with Apple Push Notification service (APNs).\nIf you haven’t generated your key yet, see Generate an APNs Auth Key.\nOpen the mattermost-push-proxy.json file under the mattermost-push-proxy/config directory and configure it with your key details:\n\"ApplePushSettings\":[ { \"Type\":\"apple_rn\", \"ApplePushUseDevelopment\":true, \"ApplePushTopic\":\"your.bundle.id\", \"AppleAuthKeyFile\":\"./config/beta/YourAuthKeyFile.p8\", \"AppleAuthKeyID\":\"YourAuthKeyID\", \"AppleTeamID\":\"YourAppleTeamID\" } ], ApplePushTopic: Your app’s bundle ID (APNs topic).\nAppleAuthKeyFile: Path to the .p8 file.\nAppleAuthKeyID: Key ID from Apple Developer portal.\nAppleTeamID: Team ID from Apple Developer Membership.\nApplePushUseDevelopment: true for sandbox APNs, false for production.\nNote: If you are migrating from certificate-based authentication, you can remove the ApplePushCertPrivate field and replace it with the new AppleAuthKeyFile, AppleAuthKeyID, and AppleTeamID values. Configure the Mattermost Server to use the Mattermost push notification service In your Mattermost instance, enable mobile push notifications.\nGo to System Console \u003e Notifications \u003e Mobile Push.\nUnder Send Push Notifications, select Manually enter Push Notification Service location.\nEnter the location of your Mattermost Push Notification Service in the Push Notification Server field.\n(Optional) Customize mobile push notification contents.\nGo to System Console \u003e Notifications \u003e Mobile Push.\nSelect an option for Push Notification Contents to specify what type of information to include in the push notifications.\nMost deployments choose to include the full message snippet in push notifications unless they have policies against it to protect confidential information.\nFinally, start your Mattermost Push Notification Service, and your app should start receiving push notifications.\nTest the Mattermost push notification service Verify that the server is functioning normally and test the push notification using curl: curl http://127.0.0.1:8066/api/v1/send_push -X POST -H \"Content-Type: application/json\" -d '{\"type\": \"message\", \"message\": \"test\", \"badge\": 1, \"platform\": \"PLATFORM\", \"server_id\": \"MATTERMOST_DIAG_ID\", \"device_id\": \"DEVICE_ID\", \"channel_id\": \"CHANNEL_ID\"}'\nReplace MATTERMOST_DIAG_ID with the value found by running the SQL query: SELECT * FROM Systems WHERE Name = 'DiagnosticId'; Replace DEVICE_ID with your device ID, which can be found using (where your_email@example.com is the email address of the user you are logged in as): SELECT Email, DeviceId FROM Sessions, Users WHERE Sessions.UserId = Users.Id AND DeviceId != '' AND Email = 'your_email@example.com'; Replace CHANNEL_ID with the Town Square channel ID, which can be found using: SELECT Id FROM Channels WHERE DisplayName = 'Town Square'; Migration Remove the apple:, apple_rn, android: or android_rn: prefix from your device ID before replacing DEVICE_ID. Use that prefix as the PLATFORM (make sure to remove the “:”). You can also verify push notifications are working by opening your Mattermost site and mentioning a user who has push notifications enabled in Settings \u003e Notifications \u003e Mobile Push Notifications. To view the log file, use:\n$ sudo tail -n 1000 /var/log/upstart/mattermost-push-proxy.log Note: Note that device IDs can change somewhat frequently, as they are tied to a device session. If you’re having trouble, double-check the latest device IDs by re-running the above queries. Troubleshooting DeviceTokenNotForTopic For iOS / Apple Push Notifications: If the logs are reflecting DeviceTokenNotForTopic (error 400) this may be because you’re using an older / previous Device ID. Re-run the queries you need to get device IDs and test.\nThis could also be because you generated a key for the wrong bundle ID. The bundle ID used in mattermost-push-proxy.json should be the same one as the app, and should be for the same app it was generated for.\nReporting issues For issues with repro steps, please report to https://github.com/mattermost/mattermost/issues\n","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/push-notifications/service/","section":"contribute","subsection":"more info","tags":null,"title":"Push notification service"},{"categories":null,"contents":"When building a custom version of the Mattermost mobile app, you will also need to host your own Mattermost Push Notification Service and make a few modifications to your Mattermost mobile app to be able to get push notifications.\nSetup the custom mobile apps to receive push notifications Android iOS Setup the Mattermost push notification service If the use of a proxy server is required by your IT policy, see the corporate proxy page.\n","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/push-notifications/","section":"contribute","subsection":"more info","tags":null,"title":"Set up push notifications"},{"categories":null,"contents":"To contribute to Mattermost, you must sign the Contributor License Agreement. Doing so adds you to our list of Mattermost Approved Contributors. Please also read our community expectations and note that we all abide by the Mattermost Code of Conduct (CoC), and by joining our contributor community, you agree to abide by it as well.\nTip: Love swag? If you choose to provide us with your mailing address in the signed agreement, you’ll receive a Limited Edition Mattermost Mug as a thank you gift after your first pull request is merged. Before contributing There are many ways to contribute to Mattermost beyond a core Mattermost repository:\nYou can create lightweight external applications that don’t require customizations to the Mattermost user experience by using incoming and outgoing webhooks, or by using the Mattermost API. You can activate external functionality within Mattermost by creating custom slash commands. You can extend, modify, and deeply integrate with the Mattermost server and its UI/UX by using plugins. However, please note that plugin development comes with the highest level of overhead and must be written in Go and React. You can use Mattermost from other applications, by embedding and launching Mattermost within other applications and mobile apps. To get started:\nIdentify which repository you need to work in (see point below), then review the README located within the root of the repository to learn more about getting started with your contribution and any processes that may be unique to that repository.\nThese are the Mattermost Core repositories you can contribute to:\nServer: Highly-scalable Mattermost server written in Go. Web App: JavaScript client app built on React and Redux. Mobile Apps: JavaScript client apps for Android and iOS built on React Native. Desktop App: An Electron wrapper around the web app project that runs on Windows, Linux, and macOS. Core Plugins: A core set of officially-maintained plugins that provide a variety of improvements to Mattermost. Boards and Playbooks core integrations. To contribute to documentation, you should be able to edit any page and get to the source file in the documentation repository by selecting the Edit on GitHub button in the top right of its respective published page. You can read more about this process on the why and how to contribute page. You can contribute to the following Mattermost documentation sites:\nProduct documentation Developer documentation API reference documentation Handbook documentation During the contribution process Check in regularly with your Pull Request (PR) to review and respond to feedback. Thoroughly document what you’re doing in your PR. This way, future contributors can pick up on your work (including you!). This is especially helpful if you need to step back from a PR. Each PR should represent a single project, both in code and in content. Keep unrelated tasks in separate PRs. Make your PR titles and commit messages descriptive! Briefly describing the project in the PR title and in your commit messages often results in faster responses, less clarifying questions, and better feedback. Tip: If you need to take a break from an assigned issue during, for example, the Hacktoberfest project, please commit any completed work to date in a PR, and note that you’re stepping away in the issue itself. These two steps help ensure that your contributions are counted and outstanding work on a given ticket can be made available to other contributors. Writing code Thoroughly test your contributions! We recommend the following testing best practices for your contribution:\nDetail exactly what you expect to happen in the product when others test your contributions. Identify updates to existing product, developer, and/or API documentation based on your contributions, and identify documentation gaps for new features or functionality. Note: Contributors and reviewers are strongly encouraged to work with the Mattermost Technical Writing team via the Documentation Working Group channel on the Mattermost Community Server before approving community contributions. See the Mattermost Handbook for additional details on engaging the Mattermost Technical Writing team, and for submitting documentation with your PR. If your PR adds a new plugin API method or hook, please add an example to the Plugin Starter Template. If your code adds a new user interface string, include it in the proper localization file, either for the server, the webapp, or mobile. Note: When working within the webapp repository, additionally run make i18n-extract from a terminal to update the list of product strings with your changes. Writing content Always consider who will consume your content, and write directly to your target audience.\nWrite clearly and be concise. Write informally, in the present tense, and address the reader directly. See our voice, tone, and writing style guidelines, and the Mattermost Documentation Style Guide for details on general writing principles, syntax used to format content, and common terms used to describe product functionality.\n","permalink":"https://developers.mattermost.com/contribute/expectations/","section":"contribute","subsection":"contributor expectations","tags":null,"title":"Contributor expectations"},{"categories":null,"contents":"Here are a few links to Mattermost content that you might find helpful depending on the specific area where you’ll be contributing.\nMattermost resources Specific localization guides (coming soon!) Mattermost contributor agreement Approved contributor list The API Incoming and outgoing webhooks Plugins, also Plugins Making slash commands Embedding Mattermost in other places Server project Webapp project Mobile apps Desktop app Focalboard Playbooks External resources How ICU syntax works Use gender-neutral language in communications and in content ","permalink":"https://developers.mattermost.com/contribute/more-info/","section":"contribute","subsection":"more info","tags":null,"title":"Where to find more information?"},{"categories":null,"contents":"To provide access to different servers, we create a series of BrowserView objects that directly render on top of the Main Window and load the Mattermost Web App directly from the server they correspond to. We wrap these BrowserView objects into a MattermostView that manages the loading of the view and handles events such as navigation and notifications.\nThese views are also contained within and managed by the viewManager class. The class is responsible for adding and removing the views from the Main Window when the user needs them and handling IPC calls from the renderer processes and passing them to the child objects.\n","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/architecture/external-views/","section":"contribute","subsection":"more info","tags":null,"title":"External views"},{"categories":null,"contents":"The Mattermost mobile apps are written in JavaScript using React Native.\nRepository Further explanation of the file structure can be found in the Developer Setup. For more information about E2E testing on the Android and iOS apps, check out Mobile End-to-End (E2E) tests\nMobile app contributor resources GitHub Repository - Get the code, report issues, or submit PRs. Running the app - Compile recommendations. Channel - Use the channel to interact with other Mattermost Mobile developers. Help Wanted - Find help wanted tickets here. Android specific documentation Build guide Push Notifications Sign Unsigned Builds iOS specific documentation Build guide Push Notifications Sign Unsigned Builds ","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/","section":"contribute","subsection":"more info","tags":null,"title":"Mobile apps"},{"categories":null,"contents":"When your IT policy requires a corporate proxy to scan and audit all outbound traffic the following options are available:\n1. Deploy Mattermost with connection restricted post-proxy relay in DMZ or a trusted cloud environment Some legacy corporate proxy configurations may be incompatible with the requirements of modern mobile architectures, such as the requirement of HTTP/2 requests from Apple to send push notifications to iOS devices.\nIn this case, a post-proxy relay (which accepts network traffic from a corporate proxy such as NGINX, and transmits it to the final destination) can be deployed to take messages from the Mattermost server passing through your corporate IT proxy in the incompatible format, e.g. HTTP/1.1, transform it to HTTP/2 and relay it to its final destination, either to the Apple Push Notification Service (APNS) and Google Fire Cloud Messaging (FCM) services.\nThe post-proxy relay can be configured using the Mattermost Push Proxy installation guide with connection restrictions to meet your custom security and compliance requirements.\nYou can also host in a trusted cloud environment such as AWS or Azure in place of a DMZ (this option may depend on your organization’s internal policies).\n2. Whitelist Mattermost push notification proxy to bypass your corporate proxy server Depending on your internal IT policy and approved waivers/exceptions, you may choose to deploy the Mattermost Push Proxy to connect directly to Apple Push Notification Service (APNS) without your corporate proxy.\nYou will need to whitelist one subdomain and one port from Apple for this option:\nDevelopment server: api.development.push.apple.com:443 Production server: api.push.apple.com:443 3. Run App Store versions of the Mattermost mobile apps You can use the mobile applications hosted by Mattermost in the Apple App Store or Google Play Store and connect with Mattermost Hosted Push Notification Service (HPNS) through your corporate proxy.\nThe use of hosted applications by Mattermost can be deployed with Enterprise Mobility Management solutions via AppConfig. Wrapping is not supported with this option.\n","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/push-notifications/corporate-proxy/","section":"contribute","subsection":"more info","tags":null,"title":"Corporate proxy"},{"categories":null,"contents":"Build You can build the Desktop App by running the following command:\nnpm run build You can build the Desktop App for development and watch for changes in the main process with this command:\nnpm run watch Our application uses webpack to bundle the scripts together for the main and renderer process.\nThere are bundles generated for each page used the renderer process, and one bundle for the main process.\nA bundle is also generated for the E2E tests when needed.\nYou can predefine certain variables in the app before building, by editing the build config under src/common/config/buildConfig.ts. For example, you can predefine servers, or disable server management.\nPackage Our app uses electron-builder to package the app into a distributable format for release.\nYou can find the configuration for the builder in the electron-builder.json file in the root directory.\nYou can run the packager using this command:\nnpm run package:\u003cos\u003e where \u003cos\u003e is one of the following values: windows, mac, mac-with-universal, mas, linux\nAll of the above values will generate builds for x64 and arm64 architectures:\nwindows: exe, zip and msi formats mac: dmg and zip formats mac-with-universal: universal binary for all architectures - dmg mas: universal Mac App Store-compliant build linux: deb, rpm and tar.gz formats You can build for more specific targets using the commands here\nAfter pack script We include an afterPack script to run functions after the application is built into a binary. This is a good place to inject code and make any modifications to the binary after build.\nCode sign In order to generate signed builds of the application for Windows and macOS, you’ll need a certificate file for each of the operating systems.\nThese files are under control of Mattermost and aren’t generally distributed, but you can obtain your own certificate and sign the app yourself if necessary.\nFor macOS, you’ll need a valid Mac Developer or Developer ID Application certificate from the Apple Developer Program.\nFor Windows, you’ll need a valid code signing certificate.\nMore information on Code Signing can be found here: https://www.electron.build/code-signing\nRelease Releasing a new version of the Desktop App can be done by running the shell script release.sh under the scripts/ folder.\nIt will increment the version number in package.json for you, create a tag and generate a commit for you. It will also give you the git command to run to push all these changes to your repository.\nIt has the following options:\n// generates a patch version release candidate, will increment x of v0.0.x (so v5.0.1 becomes v5.0.2-rc1) $ ./scripts/release.sh patch // generates a release candidate version, on top of a current release candidate (so v5.0.2-rc1 becomes v5.0.2-rc2) $ ./scripts/release.sh rc // generates a final version, on top of a current release candidate (so v5.0.2-rc2 becomes v5.0.2) $ ./scripts/release.sh final ","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/packaging-and-releasing/","section":"contribute","subsection":"more info","tags":null,"title":"Package and release"},{"categories":null,"contents":"For most changes that happen to the desktop app, consider writing an automated test to ensure that the change or fix is maintained in the codebase. Depending on the nature of the change, you will write either a unit test or an E2E test.\nUnit tests The Jest test runner is used to run unit tests in the desktop app. You can run the following command to run the tests: npm run test:unit. You can also run subsets of the tests by filtering using testNamePattern or testPathPattern on the spec files.\nUnit tests are usually written for parts of the common and main modules, and usually cover individual functions or classes. We should endeavor to write our code such that it allows for simple testing, and any new features or bug fixes should likely have an associated unit test if possible. Check out [MM-40146][MM-40147] Unit tests for authManager and certificateManager #1874, which is an example of a unit test pull request (PR).\nIn order to ensure that most of the app is covered, we try to maintain 70% coverage of the common and main modules. You can view a coverage map by running this command: npm run test:coverage.\nE2E tests We use a combination of two technologies to facilitate E2E testing in the desktop app:\nPlaywright: A testing framework similar to Cypress or Selenium that acts as a Chromium driver for testing. It’s used to simulate interactions with the various web environments that make up the Desktop App, including the top bar (servers and tabs) and the individual Mattermost views. RobotJS: A multi-platform OS level automation framework written in NodeJS, used for simulating arbitrary keyboard and mouse inputs. It’s generally used to mock actions involving keyboard shortcuts and the Electron menu, as those are not web environments. To build the app and run the E2E tests, you can run the following command: npm run test:e2e. You can also run this command to build the tests without rebuilding the app: npm run test:e2e:nobuild. You can also run subsets of the tests by filtering using grep, for example: npm run test:e2e:run -- --grep back_button.\nE2E tests are usually written to cover parts of the renderer module and should generally cover complete workflows, such as creating and editing a server. You will generally need a combination of both Playwright and RobotJS APIs to test most workflows.\nAn example of an E2E test PR is [MM-39680] E2E Test for Deep Linking #1843.\nNotes There are many interactions (i.e. things that integrate with the operating system), such as notifications, that cannot be adequately tested using the automation frameworks we have. If this is the case, we will generally create a script to test in Rainforest, our crowd-sourced QA platform to perform these tests manually.\nCheck out the page on web app unit testing to see more of Jest in action. For other CLI commands related to testing, go to Build and CLI commands.\n","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/testing/","section":"contribute","subsection":"more info","tags":null,"title":"Unit and End-to-End (E2E) Tests"},{"categories":null,"contents":"If you need to add a new dependency to the project, it is important to add them in the right way. Instructions for adding different types of dependencies are described below.\nJavaScript only If you need to add a new JavaScript dependency that is not related to React Native, use npm, not yarn. Be sure to save the exact version number to avoid conflicts in the future.\n$ npm i --save-exact \u003cpackage-name\u003e If the dependency is only for development\n$ npm i --save-exact --save-dev \u003cpackage-name\u003e React Native As with JavaScript only, use npm to add your dependency and include an exact version.\nIf the library contains iOS native code, make sure to run:\n$ npm run pod-install Most of the time linking the library to React Native is done automatically, but at times some libraries need to be manually linked. In this case follow the library’s documentation.\n","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/developer-setup/dependecies/","section":"contribute","subsection":"more info","tags":null,"title":"Add new dependencies"},{"categories":null,"contents":"Mattermost publishes an unsigned build of the mobile app in the GitHub Releases page with every version that gets released.\nThese unsigned builds cannot be distributed nor installed directly on devices until they are properly signed.\nNote: Android and Apple require all apps to be digitally signed with a certificate before they can be installed. To avoid rebuilding the apps from scratch, you could just sign the unsigned builds published by Mattermost with your certificates and keys.\nSign Unsigned Android Sign Unsigned iOS ","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/unsigned/","section":"contribute","subsection":"more info","tags":null,"title":"Sign unsigned builds"},{"categories":null,"contents":"A core committer is a maintainer on the Mattermost project that has merge access to Mattermost repositories. They are responsible for reviewing pull requests, cultivating the Mattermost developer community, and guiding the technical vision of Mattermost.\n","permalink":"https://developers.mattermost.com/contribute/more-info/getting-started/core-committers/","section":"contribute","subsection":"more info","tags":null,"title":"Core committers"},{"categories":null,"contents":"In Redux, actions represent an operation either performed by the user or the server that cause a change to the state of the web app which is stored in the Redux store. It’s generally represented as a plain JavaScript object with a constant type string with other data stored in fields such as data.\n{ type: 'SELECT_CHANNEL', data: channelId, } Actions are created by functions called action creators. In regular Redux, this function will take some arguments and return an action representing how the store should be changed. Something to note with Mattermost Redux is that we typically refer to the action creators as the “actions” themselves since there’s often a single action creator for a given type of action.\nfunction selectChannel(channelId: string) { return { type: 'SELECT_CHANNEL', data: channelId, }; } This action is later received by the Redux store’s reducers which will know how to read the contents of the action and modify the store accordingly.\nBecause we use the Thunk middleware for Redux, we have the ability to use more powerful action creators that can read the state of the store, perform asynchronous actions like network requests, and dispatch multiple actions when needed. Instead of returning a plain object, these action creators return a function that takes the Redux store’s dispatch and getState to be able to dispatch actions as needed.\nfunction loadAndSelectChannel(channelId: string) { return async (dispatch: DispatchFunc, getState: GetStateFunc) =\u003e { const {channels} = getState().entities.channels; if (!channels.hasOwnProperty(channelId)) { // Optionally call another action to asynchronously load the channel over the network dispatch(setChannelLoading(true)); await dispatch(loadChannel(channelId)); dispatch(setChannelLoading(false)); } // Switch to the channel dispatch(selectChannel(channelId)); }; } Actions live in the src/actions directory with the constants that define their types being in the src/action_types directory.\nUse actions To use an action, you need to pass it into the dispatch method of the Redux store so that it can be passed off to the reducers.\nconst store = createReduxStore(); store.dispatch(loadAndSelectChannel(channelId)); Typically, you won’t have direct access to the store to get its dispatch method. Instead, you’ll receive it from either React Redux or Redux Thunk depending on what part of the code you’re working on.\nDispatch actions from a component React Redux provides two ways of accessing dispatch, and you’ll see both used throughout Mattermost.\nThe first is by its connect higher order component. Its second parameter mapDispatchToProps is used to wrap action creators so that they will automatically be dispatched when called.\n// src/components/widget/index.jsx import {connect} from 'react-redux'; import {loadAndSelectChannel} from 'src/actions/channels'; import Widget from './widget'; // mapDispatchToProps is an object containing all actions passed into the component const mapDispatchToProps = { loadAndSelectChannel, }; export default connect(null, mapDispatchToProps)(Widget); // src/components/widget/widget.tsx type Props = { channelId: string; // Notice that the type of the wrapped action omits the `getState` and `dispatch` parameters of the Thunk action loadAndSelectChannel: (channelId: string) =\u003e void; } export default function Widget(props: Props) { const handleClick = useCallback(() =\u003e { // We don't need to dispatch anything at this point props.loadAndSelectChannel(props.channelId); }, [props.loadAndSelectChannel, props.channelId]); return ( \u003cbutton onClick={handleClick}\u003e {'Click me!'} \u003c/button\u003e ); } Alternatively, you can use the useDispatch hook to dispatch actions directly in the component.\n// src/components/widget/widget.tsx import {useDispatch} from 'react-redux'; import {loadAndSelectChannel} from 'src/actions/channels'; type Props = { channelId: string; } export default function Widget(props: Props) { const dispatch = useDispatch(); const handleClick = useCallback(() =\u003e { dispatch(loadAndSelectChannel(props.channelId)); }, [dispatch, props.channelId]); return ( \u003cbutton onClick={handleClick}\u003e {'Click me!'} \u003c/button\u003e ); } The choice of which method to use is left up to the developer at the moment. connect is more widely used throughout the code base, but that’s primarily because hooks are relatively new compared to it. The class-based components that make up older parts of the app also aren’t compatible with hooks.\nWhen deciding which one to use though, try to match the area of the code that you’re working in. Individual components should never mix the two.\nAdd an action The steps for adding a new Redux action are as follows:\nDecide where to the action creator will be located. Depending on where the action will be located you will want to put it in one of the following locations:\nIf the action is more general and affects Redux state stored in state.entities, it should be put somewhere in webapp/channels/src/packages/mattermost-redux/src/actions. If the action is specific to the web app, affects state.views and will be used in multiple places throughout the app, it should be put in actions. If the action is very specific and will likely only be used by one or more closely related components, it should be put in an actions.ts located in the same directory as those components. If the action creator will have an effect on the Redux state that isn’t covered by existing action types, you’ll need to add a new “action type” constant that will be used by the action creator and will be handled by a reducer. These are located separate from the definition of the action creator itself to avoid having reducers import code from the action creators directly.\nDepending on where the action is located, the action creator will be located in one of the following:\nIf the action is located in mattermost-redux, the action type should be added to one of the files in webapp/channels/src/packages/mattermost-redux/src/action_types. If the action is specific to the web app or a single component, the action type should be added to the ActionTypes object in webapp/channels/src/utils/constants.tsx. export default keyMirror({ SOMETHING_HAPPENED: null }); Write the action creator itself. Depending on what data is needed by the action and if it needs to perform any async operations will change whether or not a Thunk action should be used. We should generally try to use plain Redux actions wherever possible since they’re a bit more complex, both to read and to process.\nfunction somethingHappened(channelId: string) { return { type: SOMETHING_HAPPENED, channelId, data: 1234, }; } function somethingAsyncHappened(channelId: string) { return async (dispatch: DispatchFunc, getState: GetStateFunc) =\u003e { const currentUserId = getCurrentUserId(getState()); let data; try { data = await Client4.doSomething(currentUserId, channelId); } catch (error) { dispatch({ type: SOMETHING_FAILED, channelId, error, }); return {error}; } // Note that if you need to access state again after waiting for something asynchronous, you should call // getState a second time to ensure you have an up to date version of the state dispatch({ type: SOMETHING_HAPPENED, channelId, data, }); }; } If you added a new action type, make sure to add or update existing reducers to handle the new action. More information about reducers is available here.\nAdd unit tests to make sure that the action has the intended effects on the store. More information on unit testing reducers is available on the page Redux Unit and E2E Testing.\nAdd a new API action If your action is corresponds to an API call, there are a few extra steps required but also a helper function to simplify the error handling for the action. The additional steps are as follows:\nEnsure that Client4, the JavaScript API client for Mattermost which is located in webapp/platform/client/src/client4.ts, has a method that corresponds to the API endpoint that you’re using. That method will likely involve simply constructing the URL for the endpoint, optionally constructing a body for the request, and then using the doFetch method to actually make the request.\nclass Client4 { doSomething = (userId: string, channelId: string) =\u003e { return this.doFetch\u003cSomethingResponse\u003e( `${this.getUserRoute(userId)}/something`, {method: 'post', body: JSON.stringify({channelId})}, ); } } Depending on your use case, you’ll likely want to dispatch a Redux action containing the response to the API request when it succeeds. You may optionally also want to dispatch actions when the request is made or fails to update the Redux state as the request progresses.\nexport default keyMirror({ SOMETHING_HAPPENED: null, // The following actions are optional. They used to be added for every API request, but we found we were only // rarely using their results, so we don't recommend adding them any more SOMETHING_REQUEST: null, SOMETHING_SUCCESS: null, SOMETHING_FAILURE: null, }); Most actions involving an API request follow a similar pattern of calling Client4 with the provided parameters, handling any errors that may occur, and dispatching an action containing the result if successful. The bindClientFunc helper can help with that.\nfunction somethingAsyncHappened(channelId: string) { return bindClientFunc({ clientFunc: client.doSomething, onSuccess: SOMETHING_HAPPENED, params: [channelId], }; } // clientFunc is the only mandatory parameter of bindClientFunc. The rest may be added as needed. function somethingVerboseHappened(userId: string, channelId: string) { return bindClientFunc({ clientFunc: client.doSomething, // The onRequest action will be dispatched before the request is made onRequest: SOMETHING_REQUEST, // The onSuccess action will be dispatched if the request succeeds. It will include a data parameter // containing the response to the request. Additionally, onSuccess can be an array of actions if multiple // should be dispatched when the request succeeds. onSuccess: [SOMETHING_SUCCESS, SOMETHING_HAPPENED], // The onFailure action will be dispatched if the request fails due to a network issue or an invalid request. // It will include an error parameter containing an Error object. onFailure: SOMETHING_FAILED, // An array of parameters will be passed into clientFunc in the order they're received params: [userId, channelId], }; } ","permalink":"https://developers.mattermost.com/contribute/more-info/webapp/redux/actions/","section":"contribute","subsection":"more info","tags":null,"title":"Actions"},{"categories":null,"contents":"This page describes how to build a new React component in the Mattermost web app. A new component must meet the following requirements:\nIs pure, meaning that all information required to render is passed in by props. Has no direct store interaction. Use connect to wrap the component if needed. Has component tests. Is generic and re-usable when possible. Has documented props. If none of those make any sense to you or you’re new to React and Redux, then check out these links:\nhttps://react.dev/learn/ http://redux.js.org/ These requirements are discussed in more detail in the following sections.\nDesign the component The most important part of designing your component is deciding on what the props will be. Props are very much the API for your component. Think of them as a contract between your component and the users.\nProps are read-only variables that get passed down to your component either directly from a parent component or from an index.ts container that connects the component to the Redux store.\nHow do you decide what props your component should have? Think about what your component is trying to display to the user. Any data you need to accomplish that should be part of the props.\nAs an example, let’s imagine we’re building an ItemList component with the purpose of displaying a list of items. The props for such a component might look like:\ntype Props = { /** * The title of the list */ title?: string; /** * An array of items to display */ items: ListItem[]; } The title prop is a string that’s displayed as the title of the list, and items is an array of objects that make up the contents of the list. Note that items is required while title is optional.\nMake sure you add brief but clear comments to each prop type as shown in the example.\nOur ItemList component would live in a file named item_list.tsx.\nUse a Redux container component The next question to ask yourself is whether you’re going to need a container component. This is the index.ts file mentioned above. If your component needs either of the following, then you’ll need a container:\nNeeds some data injected into its props that the parent component doesn’t have access to Needs to be able to perform some sort of action that affects the state of the store Continuing the ItemList example above, maybe our parent component doesn’t care about our list of items and doesn’t have access to them. Let’s also imagine that we want to let the user remove items from the list by clicking on them. This means our component now needs a container for both criteria above and our props will change slightly:\ntype Props = { /** * The title of the list */ title?: string; /** * An array of item components to display */ items: ListItem[]; actions: { /** * An action to remove an item from the list */ removeItem: (item: ListItem) =\u003e void; }; } Note that the type definition for actions passed from Redux won’t include the any reference to Redux’s dispatch or Redux Thunk’s getState. This is intentional as connect hides those details from the component.\nThe container will then handle getting data from the Redux state in mapStateToProps and passing Redux actions to the component using mapDispatchToProps. Note that either of these are optional if only one is needed.\nimport {connect} from 'react-redux'; import {bindActionCreators, Dispatch} from 'redux'; import {removeItem} from 'mattermost-redux/actions/items'; import {getItems} from 'mattermost-redux/selectors/entities/items'; import {GlobalState} from 'types/store'; import ItemList from './item_list'; function mapStateToProps(state: GlobalState) { return { items: getItems(state), }; } function mapDispatchToProps(dispatch: Dispatch) { return { actions: bindActionCreators({ removeItem, }, dispatch), }; } export default connect(mapStateToProps, mapDispatchToProps)(ItemList); If the selectors and/or actions you need don’t yet exist in Redux then you should go add those first by following the guide to adding actions and selectors.\nYour index.ts and item_list.ts files will live together in an item_list/ directory.\nImplement the component With the props defined and, if necessary, the container built, you’re ready to implement the rest of your component. For the most part, implementing a component for the web app is no different than building any other React component. While older code tends to use class components which extend React.PureComponent, most newer code should use functional components.\nOur ItemList example might look something like this:\ntype Props = { /** * The title of the list */ title?: string; /** * An array of item components to display */ items: ListItem[]; actions: { /** * An action to remove an item from the list */ removeItem: (item: ListItem) =\u003e void; }; } export default function ItemList(props: Props) { const title = this.props.title ? \u003ch1\u003e{this.props.title}\u003c/h1\u003e : null; const items = this.props.items.map((item: ListItem) =\u003e ( \u003cItem key={item.id} item={item} removeItem={this.props.actions.removeItem} /\u003e )); return ( \u003cdiv className='item-list'\u003e {title} {items} \u003c/div\u003e ); } To test your component, follow the guide here.\n","permalink":"https://developers.mattermost.com/contribute/more-info/webapp/build-component/","section":"contribute","subsection":"more info","tags":null,"title":"Build a component"},{"categories":null,"contents":"The REST API is a JSON web service that facilitates communication between Mattermost clients, as well as integrations, and the server. The server is currently on API version 4.\nReference Looking for the API reference? You can find it here: https://api.mattermost.com.\nAdd an endpoint To add an endpoint to API version 4, all of the following must be completed:\nReference Add an endpoint Document the endpoint Implement the API handler Update the Golang driver Write a unit test Submit your pull request (PR) Legacy Notes Document the endpoint At Mattermost, the OpenAPI specification is used for API documentation. The API documentation lives in the main Mattermost repository alongside the server: api.\nTo document an endpoint, follow these steps:\nFind the .yaml file in the api/v4/source directory that fits your endpoint.\nFor example, if you were adding the GET /users/{user_id} endpoint you would be looking for the users.yaml file. If the file doesn’t exist yet, you may need to create it and then update the Makefile to include the file. Copy an existing endpoint from the same or a different file.\nUpdate the documentation you copied with the correct information for your endpoint, including:\nTag - the resource type Summary - a summary of few words Description - a brief 1-2 sentence description Permissions - the permission(s) required Parameters - the URL and body parameters Responses - the success and error responses Confirm you don’t have any syntax errors by running make build within the api directory.\nContinue with the implementation of your API handler, updating this documentation as needed.\nImplement the API handler To implement the API handler, you’ll first need to setup your developer environment, and then follow these steps:\nAdd the declaration for your endpoint. For an example, check out the /api4/user.go file.\nImplement the handler for your endpoint. Follow this general pattern for handlers:\nfunc handlerName(c *Context, w http.ResponseWriter, r *http.Request) { // 1. Parse the request URL and body. // 2. Do a permissions check if required. // 3. Invoke handler logic through the app package. // 4. (Optional) Check the Etag. // 5. Format the response and write the response. } For examples, see the createUser() and the getUser() handlers.\nRun the server by runing make run-server within the server directory.\nUse curl or Postman to test the basics of your endpoint.\nUpdate the Golang driver The Go driver for APIv4 is in /model/client4.go. To add a function to support your new endpoint:\nCopy over an existing driver function, such as CreateUser.\nPaste the function into the section for your endpoint. For example, POST /teams would go in the Teams section.\nModify the function to correctly hit your endpoint. Make sure to update the request method to match your endpoint’s HTTP method.\nWrite a unit test The most important part of this process is to make sure the new endpoint works correctly. Follow these steps to write a unit test:\nOpen the test Go file related to your endpoint, or create one if necessary. For example, if you put your handler in /api4/user.go, your test will go in /api4/user_test.go.\nWrite your test based on the other tests in your file (or folder). There are several helper functions in /api4/apitestlib.go that you may use.\nEnsure that your test covers the following:\nAll combinations of correct inputs to your endpoint. Etags for your endpoint, if applicable. Incorrect URL or body parameters return a 400 Bad Request status code. Requests without a token return a 401 Unauthorized status code (for endpoints requiring a session). Requests with insufficient permissions return a 403 Forbidden status code (for endpoints requiring permission). Requests to non-existent resources or URLs return a 404 Not Found status code. Returning the correct error code might require investigation in the app or store to find the source of errors. Status codes on errors should be set at the creation of the error.\nSubmit your pull request (PR) Submit your pull request against the mattermost/mattermost repository by following these instructions.\nLegacy Notes The Mattermost API used to be defined in https://github.com/mattermost/mattermost-api-reference, but the source has since been moved to the mattermost/mattermost to streamline making code and documentation changes at the same time.\n","permalink":"https://developers.mattermost.com/contribute/more-info/server/rest-api/","section":"contribute","subsection":"more info","tags":null,"title":"REST API"},{"categories":null,"contents":"Our application uses the electron-log module to do most of our logging. It facilitates both file and console logging. For file logging, you can find the location of the log files by going to Help \u003e View Logs from within the application.\nOur app supports the following log levels: error, warn, info, verbose, debug, and silly.\nIn addition to the library, we provide a Logger object that simplifies and streamlines setting up logging for an individual module. To create a Logger object, simply create a new one:\nimport {Logger} from 'common/log'; const log = new Logger('MyModuleName'); You can then use the resulting log object to call any of the provided electron-log functions, and each log entry with be automatically prefixed with your module name.\n// Will print out \"[MyModuleName] a long entry\" log.debug('a log entry'); If you need to add additional prefixing, for example to log events on a specific object instance, we provide the withPrefix() method which allows you to add additional prefixes.\n// Will print out \"[MyModuleName] [some-id] a long entry\" const myObjectId = 'some-id'; log.withPrefix(myObjectId).debug('a log entry'); ","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/architecture/logging/","section":"contribute","subsection":"more info","tags":null,"title":"Logging"},{"categories":null,"contents":"As of 6.0, Mattermost CLI has been replaced by mmctl. mmctl is built to enable access to Mattermost server from the command line. The tool leverages the public API so that administrator and user tasks can be performed.\nSince mmctl uses the public API, an authorization mechanism is required. Which means the access rights are managed on the server side. There is a pre-run check to read credentials and use it in the client. In addition to authentication via credentials, mmctl can communicate to a local server without any authentication. This must be enabled via server configuration and both mmctl and mattermost/server needs to be running in the same machine.\nIn addition to provide more functionality towards testing and development, db subcommand has been added Mattermost server binary.\nThe CLI interface is written using Cobra, a powerful and modern CLI creation library. If you have never used Cobra before, it is well documented in its GitHub Repository.\nThe source code used to build our CLI interface is written in the commands directory of the mmctl repository.\nEach “command” of the CLI is stored in a different file of the commands directory. Within each file, you can find multiple “subcommands”.\nAdd a new subcommand If you want to add a new subcommand in an existing mattermost command, first find the relevant file. For example, if you want to add a show command to the channel command, go to commands/channel.go and add your subcommand there.\nTo add the subcommand, start by creating a new Command instance, for example:\nvar ChannelShowCmd = \u0026cobra.Command{ Use: \"show\", Short: \"Show channel info\", Long: \"Show channel information, including the name, header, purpose and the number of members.\", Example: \" channel show --team myteam --channel mychannel\" RunE: showChannelCmdF, } Then implement the subcommand function, in this example showChannelCmdF.\nfunc showChannelCmdF(c client.Client, cmd *cobra.Command, args []string) error { // Your code implementing the command itself newChannel, _, err := c.ShowChannel(channel) if err != nil { return err } return nil } Now, you set the flags of your subcommand and register it in the command. In our case we register our new ChannelShowCmd flag in ChannelCmd.\nfunc init() { ... ChannelShowCmd.Flags().String(\"team\", \"\", \"Team name or ID\") ChannelShowCmd.Flags().String(\"channel\", \"\", \"Channel name or ID\") ... ChannelCmd.AddCommand( ... ChannelShowCmd, ) ... } Finally, implement unit tests in commands/channel_test.go and end-to-end tests to commands/channel_e2e_test.go`.\nAdd a new command If you want to add a new command to mmctl, first create a file for the command. For example, if you want to add a new emoji command to manage emojis in Mattermost from the CLI, create commands/emoji.go and add your command and your subcommands there.\nA command is exactly the same as a subcommand, so you can follow the same steps of the previous section. However, you must also register the new command in the “Root” command as follows:\nvar EmojiCmd = \u0026cobra.Command{ Use: \"emoji\", Short: \"Emoji management\", Long: \"Lists, creates and deletes custom emoji\", } func init() { ... RootCmd.AddCommand(EmojiCmd) ... } Usually, you would then add several subcommands to perform various tasks.\nSubmit your pull request Please submit a pull request against the mattermost/mmctl repository by following these instructions.\n","permalink":"https://developers.mattermost.com/contribute/more-info/server/cli-commands/","section":"contribute","subsection":"more info","tags":null,"title":"CLI commands"},{"categories":null,"contents":"Add fields to the configuration In order to add fields to the configuration, you need to modify model/config.go in the server by adding the desired field to one of the structs such as ServiceSettings and setting its default value in the corresponding SetDefaults method.\nNote that some of the configuration values are collected as telemetries. The telemetry definitions are defined in the services/telemetry package. Once a configuration is added, it should be added to the telemetry package. If the configuration value is not going to be collected as a telemetry, a // telemetry: none comment must be added to prevent the configtelemetry check from failing.\nAlso we use struct tags to identify access level for configuration values. If the value requires a restriction, please use this tag accordingly.\nExpose settings in the System Console To expose the newly-added field in the System Console, you need to add that same setting to the AdminDefinition JS object in webapp/channels/src/components/admin_console/admin_definition.jsx. This object defines most of the settings in the System Console.\nMake settings available for non-admin users To make the newly added setting accessible to non-admin users in the apps, you’ll need to add it to the GenerateClientConfig method in config/client.go in the server. Note that this always encodes the setting as a string, so anywhere that you would want to use this value in the client, you have to look for a string.\n","permalink":"https://developers.mattermost.com/contribute/more-info/server/system_console/","section":"contribute","subsection":"more info","tags":null,"title":"System Console"},{"categories":null,"contents":"This page describes how to write and run End-to-End (E2E) testing for Mobile Apps for both iOS and Android. Mobile products use Detox, which is a “gray box end-to-end testing and automation library for mobile apps.” See its documentation to learn more.\nFile Structure The folder structure is as follows:\n|-- detox |-- e2e |-- support |-- test |-- config.json |-- environment.js |-- init.js |-- .babelrc |-- .detoxrc.json |-- package-lock.json |-- package.json /detox/e2e/support is the support folder, which is a place to put reusable behavior such as Server API and UI commands, or global overrides that should be available to all test files. /detox/e2e/test: To start writing tests, create a new file (e.g. login.e2e.js) in the /detox/e2e/test folder. The subfolder naming convention depends on the test grouping, which is usually based on the general functional area (e.g. /detox/e2e/test/messaging/ for “Messaging”). Test cases that require an Enterprise license should fall under /detox/e2e/test/enterprise/. This is to easily identify license requirements, both during local development and production testing for Enterprise features. /detox/.detoxrc.json: for Detox configuration. /detox/package.json : for all dependencies related to Detox end-to-end testing. Writing an E2E Test This process has many similarities to writing an E2E test for the mattermost-webapp project. Before writing a script, ensure that it has a corresponding test case in Zephyr. All test cases may be found in this link. If test case is not available, feel free to prompt the QA team who will either search from an existing Zephyr entry or if it’s a new one, it will be created for you.\nCreate a test file based on the file structure aforementioned above.\nInclude Zephyr identification (ID) and title in the test description, following the format of it('[Zephyr_id] [title]') or it('[Zephyr_id]_[step] [title]') if the test case has multiple steps. For test case “MM-T109 RN apps: User can't send the same message repeatedly”, it should be:\ndescribe('Messaging', () =\u003e { it('MM-T109 User can\\'t send the same message repeatedly', () =\u003e { // Test steps and assertion here } } Target an element using available matchers. For best results, it is recommended to match elements by unique identifiers using testID. The identifier should follow the following format to avoid duplication, \u003clocation\u003e.\u003cmodifier\u003e.\u003celement\u003e.\u003cidentifier\u003e, where: NOTE: Not all fields are required. When assigning a testID, carefully inspect the actual render structure and pick up the minimum fields combination to create a unique value. Some examples include: send.button and post.\u003cpost-id\u003e. location - can be a parent component, a main section, or a UI screen. modifier - adds meaning to the element. element - common terms like button, text_input, image, and the like. identifier - could be unique ID of a post, channel, team or user, or a number to represent order. Prefix each comment line with appropriate indicator. Each line in a multi-line comment should be prefixed accordingly. Separate and group test step comments and assertion comments for better readability.\n# indicates a test step (e.g. // # Go to a screen) * indicates an assertion (e.g. // * Check the title) Simulate user interaction using available actions, and verify user interface (UI) expectations using expect. When using action, match, or another API specific to a particular platform, verify that the equivalent logic is applied so that the API does not impact other platforms. Always run tests in applicable platforms.\nRunning E2E Tests Testing Android Locally Local setup Install the latest Android SDK:\nsdkmanager \"system-images;android-30;google_apis;x86\" sdkmanager --licenses Create the emulator using npm run e2e:android-create-emulator from the /detox folder. Android testing requires an emulator named detox_pixel_4_xl_api_30 and the script helps to create it automatically.\nComplete a test run in debug mode This is the typical flow for local development and test writing:\nOpen a terminal window and run react-native packager by npm install \u0026\u0026 npm start from the root folder. Open a second terminal window and: Change directory to /detox folder. Install npm packages by npm install. Build the app together with the android test using npm run e2e:android-build. Run the test using npm run e2e:android-test. For running a single test, follow this example command: npm run e2e:android-test -- connect_to_server.e2e.ts. Complete a test run in release mode This is the typical flow for CI test run:\nBuild a release app by running npm install \u0026\u0026 npm run e2e:android-build-release from the /detox folder. Run a test using npm run e2e:android-test-release from the /detox folder. Testing iOS Locally Local setup Install applesimutils: brew tap wix/brew brew install applesimutils Set XCode’s build location so that the built app, especially debug, is expected at the project’s location instead of the Library’s folder which is unique/hashed. Open XCode, then go to XCode \u003e Settings \u003e Locations. Under Derived Data, click Advanced…. Select Custom \u003e Relative to Workspace, then set Products as Build/Products. Click Done to save the changes. Complete a test run in debug mode In one terminal window, from the root folder run npm run start and on another npm run ios from the root folder. Once the build is complete and installed, there will be a message informing of the path where the app is installed. Copy that path. Sample output: info Building (using \"xcodebuild -workspace Mattermost.xcworkspace -configuration Debug -scheme Mattermost -destination id=00008110-00040CA10263801E\") info Installing \"/Users/myuser/Proyectos/mattermost-mobile/ios/Build/Products/Debug-iphonesimulator/Mattermost.app info Launching \"com.mattermost.rnbeta\" success Successfully launched the app Edit /detox/.detoxrc and In apps \u003e ios.debug, substitute binaryPath by that value. In devices \u003e ios.simulator \u003e device, substitute type and os with the values corresponding to the ones being used by the simulator. If unsure wich ones, either open the simulator or go to Xcode to see where it is being built. Export the values for ADMIN_USERNAME and ADMIN_PASSWORD with the appropiate values for your test server. Check the Environment Variables section below to learn more about default values and other variables available. export SITE_1_URL=\"http://localhost:8065\" export ADMIN_USERNAME=\"sysadmin\" export ADMIN_PASSWORD=\"Sys@dmin-sample1\" In another terminal window, run npm i then npm run e2e:ios-test from the /detox folder. For running a single test, follow this example command: npm run e2e:ios-test -- connect_to_server.e2e.ts. cd detox npm i npm run e2e:ios-test Complete a test run in release mode Build the release app by running npm run build:ios-sim from the root folder or npm run e2e:ios-build-release from within the /detox folder. Run the test using npm run e2e:ios-test-release from the /detox folder. Environment variables Test configurations are defined at test_config.js and environment variables are used to override default values. In most cases you don’t need to change the values, because it makes use of the default local developer setup. If you do need to make changes, you may override by exporting, e.g. export SITE_URL=\u003csite_url\u003e.\nVariable Description SITE_URL Host of test server.\nDefault: http://localhost:8065 for iOS or http://10.0.2.2:8065 for Android. ADMIN_USERNAME Admin’s username for the test server.\nDefault: sysadmin when server is seeded by make test-data. ADMIN_PASSWORD Admin’s password for the test server.\nDefault: Sys@dmin-sample1 when server is seeded by make test-data. LDAP_SERVER Host of LDAP server.\nDefault: localhost LDAP_PORT Port of LDAP server.\nDefault: 389 ","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/e2e/","section":"contribute","subsection":"more info","tags":null,"title":"Mobile End-to-End (E2E) Tests"},{"categories":null,"contents":"The Mattermost server uses Go modules to manage dependencies.\nAdd or update a new dependency Adding a dependency is easy. All you have to do is import the dependency in the code and recompile. The dependency will be automatically added for you. Updating uses the same procedure.\nBefore committing the code with your new dependency added, be sure to run go mod tidy to maintain a consistent format and go mod vendor to synchronize the vendor directory.\nIf you want to add or update to a specific version of a dependency you can use a command of the form:\ngo get -u github.com/pkg/errors@v0.8.1 go mod tidy go mod vendor If you just want whatever the latest version is, you can leave off the @version tag.\nRemove a dependency Be sure you have enabled go modules support. After removing all references to the dependency in the code, you run:\ngo mod tidy go mod vendor to remove it from the go.mod file and the vendor directory.\n","permalink":"https://developers.mattermost.com/contribute/more-info/server/dependencies/","section":"contribute","subsection":"more info","tags":null,"title":"Dependencies"},{"categories":null,"contents":"Unit Tests for Component and Utility Files The last required step in building a web app component is to test it. Jest and React Testing Library are the main frameworks/testing utilities used in unit testing components and utility files in our web app. Please visit their respective documentation for detailed information on how to get started, best practices, and updates. React Testing Library is used to render and interact with React components in a way that closely mirrors how users interact with them in the browser, while Jest is used to perform snapshot testing against these components.\nWith React Testing Library, you can focus on testing the behavior of your components, rather than their implementation details. This means that your tests will be more resilient to changes in your codebase, and will give you more confidence that your application is working as expected. If you need to unit test something related to Redux, please check out Redux Unit and E2E Testing.\nWriting unit tests Below is a brief guide on how to do component testing:\nUse our testing library helpers to render a component and its child components. Use screen to interact with the rendered component and assert on the expected results. Match snapshots using default or expected props. Note that while using snapshots is convenient, do not rely solely on this for every test case, as changes can be easily overlooked when using the command jest --updateSnapshot to update multiple snapshots at once. For example:\nimport {render, screen, userEvent} from 'tests/react_testing_utils'; const baseProps = { active: true, onSubmit: jest.fn(), }; test('should match snapshot, not send email notifications', () =\u003e { // For components that use Redux, React Intl, or Reach Router, you can also use the renderWithFullContext helper const {container} = render(\u003cEmailNotificationSetting {...baseProps}/\u003e); expect(container.firstChild).toMatchSnapshot(); // Assert on a specific element rendered by the component const submitButton = screen.getByRole('button', { name: 'Submit' }); expect(submitButton).toBeInTheDocument(); }); Use screen from React Testing Library to query for elements by text or role and use assertions to verify their existence and properties.\nexpect(screen.getByRole('checkbox', { id: 'emailNotificationImmediately' })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: props.siteName })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: props.customDescriptionText })).toBeInTheDocument(); Use toHaveClass matcher from Jest DOM to check CSS classes.\nexpect(screen.getByTestId('create_post')).toHaveClass('center'); Simulate events using userEvent and verify state changes using expect.\ntest('should pass handleChange', () =\u003e { const {getByRole} = render(\u003cEmailNotificationSetting {...baseProps}/\u003e); const emailNotificationImmediately = getByRole('checkbox', { id: 'emailNotificationImmediately' }); userEvent.click(emailNotificationImmediately); expect(emailNotificationImmediately.checked).toBe(true); expect(screen.getByTestId('emailInterval').value).toBe('30'); }); Ensure that all functions of a component are tested. This can be done via events, state changes, or just calling it directly.\nconst baseProps = { updateSection: jest.fn(), }; test('should call updateSection on handleExpand', () =\u003e { const {getByRole} = render(\u003cEmailNotificationSetting {...baseProps}/\u003e); const button = getByRole('button', { name: /expand/i }); userEvent.click(button); expect(baseProps.updateSection).toBeCalled(); expect(baseProps.updateSection).toHaveBeenCalledTimes(1); expect(baseProps.updateSection).toBeCalledWith('email'); }); When a function is passed to a component via props, make sure to test if it gets called for a particular event call or its state changes.\nconst baseProps = { onSubmit: jest.fn(), updateSection: jest.fn(), }; test('should call functions on handleSubmit', () =\u003e { const {getByTestId} = render(\u003cEmailNotificationSetting {...baseProps}/\u003e); const submitButton = getByTestId('submit-button'); userEvent.click(submitButton); expect(baseProps.onSubmit).not.toBeCalled(); expect(baseProps.updateSection).toHaveBeenCalledTimes(1); expect(baseProps.updateSection).toBeCalledWith('email'); const emailNotificationNever = getByTestId('email-notification-never'); userEvent.change(emailNotificationNever); userEvent.click(submitButton); expect(baseProps.onSubmit).toBeCalled(); expect(baseProps.onSubmit).toHaveBeenCalledTimes(1); expect(baseProps.onSubmit).toBeCalledWith({enableEmail: 'false'}); expect(baseProps.updateSection).toHaveBeenCalledTimes(2); expect(baseProps.updateSection).toBeCalledWith(''); }); Provide a mock for a single function imported from another file while keeping the original version of the rest of that file’s exports.\njest.mock('utils/utils', () =\u003e { const original = jest.requireActual('utils/utils'); return { ...original, isMobileView: jest.fn(() =\u003e true), }; }); Mock async redux actions as necessary while providing a readable action type and having them pass their arguments.\njest.mock('mattermost-redux/actions/channels', () =\u003e { const original = jest.requireActual('mattermost-redux/actions/channels'); return { ...original, fetchMyChannelsAndMembers: (...args) =\u003e ({type: 'MOCK_FETCH_CHANNELS_AND_MEMBERS', args}), }; }); // Then compare the dispatched actions await testStore.dispatch(Actions.loadChannelsForCurrentUser()); expect(testStore.getActions()).toEqual(expectedActions); For utility functions, list all test cases with test description, input and output.\ndescribe('stripMarkdown | RemoveMarkdown', () =\u003e { const testCases = [{ description: 'emoji: same', inputText: 'Hey 😄 👍 :)', outputText: 'Hey 😄 👍 :)', }, { description: 'at-mention: same', inputText: 'Hey @user and @test', outputText: 'Hey @user and @test', }]; testCases.forEach((testCase) =\u003e { test(testCase.description, () =\u003e { expect(stripMarkdown(testCase.inputText)).toEqual(testCase.outputText); }); }); } Running unit tests To run all tests that have been modified or added since the last commit, run Jest in watch mode with the command: npm run test:watch. If there are new tests in the first run, test snapshots will be generated and placed in the __snapshots__ directory in the folder where the test suite is located.\nIn watch mode, you can also run specific sets of tests:\n› Press a to run all tests. › Press f to run only failed tests. › Press o to only run tests related to changed files. › Press q to quit watch mode. › Press p to filter by a filename regex pattern. › Press t to filter by a test name regex pattern. › Press Enter to trigger a test run. To see if a component test passes, select p in watch mode and enter the filename for the component test. Troubleshooting If you get an error similar to UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'filter' of undefined:\nCheck if the code being tested uses native timer functions (i.e., setTimeout, setInterval, clearTimeout, clearInterval). You can mock the timers and/or run fake timers (e.g. jest.useFakeTimers()) if necessary. Note that jest.useFakeTimers() is already used in the Jest global setup, but there are cases where it needs to run specifically depending on how the component uses the native timer functions. If you get an error similar to UnhandledPromiseRejectionWarning: TypeError: (0 , \\_fff.hhh) is not a function:\nCheck if you’re mocking part of an imported module without providing other exports which are used. You can use jest.requireActual to get the un-mocked version of the file.\n// DO NOT partially mock the module jest.mock('actions/storage', () =\u003e ({ setGlobalItem: (...args) =\u003e ({type: 'MOCK_SET_GLOBAL_ITEM', args}), })); // DO fully mock the module jest.mock('actions/storage', () =\u003e { const original = jest.requireActual('actions/storage'); return { ...original, setGlobalItem: (...args) =\u003e ({type: 'MOCK_SET_GLOBAL_ITEM', args}), }; }); If you get an error similar to UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'data' of undefined:\nUse async mock functions with resolved values. The property in question that cannot be read can be error, data, exists, match, or whatever else the resolved value(s) contains. For example, if the file being tested contains a line where it awaits on an async value like:\nconst {data} = await this.props.actions.addUsersToTeam(this.props.currentTeamId, userIds); The mocked function should return a Promise by using mockResolvedValue:\n// DO NOT assign a regular mock function. const addUsersToTeam = jest.fn(); // DO NOT forget to provide a resolved value. const addUsersToTeam: jest.fn(() =\u003e { return new Promise((resolve) =\u003e { process.nextTick(() =\u003e resolve()); }); }), // DO mock async function with resolved value. `mockResolvedValue` is the easiest way to do this. const addUsersToTeam = jest.fn().mockResolvedValue({data: true}) // DO mock async function with several resolved values for repeated calls. const addUsersToTeam = jest.fn(). mockResolvedValueOnce({error: true}). mockResolvedValue({data: true}); Remember to make individual test cases async when testing async functions:\n// DO NOT forget to wait for the async function to complete. test('should match state when handleSubmit is called', () =\u003e { wrapper.instance().handleSubmit(); expect(...) }); // DO remember to wait on the async function and to make the entire test case async. test('should match state when handleSubmit is called', async () =\u003e { await wrapper.instance().handleSubmit(); expect(addUsersToTeam).toHaveBeenCalledTimes(1); }); ","permalink":"https://developers.mattermost.com/contribute/more-info/webapp/unit-testing/","section":"contribute","subsection":"more info","tags":null,"title":"Unit tests"},{"categories":null,"contents":"All changes to the product should be reviewed. Every team will have its own workflow, but in general:\nUser experience changes should be reviewed by a product manager or designer. Code changes should be reviewed by at least two core committers. Documentation changes should be reviewed by a product manager or technical writer. Staff should consult the internal organization chart as needed when finding the right reviewer.\nIf you are a community member seeking a review Submit your pull request. Follow the contribution checklist. Wait for a reviewer to be assigned. Product managers are on the lookout for new pull requests and usually handle this for you automatically. If you have been working alongside a core committer, feel free to message them for help. When in doubt, ask for help in the Developers channel on our community server. If you are still stuck, message Jason Blais (@jasonblais on GitHub) or Jason Frerich (@jfrerich on GitHub). Wait for a review. Expect some interaction with at least one reviewer within 5 business days. (Business days are typically Monday to Friday, excluding statutory holidays.) Keep in mind that core committers are geographically distributed around the world and likely in a different time zone than your own. If no interaction has occurred after 5 business days, at-mention a reviewer with a comment on your PR. Make any necessary changes. If a reviewer requests changes, your pull request will disappear from their queue of reviews. Once you’ve addressed the concerns, re-request a review from any reviewer requesting changes. Avoid force pushing. Wait for your code to be merged. Larger pull requests may require more time to review. Once all reviewers have approved your changes, they will handle merging your code. If you are a core committer seeking a review Submit your pull request. Follow the contribution checklist. Immediately add the 1: UX Review, 2: Dev Review, and 3: QA Review labels. Your pull request should not be merged until these labels are later removed in the review process. Apply additional labels as necessary: CherryPick/Approved: Apply this if the pull request is meant for a quality or patch release. Do Not Merge/Awaiting PR: Apply this if the pull request depends on another (e.g. server changes) Setup Test Server: Apply this to create a test server with your changes for review. See labels for additional documentation. Assign a milestone as necessary. Most pull requests do not require a milestone, but will simply ship once merged. Some reviewers may prioritize reviews for known upcoming milestones. The milestone is mandatory for bug fixes that must be cherry-picked. Request a review from a Product Manager and/or a Designer. The choice of Product Manager or Designer is up to you. In most cases, choose the individual embedded with your team. If your change primarily touches another team’s codebase, assign the individual from that team. If in doubt, assign any Product Manager or Designer and comment about the uncertainty. They may reassign as appropriate. Wait for their review to complete before continuing so as to avoid churn if changes are requested. Remove the 1: UX Review label only when these reviews are done and they accept the changes. Product Managers and Designers ensure the changes meet user experience guidelines. If your changes do not affect the user experience, you may remove 1: UX Review immediately. After UX review, request a review from two core committers. The choice of core committers is up to you. When picking your first core committer, consider someone with domain expertise relative to your changes. Sometimes GitHub will recommend a recent editor of the code, but often you must rely on your own intuition from past interactions. When picking your second core committer, consider someone who may have expertise in the language you’re using or the problem you’re solving, even if they aren’t intricately familiar with the codebase. This can provide a fresh set of eyes on the code to reveal blindspots that are not biased by hitting deadlines, and helps expose the team to new parts of the code to help spread out domain knowledge. This may not make sense for every pull request but is a practice to keep in mind. If you don’t have someone specific in mind, consider using the @core-reviewers group to assign an available reviewer automatically. Don’t be afraid to pick someone who gives “hard” reviews. Code review feedback is never a personal attack: it should “sharpen” the skills of both the author and the reviewers, not to mention improving the quality of the product. Try to avoid assigning the same person to all of your reviews unless they are related. When in doubt, ask for recommendations on our community server. Wait for their review to complete before continuing so as to avoid churn if changes are requested. Remove the 2: Dev Review label only when these reviews are done and they accept the changes. After Dev review, assign a QA tester. Ensure that your PR includes test steps or expected results for QA reference if the QA Test Steps in the Jira ticket have not already been filled in. The choice of QA tester is up to you. In most cases, choose the individual embedded with your team. If your change primarily touches another team’s codebase, assign the individual from that team. If in doubt, assign any QA tester and comment about the uncertainty. They may reassign as appropriate. Wait for their review to complete before continuing. It is the QA tester’s responsibility to determine the scope of required testing, if any. Remove the 3: QA Review label only when their review is done and they accept the changes. In the rare event your changes do not require QA review, you may remove 3: QA Review immediately. Comment on the pull request clearly explaining the rationale. Merge the pull request. Do not merge until the labels 1: UX Review, 2: Dev Review and 3: QA Review labels have been removed. Add the 4: Reviews Complete label if the last reviewer did not already add it. Do not merge if there are outstanding changes requested. Merge your pull request and delete the branch if not from a fork. Note that any core committer is free to merge on your behalf. If your pull request depends on other pull requests, consider assigning the Do Not Merge/Awaiting PR label to avoid merging prematurely. Handle any cherry-picks. There is an automated cherry-pick process. As the author of the pull request, you should make sure the cherry-pick succeeds. Check here for details. After a pull request is merged (and cherry-picked where needed), update the Jira ticket. Resolve the ticket for QA from “Ready for QA” button with QA test steps (or “No Testing Required” if no QA testing is needed). If you are awaiting a review Wait patiently for reviews to complete. Expect some interaction with each of your reviewers within 2 business days. There is no need to explicitly mention them on the pull request or to explicitly reach out on our community server. Core committers and QA testers are expected to have the GitHub plugin installed to automate notifications and to trigger a daily review of their outstanding requested reviews. Make any necessary changes. If a reviewer requests changes, your pull request will disappear from their queue of reviews. Once you’ve addressed the concerns, assign them as a reviewer again to put your pull request back in their queue. If you are a core committer asked to give a review Respond promptly to requested reviews. Assume the requested review is urgent and blocking unless explicitly stated otherwise. Try to interact with the author within 2 business days. Configure the GitHub plugin to automate notifications. Review your outstanding requested reviews daily to avoid blocking authors. Prioritize earlier milestones when reviewing to help with the release process. Responding quickly doesn’t necessarily mean reviewing quickly! Just don’t leave the author hanging. Feel free to clarify expectations with the author. If the PR adds a substantial feature, check that a feature flag is included. Please see criteria here. If the code is experimental, they may need only a cursory glance and thumbs up to proceed with productizing their changes. If the review is large or complex, additional time may be required to complete your review. Be upfront with the author. If you are not comfortable reviewing the code, avoid “rubber stamping” the review. Be honest with the author and ask them to consider another core committer. Never rush a review. Take the time necessary to review the code thoroughly. Don’t be afraid to ask for changes repeatedly until all concerns are addressed. Feel free to challenge assumptions and timelines. Rushing a change into a patch release may cause more harm than good. Avoid leaving a review hanging. Try to accept or reject the review instead of just leaving comments. If you are the last developer to approve the changes, consider requesting a review from the appropriate QA tester to speed up the process. Run the E2E tests against the PR code After the PR has been reviewed, the reviewer should trigger an E2E test run on it by using one of the available mechanisms. The E2E Test result will be posted back to the PR as a comment. It’s the PR author’s responsibility to address the E2E test failures, with the assistance of the reviewers. Core committers can refer to this Confluence page for additional informations on E2E testing and related processes. Merge the pull request. Do not merge until the labels 1: UX Review, 2: Dev Review and 3: QA Review labels have been removed. Add the 4: Reviews Complete label if the last reviewer did not already add it. Do not merge if there are outstanding changes requested. Do not merge if there are any Do Not Merge labels applied. When in doubt, leave the merging of the pull request to the author. Merge the pull request, and delete the branch if not from a fork. Handle any cherry-picks. There is an automated cherry-pick process and the author of the pull request should make sure the cherry-pick succeeds. Assume this is the case unless you are explicitly asked to help cherry-pick. ","permalink":"https://developers.mattermost.com/contribute/more-info/getting-started/code-review/","section":"contribute","subsection":"more info","tags":null,"title":"Code review"},{"categories":null,"contents":"Mattermost supports plugins that offer powerful features for extending and deeply integrating with both the server and web/desktop apps.\nThis document covers the plugin infrastructure and how to contribute to it.\nBuild plugins Looking to build a plugin? Then you want the plugin author documentation.\nOverview Plugins are generally made of at least two parts: a manifest and a server binary and/or a JavaScript bundle.\nThe manifest tells Mattermost what the plugin is and provides a set of metadata used by the server to install and run the plugin. Please see the manifest reference for more information. Manifests may be defined in JSON or YAML.\nThe server binary is a compiled Go program that extends the MattermostPlugin struct of the plugin package. When enabled, the plugin’s server binary is started as a process by the Mattermost server. Plugin developers then have access to interact with the Mattermost server over RPC through the plugin API and Hooks. The server-side of plugins is built using the go-plugin library from Hashicorp. More information is available in the server side of the plugin author documentation.\nThe JavaScript bundle is a webpack-built collection of JavaScript code that will be run on the Mattermost web/desktop apps. When a plugin is enabled, the client is notified and it makes a request to add the JS bundle to the document. The plugin’s client code then registers itself and its components with the Mattermost client through the client’s plugin registry. The registry contains many methods for registering different components and callbacks. These are all stored as part of the app’s plugin reducer. The Pluggable component is then inserted into various places in the app, allowing plugins to insert components into these locations in the UI. In some special cases, the Pluggable component is not used and we instead implement the plugs manually. More information is available in the webapp side of the plugin author documentation.\nAll these different components of a plugin are compressed into a .tar.gz bundle. Installing a plugin is the process of uploading this bundle to the Mattermost server (via the UI, REST API or CLI). The server then unpacks the bundle, performs some validation and extracts it into the configured directory for storing installed plugins. Installed plugins are not yet running. To start a plugin it must be enabled (again via the UI, REST API or CLI). Once it is enabled, the server will then start the server process and prepare the web app bundle for serving to the client. Plugin settings, configuration and enabled/disabled status are managed by the Mattermost config.json using a PluginSettings struct.\nCheck out the `plugin` package and the plugin_* files in the `app` package for the code, and mattermost-plugin-demo for an example plugin. To start developing your own plugin, please follow the instructions here.\nAdd an API To add a plugin API you need to add the signature of your new method to the API interface. You then need to implement the API in the plugin_api.go of the app package. Finally, you need to run make pluginapi to generate the RPC glue code needed for your new API and make plugin-mocks to generate the mocks used for plugin testing.\nThat’s it! Submit your pull request.\nQuestions? If you have any questions, feel free to ask in the Toolkit channel of our Mattermost community instance.\n","permalink":"https://developers.mattermost.com/contribute/more-info/server/plugins/","section":"contribute","subsection":"more info","tags":null,"title":"Plugins"},{"categories":null,"contents":"Reducers in Redux are pure functions that describe how the data in the store changes after any given action. A reducer receives the previous state of the store and an action as a JavaScript object (see here for more information on actions) and should output the resulting state without receiving any outside data. Because reducers are pure, they will always produce the same resulting state for a given state and action.\nconst myValueDefault = ''; function myValue(state = myValueDefault, action) { switch(action.type) { case SET_MY_VALUE: // This data changes myValue, so just return the new value return action.data; default: // This action doesn't affect us, so return the previous value return state; } } Most reducers used by Mattermost Redux are simple like the one above in that they only affect a single part of the store. These can be easily composed using Redux’s `combineReducers` function to make a more complex data store.\nfunction myStringValue(state = '', action) { ... } function myNumberValue(state = 0, action) { ... } function myOtherValue(state = {}, action) { ... } // This combines the reducers so that the resulting store will look like // { // myStringValue: 'abc', // myNumberValue: '1234', // myOtherValue: {color: 'red', weather: 'rain'} // } export const combineReducers({ myStringValue, myNumberValue, myOtherValue }); combineReducers can be nested further to make up the complex data store as used by Mattermost Redux.\nAvoiding mutating the store One of the core principals of Redux is that the state of the store should never be modified. If the state changes, a completely new state tree should be returned. That’s not to say the entire thing is destroyed and recreated from scratch any time anything changes, but only the parts that are modified are recreated.\nFor example, if the store holds a state like:\nentities channels channels users currentUserId profiles requests getUser getChannel and we make a request to get the profile for a user that we don’t have, the following fields would be changed (shown in bold):\nentities channels channels users currentUserId profiles requests getUser getChannel To do this, it is important to always return new objects from a reducer if any of its contents change. This is trivial for reducers for state that is a primitive string or number, but it can be more complicated for other types of data. You should also make sure to return the same object if nothing needs to change. As long as you do that, combineReducers will make sure to update the rest of the state tree accordingly.\nfunction myOtherValue(state = {color: 'red', weather: 'rain'}, action) { switch(action.type) { case SET_OTHER_VALUE_COLOR: // This destructuring syntax is used to create a new object that is a shallow copy of state // with the color field updated to the new value return { ...state, color: action.data }; case SET_OTHER_VALUE_WEATHER: return { ...state, weather: action.data }; default: // There's no changes, so return the previous value return state; } ... } If you accidentally mutate the state, you’ll receive an error when Mattermost Redux is running in development mode.\n","permalink":"https://developers.mattermost.com/contribute/more-info/webapp/redux/reducers/","section":"contribute","subsection":"more info","tags":null,"title":"Reducers"},{"categories":null,"contents":"At Mattermost, we write tests because we want to be confident that our product works as expected for our users. As developers, we write tests as a gift to our future selves or to be confident that changes won’t cause regressions or unintended behaviors. We value contributors who write tests as much as any others, and we want the process to be integrated in our core development workflow rather than being an afterthought or follow-up action.\nThis page stresses the importance of tests, including for every pull request being submitted. It is the foundation of our test guidelines, and serves as a reference on why we do not merge code without tests. This is not to meet higher code coverage but rather to write effective and well-planned use cases depending on the changes being made. But of course, there’s always an exception. If, for some reason, it isn’t possible to write a test, let the reviewers know by writing a description to start a discussion and to fully understand the situation you are facing.\nTest categories Not all test types are required in a single pull request. Only write whichever test types are most effective and appropriate.\nUnit tests - Unit tests verify that individual, isolated parts work as expected. Integration tests - Integration tests verify that several units work together in harmony. End-to-End (E2E) tests - End-to-End tests exercise most of the parts of a large application. Note: Wisdom and definitions mostly taken from Martin Fowler's Software Testing Guide and Kent C. Dodds's personal site. In general, when are tests necessary? For all files written in the main language(s) of the repository; this could be JavaScript/Typescript and JSX/TSX files, Go files, or both which are exported such as functions, modules, or components used in various places. Un-exported functions or methods, which have low or no test coverage from the parent exported function/method, that affect critical functionality or behavior of the application. New features and bug fixes, especially those originating from customer and community bugs. When is it fine not to have tests? For implementation details of standard libraries or external packages that are implicitly covered by the standard library or external package itself. Where the situation may require external services running to effectively test the functionality, such as dependencies on feature flags via Split, OAuth with third-party providers such as Google, etc. Tests should be made at the most effective and lowest possible level, but if it requires too much effort or complicated setup to accomplish at a unit test level, it would be best to skip and assess feasibility on the next level such as integration or end-to-end testing. Mocks and test helpers. Types only. Interfaces only or interfaces to other repositories, such as with private Enterprise via “interfaces”. End-to-end tests codebase. Automatically generated code for database migrations, store layers, etc. External dependencies, modules, imports, or vendors. How to run and write tests Server For writing and running unit tests in general, see the Server workflow page. If you have written a new endpoint or changed an endpoint for the Mattermost REST API, check out the REST API page.\nWeb App For writing and running unit tests in general, see the Unit tests page. For writing and running E2E tests in general, see the End-to-End testing section, and the End-to-End cheatsheets section.\nFor writing and running E2E (and unit) tests for Redux components, see the Redux Unit and E2E Testing page.\nMobile Apps For writing and running E2E tests for both Android and iOS systems, take a look at the Mobile End-to-End (E2E) Tests page.\nDesktop App For writing and running unit and E2E tests for the desktop app, check out the Unit and End-to-End (E2E) Tests in the desktop app page.\nHow to Contribute E2E Tests If you’re looking to improve your development skills or improve your familiarity with the Mattermost code base, issues for E2E tests that are marked Help Wanted are a great place to start.\nLook for issues in the mattermost repository that have the Help Wanted label and either the Area/E2E Tests label or something related to E2E in the issue title. Once you find an issue you would like to work on, comment on the issue to claim it. Each issue is filled with specific test steps and verifications that need to be accomplished as a minimum requirement. Additional steps and assertions for robust test implementation are very welcome. The contents of an E2E issue follow this general format: Steps: What the code in the test should do and/or emulate. Expected: What the results of the test should be. Test Folder: Where the file that holds the test code should be located. Test code arrangement: Starter code for the test. Notes: Comments on what to add and not to add to the test file, plus resources for contributions, asking questions, etc. If you’d like to see an example of an ideal E2E test contribution, please view these issues and their associated PRs:\nWrite Webapp E2E with Cypress: “MM-T642 Attachment does not collapse” Cypress test: “CTRL/CMD+K - Open private channel using arrow keys and Enter” ","permalink":"https://developers.mattermost.com/contribute/more-info/getting-started/test-guideline/","section":"contribute","subsection":"more info","tags":null,"title":"Test guidelines"},{"categories":null,"contents":"Storybook has been added to the mobile repository to help prototype components. To use Storybook:\nIn the root of the repository, run npm run storybook. This step automatically scans and loads all stories, then opens a new browser tab with the Storybook interface.\nNote: When using a real device, you may need to configure the Storybook Host URL by updating the .env file in the root of the repository. When running in an emulator, the code tries to use the default network values.\nRun the usual npm run android (or npm run ios) and npm start commands.\nStorybook has been integrated into the react-native dev menu.\nOn Mac OS, press CMD+D to open the dev menu when your app is running in an iOS Simulator, or press CMD+M when running in an Android emulator. On Windows and Linux, press CTRL+M to open the dev menu, then select the “Storybook” option. If running on a real device, shaking the device brings up the react-native dev menu. You can also press d in the terminal window where you ran npm start. The Storybook interface opens in the mobile app. The stories can be controlled either through the desktop browser Storybook UI or the mobile browser Storybook UI. Both will render the component on the device.\nCaveat: Promises are currently broken in Storybook for react native. Components using promises will not work correctly. There is a temporary hacky fix to work around this issue: storybookjs/react-native#57.\n","permalink":"https://developers.mattermost.com/contribute/more-info/mobile/storybook/","section":"contribute","subsection":"more info","tags":null,"title":"Storybook"},{"categories":null,"contents":"End-to-end tests for the Mattermost web app in general use Cypress and Playwright. If you’re not familiar with Cypress, check out the Cypress Developer Guide and API Reference. Feel free to also join us on the Mattermost Community server if you’d like to ask questions and collaborate with us! NOTE: Playwright is a new framework getting added to the Mattermost web app for test automation (and is currently being used for visual tests). Documentation about Playwright in the web app is in development, so all other content about E2E testing will be related to Cypress.\nIf you’re looking for information related to E2E tests and Redux, please check out Redux Unit and E2E Testing.\nWhat requires an E2E test? Test cases that are defined in help-wanted E2E issues. New features and stories - For example, check out MM-19922 Add E2E tests for Mark as Unread #4243 which contains E2E tests for the Mark As Unread feature. Bug fixes - For example, see MM-26751: Fix highlighting of at-mentions of self #5908, which fixes a highlighting issue and adds a related test. Test cases from Zephyr - For example, see Added Cypress tests MM-T1410, MM-T1415 and MM-T1419 #5850 which adds automated tests for Guest Accounts. File Structure for E2E Testing E2E tests are located at the root of the repository in the `e2e-tests` folder. The file structure is mostly based on the Cypress scaffold. Here is an overview of some important folders and files:\n|-- e2e-tests |-- cypress |-- tests |-- fixtures |-- integration |-- plugins |-- support |-- utils |-- cypress.config.ts |-- package.json /e2e-tests/cypress/tests/fixtures or Fixture Files: Fixtures are used as external pieces of static data that can be used by tests. Typically used with the cy.fixture() command and most often when stubbing network requests. /e2e-tests/cypress/tests/integration or Test Files: Subfolder naming convention depends on test grouping, which is usually based on the general functional area (e.g. /e2e/cypress/tests/integration/messaging/ for “Messaging”). /e2e-tests/cypress/tests/plugins or Plugin Files: A convenience mechanism that automatically includes plugins before running every single spec file. /e2e-tests/cypress/tests/support or Support Files: A support file is a place for reusable behaviour such as custom commands or global overrides that are available and can be applied to all spec files. /e2e-tests/cypress/tests/utils: this folder contains common utility functions. /e2e-tests/cypress/cypress.config.ts: this file is for Cypress configuration. /e2e-tests/cypress/package.json: this file is for all the dependencies related to Cypress end-to-end testing. Writing End-to-End Tests Where should a new test go? You will need to either add the new test to an existing spec file, or create a new file. Sometimes, you will be informed (for example through issue descriptions) of the specific folder the test file should go in, or the actual test file being amended. As aforementioned, the e2e-tests/cypress/tests/integration folder is where all of the tests live, with subdirectories that roughly divide the tests by functional areas. Cypress is configured to look for and run tests that match the pattern of *_spec.ts, so a good new test file name for an issue like Write Web App E2E with Cypress: \"MM-T642 Attachment does not collapse\" #18184 would be attachment_does_not_collapse_spec.ts, to ensure that it gets picked up.\nNote: There may be some JavaScript spec files, but new tests should be written in TypeScript. If you are adding a test to an existing spec file, convert that file to TypeScript if necessary.\nIf you don’t know where a test should go, first check the names of the subdirectories, and select a folder that describes the functional area of the test best. From there, look to see if there is already a spec file that may be similar to what you are testing; if there is one, it would be possible to add the test to the pre-existing file.\nTest metadata on spec files Test metadata is used to identify each spec file before it is forwarded for a Cypress run, and the metadata is located at the start of a spec file. Currently, supported test metadata fields include the following:\nStage - Indicates the environment for testing; valid values for this include @prod, @smoke, @pull_request. “Stage” metadata in spec files are owned and controlled by the Quality Assurance (QA) team who carefully analyze the stability of tests and promote/demote them into certain stages. This is not required when submitting a spec file and it should be removed when modifying an existing spec file.\nGroup - Indicates test group or category, which is primarily based on functional areas and existing release testing groups. Valid values for this include: @settings for Settings, @playbooks for Playbooks, etc. This is required when submitting a spec file.\nSkip - This is a way to skip running a spec file depending on the capabilities of the test environment. This is required when submitting a spec file if there is a test that has certain limitations or requirements. Forms of capabilities include:\nPlatform-related: valid values include - @darwin for Mac, @linux for Linux flavors like Ubuntu, @win32 for Windows, etc. Browser-related: valid values include - @electron, @chrome, @firefox, @edge, etc. User interface-related: valid values include @headless or @headed. A spec file can have zero or more metadata values separated by spaces (for example, // Stage: @prod @smoke). A more full example of what metadata would look like at the start of a spec file (for example, attachment_does_not_collapse_spec.ts) would be:\n// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. // *************************************************************** // - [#] indicates a test step (e.g. # Go to a page) // - [*] indicates an assertion (e.g. * Check the title) // - Use element ID when selecting an element. Create one if none. // *************************************************************** // Stage: @prod // Group: @incoming_webhook The metadata is part of a comment block that also includes information on copyright and license, and a section to explain how to tag comments in your code appropriately.\nSetting up test code Underneath the comment header, we can add the starter code as defined from the “Test code arrangement” part of the issue. Each test (no matter the situation you’re writing a test for) should have a corresponding test case in Zephyr. Therefore, the describe block encompassing the test code should correspond to folder name in Zephyr (e.g. “Incoming webhook”), and the it block should contain Zephyr test case number as Test Key, and then the test title. For Write Web App E2E with Cypress: \"MM-T642 Attachment does not collapse\" #18184, in the spec file made for it (attachment_does_not_collapse_spec.ts), the starter code would be:\ndescribe('Integrations/Incoming Webhook', () =\u003e { it('MM-T642 Attachment does not collapse', () =\u003e { // Put test steps and assertions here }); }); For those writing E2E from Help Wanted tickets with Area/E2E Tests label, the Test Key is available in the Github issue itself. The Test Key is used for mapping test cases per Release Testing specification. It will be used to measure coverage between manual and automated tests. In case the Test Key is not available, feel free to prompt the QA team who will either search for an existing Zephyr entry or if it’s a new one, it will be created for you.\nUsing Cypress Hooks Before writing the main body of the test in the it block, it can help to write some setup code for test isolation using hooks. In a before() hook, you can run tests in isolation using the custom command cy.apiInitSetup(). This command creates a new team, channel, and user which can only be used by the spec file itself. Make use of the cy.apiInitSetup() function as much as possible, as it is recommended to log in as a new user and visit the generated team and/or channel. Avoid the use of sysadmin user or default ad-1 team if possible.\nFor attachment_does_not_collapse_spec.ts for example:\nlet incomingWebhook; let testChannel; before(() =\u003e { // # Create and visit new channel and create incoming webhook cy.apiInitSetup().then(({team, channel}) =\u003e { testChannel = channel; const newIncomingHook = { channel_id: channel.id, channel_locked: true, description: 'Incoming webhook - attachment does not collapse', display_name: 'attachment-does-not-collapse', }; cy.apiCreateWebhook(newIncomingHook).then((hook) =\u003e { incomingWebhook = hook; }); cy.visit(`/${team.name}/channels/${channel.name}`); }); }); The before() hook is also a good place to add checks if a test requires a certain kind of server license. If test(s) require a certain licensed feature, use the function cy.apiRequireLicenseForFeature('\u003cfeature name\u003e'). To check if the server has a license in general, use cy.apiRequireLicense(). You can also add hard requirements in the before() hook, such as: cy.shouldNotRunOnCloudEdition(), cy.shouldRunOnTeamEdition(), cy.shouldHavePluginUploadEnabled(), cy.shouldHaveElasticsearchDisabled(), and cy.requireWebhookServer(). For more information on custom commands and how to select elements, check out the End-to-End (E2E) cheatsheets.\nPutting what you’ve gone through so far all together, you should have code that looks similar to this template:\n// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. // ********************************************************************** // - Use [#] in comment to indicate a test step (e.g. # Go to a page) // - Use [*] in comment to indicate an assertion (e.g. * Check the title) // - Query an element with @testing-library/cypress as much as possible // ********************************************************************** // Group: @change_group describe('Change to Functional Group', () =\u003e { before(() =\u003e { // Add hard requirement(s) to immediately fail and throw a descriptive error if not met // cy.shouldNotRunOnCloudEdition(); // Add license requirement(s) // cy.apiRequireLicense(); // Init basic setup for test isolation cy.apiInitSetup({loginAfter: true}).then(({team, channel, user}) =\u003e { // Assign return values to variable/s // # Visit a channel // Do other setup per test data preconditions }); }); // Add a title of \"[Zephyr_id] - [Zephyr title]\" for test case with single step, // or \"[Zephyr_id]_[step_number] - [Zephyr title]\" for test case with multiple steps it('[Zephyr_id] - [Zephyr title]', () =\u003e { // Put test steps and assertions here }); }); Main body of the test NOTE: Use camelCase when assigning to data-testid or element ID. Also, watch out for potential breaking changes in the snapshot from unit testing. Run make test to see if all unit tests are passing, and run npm run updatesnapshot or npm run test -- -u if necessary to update snapshot tests. Now, inside the body of the it block , we will write in code the “Steps” part of the E2E issue. The following steps and code are from Write Web App E2E with Cypress: \"MM-T642 Attachment does not collapse\" #18184. Check out the complete file at: `attachment_does_not_collapse_spec.ts`.\nCreate an incoming webhook and send it through POST with attachment:\n// # Post the incoming webhook with a text attachment const content = '[very long lorem ipsum test text]'; const payload = { channel: testChannel.name, attachments: [{fallback: 'testing attachment does not collapse', pretext: 'testing attachment does not collapse', text: content}], }; cy.postIncomingWebhook({url: incomingWebhook.url, data: payload, waitFor: 'attachment-pretext'}); View the webhook post that has the attachment: you are already in the channel that has the attachment post, as specified by the line cy.visit('/${team.name}/channels/${channel.name}'); from the setup section of the code.\nType /collapse and press Enter:\n// * Check \"show more\" button is visible and click cy.getLastPostId().then((postId) =\u003e { const postMessageId = `#${postId}_message`; cy.get(postMessageId).within(() =\u003e { cy.get('#showMoreButton').scrollIntoView().should('be.visible').and('have.text', 'Show more').click(); }); }); // # Type /collapse and press Enter const collapseCommand = 'collapse'; cy.uiGetPostTextBox().type(`/${collapseCommand} {enter}`); Observe the integration post with the Message Attachment: where you ascertain what is expected of the test.\ncy.getNthPostId(-2).then((postId) =\u003e { const postMessageId = `#${postId}_message`; cy.get(postMessageId).within(() =\u003e { // * Verify \"show more\" button says \"Show less\" cy.get('#showMoreButton').scrollIntoView().should('be.visible').and('have.text', 'Show less'); // * Verify gradient cy.get('#collapseGradient').should('not.be.visible'); }); Running E2E Tests On your local development machine / Gitpod If the server is not running, launch it by running make run in the server directory. Then, confirm that the Mattermost instance has started successfully. You can also run make test-data in the server directory to preload your server instance with initial seed data (you may need to restart the server again).\nEach test case should handle the required system or user settings, but if you encounter an unexpected error while testing, you may want to reset the configuration of the server to the default by going to the server directory and running make config-reset. Change the directory to e2e-tests/cypress, and install dependencies by running npm i.\nYou can then run tests in a variety of different ways by using the following commands in the e2e-tests/cypress directory:\nRunning all E2E tests: npm run cypress:run. This does not include the spec files in the /e2e-tests/cypress/tests/integration/enterprise folder because they need an Enterprise license to run successfully. Running tests selectively based on spec metadata: For example, if you want to run all the tests in a specific group, such as those in “accessibility”, the command would be: node run_tests.js --group='@accessibility'. Using the Cypress desktop app: npm run cypress:open. This will start up the Cypress desktop app, where you will be able to do partial testing depending on the spec selected in the app. If you are using Gitpod, the Cypress app will open up in the VNC desktop, which is accessible at port 6080. Don’t forget to check your coding styles! See the Web app workflow page for helpful commands to run.\nIn a Continuous Integration (CI) pipeline All tests are run by Mattermost in a CI pipeline, and they are grouped according to test stability.\nDaily production tests against development branch (master): Initiated on the master branch by using the command node run_tests.js --stage='@prod'. These production tests are selected and also labeled with @prod in the test metadata. See link for an example test run posted in our community channel. Daily production tests against release branch: Same as above except the test is initiated against the release branch. See link for an example test run. Daily unstable tests against development branch (master): Initiated on the master branch by using the command node run_tests.js --stage='@prod' --invert to run all tests except production tests. These are called “unstable tests” as they either consistently or intermittently fail due to automation bugs, and not because of product bugs. Environment variables Several environment variables (env variables) are used when testing with Cypress in order to easily change things when running tests in CI and to cater to different values across developer machines.\nEnvironment variables are defined in cypress.config.ts under the env key. In most cases you don’t need to change the values, because it makes use of the default local developer setup. If you do need to make changes, the easiest method is to override by exporting CYPRESS_*, where * is the key of the variable, for example: CYPRESS_adminUsername. See the Cypress documentation on environment variables for details.\nVariable Description CYPRESS_adminUsername Admin’s username for the test server.\nDefault: sysadmin when server is seeded by make test-data. CYPRESS_adminPassword Admin’s password for the test server.\nDefault: Sys@dmin-sample1 when server is seeded by make test-data. CYPRESS_dbClient The database of the test server. It should match the server config SqlSettings.DriverName.\nDefault: postgres Valid values: postgres or mysql CYPRESS_dbConnection The database connection string of the test server. It should match the server config SqlSettings.DataSource.\nDefault: \"postgres://mmuser:mostest@localhost/mattermost_test?sslmode=disable\\u0026connect_timeout=10\" CYPRESS_enableVisualTest Use for visual regression testing.\nDefault: false\nValid values: true or false CYPRESS_ldapServer Host of the Lightweight Directory Access Protocol (LDAP) server.\nDefault: localhost CYPRESS_ldapPort Port of the LDAP server.\nDefault: 389 CYPRESS_runLDAPSync Option to run LDAP sync.\nDefault: true\nValid values: true or false CYPRESS_resetBeforeTest When set to true, it deletes all teams and their channels where sysadmin is a member except eligendi team and its channels.\nDefault: false\nValid values: true or false CYPRESS_webhookBaseUrl A server used for testing webhook integrations.\nDefault: http://localhost:3000 when initiated with the command npm run start:webhook in the e2e-tests/cypress directory. Submitting your pull request (PR) Review the Test Guidelines for details on how to submit your PR\nTroubleshooting Test(s) failing due to a known issue If test(s) are failing due to another known issue, follow these steps to amend your test:\nAppend the Jira issue key in the test title, following the format of -- KNOWN ISSUE: [Jira_key]. For example: describe('Upload Files', () =\u003e { it('MM-T2261 Upload SVG and post -- KNOWN ISSUE: MM-38982', () =\u003e { // Test steps and assertion here }); }); Move the test case into a separate spec file following the format of \u003cexisting_spec_file_name_[1-9].js\u003e. For example: accessibility_account_settings_spec_1.js and demote the spec file (i.e. remove // Stage: @prod from the spec file) If all the test cases are failing in a spec file, update each title as mentioned above and demote the spec file. Link the failed test case(s) to the Jira issue (the known issue). In the Jira bug, select the Zephyr Scale tab. Select the add an existing one link, then select test case(s), and finally select Add. Conversely, remove the Jira issue key if the issue has been resolved and the test is passing. Cypress failed to start after running npm run cypress:run In this problem, either the command line exits immediately without running any test or it logs out like the following with the error message:\n✖ Verifying Cypress can run /Users/user/Library/Caches/Cypress/3.1.3/Cypress.app → Cypress Version: 3.1.3 Cypress failed to start. This is usually caused by a missing library or dependency. The solution to this problem is to clear node options by initiating unset NODE_OPTIONS in the command line. Running npm run cypress:run should then proceed with Cypress testing.\nRunning any Cypress spec gives ENOSPC This error may occur in Ubuntu when running any Cypress spec:\ncode: 'ENOSPC', errno: 'ENOSPC', The solution to this problem is to run the following command: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf \u0026\u0026 sudo sysctl -p.\n","permalink":"https://developers.mattermost.com/contribute/more-info/webapp/e2e-testing/","section":"contribute","subsection":"more info","tags":null,"title":"End-to-End (E2E) tests"},{"categories":null,"contents":"Selectors are functions used to compute data from the data in the Redux stores. This is done using Reselect, a library designed to do this efficiently by memoizing any results so that they are only recalculated if relevant parts of the store change. The code for this is in the src/selectors folder of the Mattermost Redux repository.\nFor more information about reselect and how we use it at Mattermost, check out this developer talk given by core developer Harrison Healey.\nUse a selector Selectors are simple functions that take the state from the redux store and return some data computed from them. Most selectors take simply the state of the redux store and return some data from the store.\nconst currentUser = getCurrentUser(state); const currentTeam = getCurrentTeam(state); Selectors can also receive additional arguments however extra care must be taken to ensure that the extra argument doesn’t prevent proper memoization. Instead of providing the selector directly, a factory function will usually be provided so that each location or component that users the selector can be memoized separately.\nconst getPostsInThread = makeGetPostsInThread(); const selectedPost = getSelectedPost(state); const postsInSelectedThread = getPostsInThread(state, selectedPost.root_id); For an example of how a selector like makeGetPostInThread could be used with React, see here.\nAdd a selector The most basic selector is just a function that takes in the state and returns a part of it without any memoization or complicated logic. All these provide is that they make it easier to access part of the store without having to remember exactly where the data is.\nexport function getCurrentUserId(state) { return state.entities.users.currentUserId; } To create a selector that actually computes some data, you need to use Reselect’s `createSelector` to combine simple selectors like the one above and provide some proper memoziation.\nexport const getCurrentUser = createSelector( getCurrentUserId, (state) =\u003e state.entities.users.profiles, (currentUserId, profiles) =\u003e { if (!profiles.hasOwnProperty(currentUserId)) { // Current user not found return {}; } return profiles[currentUserId]; } ); The way that createSelector works is that it takes any number of “input selectors” followed by a single “result function”. When you call the selector, it calls all of the input selectors to get the values that will be passed into the result function. If any of those have changed, it passes them to the result function to calculate and return the final result. If none of those values change, it skips the result function, and just returns the previous value.\nBy keeping the input selectors simple and memoizing based on their results, we can skip recalculating the final value which saves time and keeps it so that getCurrentUser(state) === getCurrentUser(state) even when getCurrentUser returns an object that can’t normally be compared with ===. This is incredibly important when using Redux with React as your selectors will likely be called many times per second so minimizing time taken and returning new objects sparingly can have significant performance gains.\nWhile Reselect doesn’t encourage the use of selectors with parameters, these can be passed in when calling the selector. Any arguments passed in will be provided to the input selectors, but not the result function. If necessary, you can work around this by including an extra input selector to pass the argument along.\nexport function makeGetUser() { return createSelector( getProfiles, (state, userId) =\u003e userId, (profiles, userId) =\u003e { if (!profiles.hasOwnProperty(userId)) { // User not found return {}; } return profiles[userId]; } ); } Note that when we make a selector that takes arguments, we typically wrap it in a factory function so that we can create multiple instances of the selector that are each memoized separately. This is because having a single instance of the above getUser selector and calling it with different user IDs would prevent any memoization since it would be constantly called with different IDs leading it to recalculate on each call.\nThis may sound unnecessary if you’re writing a one-off selector, but if you think of something like a getPost selector in the Mattermost app, we will frequently be rendering 100+ post components each with their own copy of the getPost selector. With only a single copy of that selector, it would be constantly recalculating, but with 100+ copies, each only recalculates and rerenders when their specific post changes.\n","permalink":"https://developers.mattermost.com/contribute/more-info/webapp/redux/selectors/","section":"contribute","subsection":"more info","tags":null,"title":"Selectors"},{"categories":null,"contents":"What is Gitpod? Gitpod is a cloud development environment. The following instructions have been adapted from comments on this issue: Document general usage of Gitpod #18 . You can also check out these videos to learn how to work with Gitpod (and write an E2E test): How to set up a developer environment for Mattermost with Gitpod and Writing your first E2E test for Mattermost.\n🌀 Spinning up an environment Create a new workspace for a ticket/issue that you’ve claimed by going to the mattermost-gitpod-config repository and clicking the “Open in Gitpod” badge.\nYou can also use the Gitpod browser extension to open up a repository in Gitpod, or manually prefix GitHub repository URLs with gitpod.io/#. What the extension does is add a green Gitpod button to repository pages on GitHub, and clicking it spins up a new environment for the repository on Gitpod. You may need to sign in (through GitHub) to access the workspace on Gitpod. Once Gitpod has done loading, the user interface presented is that of VSCode. ✏️ Working on an issue/ticket Make changes to one of the projects. For example, you could be working on writing an End-to-End (E2E) test, like this issue Write Webapp E2E with Cypress: \"MM-T642 Attachment does not collapse\" #18184. The development process is very similar to how you would work locally. 🌿 Making branches and forks You most likely won’t have direct write access to the repository you are working on. Thus, you will need to bring the changes you’ve made over from the master or main branch to a new branch on your own fork of the repository.\nClick the “Source Control” icon on the left sidebar. On the panel next to the side bar, you will see a list of the repositories in the workspace, and under each repository a list of the files that have changed (if any). Next to the label for mattermost-webapp, click the button with the branch name and the source control icon.\nA dropdown will appear via the command palette. Click the option that says: “+ Create new branch…” In the box that appears, name your new branch. A good name idea is to name it after the code that refers to your issue - in this case, this is the “Test Key”, so a suitable name for the branch is MM-T642. Then, hit Enter on your keyboard. Now that the branch has changed, you can commit your changes to it. Back in the source control panel, you can make a commit message in the text box above the “commit button” in the section for the mattermost-webapp repository. Then, press the “commit button”. A modal may appear asking you to first stage your changes. You can now publish your branch to the remote. However, as you will not have access to the main repository itself, you will be prompted to first create a fork. Click on the “publish branch button” on the source control panel. A popup will appear, asking if you would like to make a fork and push to that instead. On the popup, select the “create fork” button. During the process of fork creation, you may be prompted to grant GitHub access to Gitpod’s GitHub extension (you should allow this). You may also be asked to update the permissions you give Gitpod to access GitHub through a popup. If this happens, you will have to restart the fork creation process (publish branch -\u003e say yes to creating a fork). Click the “open access control” button on the popup. You will get taken to the Integration section of Gitpod’s settings. In the list of Git Providers on the page, find GitHub, click the three dots next to the listing, and select “Edit Permissions” from the dropdown menu. A popup will appear with a list of permission checkboxes. It’s a good idea to check all of them off so you don’t need to go through this process again. When you’re done checking off the permissions, click the “Update Permissions” button. Another tab might also appear where on GitHub’s end you accept the additional permissions Gitpod is requesting. Click on the “authorize gitpod-io” button. Once the forking process is done, there will be a couple of popups that appear on Gitpod: one asking if you’d like to periodically run git fetch (which will periodically download content from the remote), another one asking if you’d like to create a pull request for the branch you’re on right inside Gitpod/the VSCode editor, and finally one informing that the fork was successfully created. On the successful fork creation popup, click the open on GitHub option. 🔍 Creating a Pull Request (PR) You’ll get taken to GitHub, to the fork of the repository you’ve worked on in your account instead of the main one in the Mattermost organization. Navigate to the branch you’ve made on your fork if you’re not there already.\nNear the top of the page will be a bar mentioning how many commits ahead/behind your branch is from the master/main branch of the main repository. There will also be two buttons: “contribute” and “sync fork”. Click on the “contribute” button, and in the dropdown that appears, click the “open pull request” button. You’ll be taken to another page on GitHub called “Open a pull request”. The first section of the page compares whether the branches (your branch on your fork - the “head repository” vs. the master/main branch on the main repository - the “base repository”) can be merged automatically.\nThe second section of the page will show other PRs that are based on your same branch, if any.\nThe third section is where you create a write-up for your pull request - giving it a title, and filling out the template. At the bottom of this section is a “Create pull request” button. This button will be faded out until you make a title for the PR.\nIf you haven’t already, give Mattermost’s Contribution Checklist a read. An important takeaway is that you will need to sign the Contributor License Agreement - this will be another check on the pull request and if you haven’t signed it, this will also block merging. Also check out this blog post about Submitting Great PRs, and other repository specific information here. Parts of a PR body: Title: a good title will refer back to the issue; and it should begin with the related Jira or GitHub ticket ID (e.g. [MM-394] or [GH-394]). In the context of the E2E issue example: MM-T642: Attachment does not collapse - Cypress Webapp E2E Test. Summary: description of what the PR does, as well as QA test steps (if applicable and if not already added to the Jira ticket). For example: Verifies that attachments on posts do not collapse after entering the slash command collapse. Ticket Link: Either link the relevant Jira ticket or if you picked up an issue/ticket with a Helped Wanted label, link to the GitHub issue. For example: Write Webapp E2E with Cypress: “MM-T642 Attachment does not collapse” #18184. Related Pull Requests: Link other PRs here if they are related to this PR. Screenshots: Illustrate what your changes have done. Release Note: There are certain conditions that require release notes: Config changes (additions, deletions, updates). API additions—new endpoints, new response fields, or newly accepted request parameters. Database changes (any). Schema migration changes. Use the Schema Migration Template as a starting point to capture these details as release notes. Websocket additions or changes. Anything noteworthy to a Mattermost instance administrator (err on the side of over-communicating). New features and improvements, including behavioral changes, UI changes, and CLI changes. Bug fixes and fixes of previous known issues. Deprecation warnings, breaking changes, or compatibility notes. If no release notes are required, write NONE. Use past-tense. For E2E tests, having NONE as a release note suffices. If you do not end up writing a release note at all, you’ll get a warning on your PR like this: Adding the \"do-not-merge/release-note-label-needed\" label [to the PR] because no release-note block was detected, please follow our release note process to remove it. If this happens, you can just edit the body of the PR, and add it back in. The last section details the code changes on your branch, including information on the commits on the branch, the files changed, and the contributors.\nOnce you’ve created your pull request, you’ll get taken to its page, like this one: MM-T642: Attachment does not collapse - Cypress Webapp E2E Test #11231. Below your initial body text of the PR will be a list of commits and other comments. At the end of this list is a checklist which notes the status of reviews required for the pull request, and the checks that the pull request must pass, plus a place to write your own additional comments. if you need to make any changes to your PR, you can return to Gitpod and stage and commit your changes from there onto your branch; and this will be reflected in GitHub.\n✅ Code Review Information from this section comes from: Code review at Mattermost.\nWait for a reviewer to be assigned - normally this is handled automatically, but if you need help feel free to ask for help in the Developers channel of the Mattermost community server.\nWait for a review - if a reviewer requests changes, your PR will disappear from their queue of reviews. Once you’ve addressed the concerns, re-request a review from any person requesting changes. Avoid force pushing, which is the act of overwriting the commit history on the remote with what you have on local.\nOnce all reviewers approve your pull request, they will handle the merging of your code.\n","permalink":"https://developers.mattermost.com/contribute/more-info/getting-started/using-gitpod/","section":"contribute","subsection":"more info","tags":null,"title":"Using Gitpod"},{"categories":null,"contents":"This page compiles all Cypress custom commands based on specific sections of the web app, as well as other general examples. The examples provided showcase the best and conventions on how to write great automated test scripts. We encourage everyone to read this information, ask questions if something is not clear, and challenge the mentioned practices so that we can continuously refine and improve.\nIf you need to add more custom commands, add them to /e2e-tests/cypress/tests/support, and check out the Cypress custom commands documentation. For ease of use, in-code documentation functionality, and making custom commands more discoverable, add type definitions. See this example of a declaration file for reference on how to include and make type definitions.\nGeneral Queries with the Testing Library The Testing Library is used through the package @testing-library/cypress, and it provides simple and complete custom Cypress commands and utilities that encourage such good testing practices. To decide on the queries from the Testing Library you should be using while writing Cypress tests, check out this article to learn more. For instance, you can select something with test ID using: cy.findByTestId.\nIf you need more help, check out the online Testing Playground—no install required, always up-to-date, and it teaches you the exact Testing-Library queries you should be using as you click through the DOM. And if you’d rather stay in a VSCode editor, check out the VS Code Testing Playground extension, which brings the same interactive query suggestions and best-practice guidance right into your IDE.\nThe following is a short summary of the recommended order of priority for queries:\n✅ Queries Accessible to Everyone These reflect the experience of visual/mouse users as well as those that use assistive technology. Examples include: cy.findByRole, cy.findByLabelText, cy.findByPlaceholderText, cy.findByText, and cy.findByDisplayValue.\n✅ Semantic Queries These use HTML5 and ARIA–compliant selectors. Note that the user experience of interacting with these attributes varies greatly across browsers and assistive technology. Some examples include: cy.findByAltText and cy.findByTitle.\n⚠️ Base Queries These are considered part of implementation details and are discouraged to be used. You will still find base queries in the codebase but they will be replaced soon. Therefore, please refrain from reusing the existing base query patterns. However, you may want to use them only to limit the scope of selection. Examples include: cy.get('#elementId') and cy.get('.class-name'). Below is an acceptable use case of base queries:\n// limit the scope but chained with recommended query cy.get('#elementId').should('be.visible').findByRole('button', {name: 'Save'}).click(); // limit the scope then use the recommended queries within the scope cy.get('.class-name').should('be.visible').within(() =\u003e { cy.findByRole('input', {name: 'Position'}).type('Software Developer'); cy.findByRole('button', {name: 'Save'}).click(); }); ✅ Query Variants Note that cy.findBy* are shown but other variants are cy.findAllBy*, cy.queryBy*, and cy.queryAllBy*. See the Queries section from testing-library.\n❌ Off-limits Queries Please do not use any Xpath selectors such as the descendant selector. Do not use the ul \u003e li and order selectors either, like ul \u003e li:nth-child(2). If an element can only be queried with this approach, then you may modify the application codebase, improve it, and make it “accessible to everyone”.\nSettings Modal Opening the settings modal The function cy.uiOpenSettingsModal(section) opens the settings modal when viewing a channel. section is of the \u003c string \u003e type. Possible values for section are: 'Notifications', 'Display', 'Sidebar', and 'Advanced'.\nOpen ‘Settings’ modal and view the default ‘General Settings’: cy.uiOpenSettingsModal(); Open the Settings modal and view a specific section (like the ‘Advanced’ section): cy.uiOpenSettingsModal('Advanced'); Open the Settings modal, view a specific section, and change a setting: // # Open 'Advanced' section of 'Settings' modal cy.uiOpenSettingsModal('Advanced').within(() =\u003e { // # Open 'Enable Join/Leave Messages' and turn it off cy.findByRole('heading', {name: 'Enable Join/Leave Messages'}).click(); cy.findByRole('radio', {name: 'Off'}).click(); // # Save and close the modal cy.uiSave(); cy.uiClose(); }); Selecting a section’s button within a modal Use the function cy.findByRoleExtended('button', {name}). name is of the \u003c string \u003e type. Possible values for name are: 'Notifications', 'Display', 'Sidebar', and 'Advanced'.\nClicking a button within the Settings modal: // # Open 'Advanced' section of 'Settings' modal cy.uiOpenSettingsModal().within(() =\u003e { // # Click 'Notifications' button cy.findByRoleExtended('button', {name: 'Notifications'}).should('be.visible').click(); }); Select a section’s setting within a modal via the name of the section Use the function cy.findByRole('heading', {name}). name is of the \u003c string \u003e type. Possible values for name are: 'Full Name', 'Username', and others depending on the sections in the modal.\nOpen a section within the Settings modal: // # Open 'Notifications' of 'Settings' modal cy.uiOpenSettingsModal('Notifications').within(() =\u003e { // # Open 'Words That Trigger Mentions' setting cy.findByRole('heading', {name: 'Words That Trigger Mentions'}).should('be.visible').click(); }); Select a section’s setting within a modal via role Use the function cy.findByRole(role, {name}). role is of the \u003c string \u003e type. Possible values for role are: 'textbox', 'radio', 'checkbox' and other roles. name is of the \u003c string \u003e type. Possible values for name are: 'On', 'Off', and others depending on a section’s settings.\nChange value of a section’s setting in the Settings modal: // # Open 'Notifications' of 'Settings' modal cy.uiOpenSettingsModal('Notifications').within(() =\u003e { // # Open 'Words That Trigger Mentions' setting cy.findByRole('heading', {name: 'Words That Trigger Mentions'}).should('be.visible').click(); // # Check channel-wide mentions cy.findByRole('checkbox', {name: 'Channel-wide mentions \"@channel\", \"@all\", \"@here\"'}).click(); }); Saving and closing a modal cy.uiSave and cy.uiClose are common functions that can be used to save things and close modals.\nSaving and closing in the Settings modal: // # Open 'Notifications' of 'Settings' modal cy.uiOpenSettingsModal('Notifications').within(() =\u003e { // # Open 'Words That Trigger Mentions' setting cy.findByRole('heading', {name: 'Words That Trigger Mentions'}).should('be.visible').click(); // # Check channel-wide mentions cy.findByRole('checkbox', {name: 'Channel-wide mentions \"@channel\", \"@all\", \"@here\"'}).click(); // # Save then close the modal cy.uiSave(); cy.uiClose(); }); Channel Menu Opening the channel menu Use the function cy.uiOpenChannelMenu(item). This will open the channel menu by clicking the channel header title or dropdown icon when viewing a channel. item is of the type \u003c string \u003e. Possible values for item are: 'View Info', 'Move to...','Notification Preferences', 'Mute Channel', 'Add Members', 'Manage Members','Edit Channel Header', 'Edit Channel Purpose', 'Rename Channel', and 'Convert to Private Channel', 'Archive Channel', and 'Leave Channel'.\nOpen the channel menu normally: // # Open 'Channel Menu' cy.uiOpenChannelMenu(); Open the channel menu and click on a specific item: // # Open 'Advanced' section of 'Settings' modal cy.uiOpenChannelMenu('View Info'); Closing the channel menu Use the function cy.uiCloseChannelMenu(). This will close the channel menu by clicking the channel header title or dropdown icon again at the center channel view, given that the menu is already open.\nGet the DOM elements of the channel menu Use the function cy.uiGetChannelMenu().\nProduct Menu Opening the product menu Use the function cy.uiOpenProductMenu(item). item is of the type \u003c string \u003e. Possible values for item are: 'Channels', 'Boards', 'Playbooks', 'System Console', 'Integrations', 'Marketplace', 'Download Apps', and 'About Mattermost'.\nOpen the product menu normally: // # Open 'Product menu' cy.uiOpenProductMenu(); Open the product menu and click on a specific item: // # Open 'Integrations' section of 'Product Menu' modal cy.uiOpenProductMenu('Integrations'); Get the DOM elements of the product menu Use the function cy.uiGetProductMenu().\n","permalink":"https://developers.mattermost.com/contribute/more-info/webapp/e2e-cheatsheets/","section":"contribute","subsection":"more info","tags":null,"title":"End-to-End (E2E) cheatsheets"},{"categories":null,"contents":"We leverage GitHub labels to track the details and lifecycle of issues and pull requests.\nIssue labels Area/\u003cname\u003e: Involves changes to the named area (APIv4, E2E Tests, Localization, Plugins, etc.) Bug Report/Open: Bug report unresolved, awaiting for more information or in development backlog. Bug Report/Scheduled for Release: Bug report resolved and scheduled for an upcoming release. Milestone indicates scheduled release version. Difficulty/1:easy: Easy ticket. Difficulty/2:medium: Medium ticket. Difficulty/3:hard: Hard ticket. Good First Issue: Suitable for first-time contributors. Help Wanted: Community help wanted. Move to Feature Ideas forum: Marked for relocation to the feature ideas forum. Move to Troubleshooting: Marked for relocation to the troubleshooting section of the documentation. PR Submitted: A pull request has been opened for this issue. Tech/\u003cname\u003e: Requires using the named technology (Go, JavaScript, ReactJS, Redux, etc.) Up for Grabs: Ready for help from the community. Removed when someone volunteers. Pull request labels 1: PM Review: Requires review by a product manager. 1: UX Review: Requires review by a UX designer. 1: SME Review: Requires review by a subject matter expert (used in the Handbook). 2: Dev Review: Requires review by a core committer. 2: Editor Review: Requires review by a technical writer. 3: QA Review: Requires review by a QA tester. May occur at the same time as Dev Review. 4: Reviews Complete: All reviewers have approved the pull request. Awaiting Submitter Action: Blocked on the author. AutoMerge: If all checks and approvals pass and the user adds this label, it will be in the queue to get merge automatically without a human intervention. Changelog/Done: Required changelog entry has been written. Changelog/Not Needed: Does not require a changelog entry. CherryPick/Approved: Meant for the quality or patch release tracked in the milestone. CherryPick/Candidate: A candidate for a quality or patch release, but not yet approved. CherryPick/Done: Successfully cherry-picked to the quality or patch release tracked in the milestone. Demo Plugin Changes/Needed: Requires changes to the demo plugin. Demo Plugin Changes/Done: Required changes to the demo plugin have been submitted. Do Not Merge/Awaiting Loadtest: Must be loadtested before it can be merged. Do Not Merge/Awaiting Next Release: To be merged with the next release (e.g. API documentation updates). Do Not Merge/Awaiting PR: Awaiting another pull request before merging (e.g. server changes). Do Not Merge: Should not be merged until this label is removed. Docs/Done: Required documentation has been written. Docs/Needed: Requires documentation. Docs/Not Needed: Does not require documentation. Hackfest: Related to a Mattermost hackathon. Hacktoberfest: Related to Hacktoberfest. Lifecycle/\u003cstate\u003e: An inactive contribution. Loadtest: Triggers an automatic load test. Major Change: The pull request is a major feature or affects large areas of the code base (e.g. moving channel store and actions to Redux). QA Deferred: Testing of this PR is expected to be completed after merge, likely when it is available on Community. Apply this in lieu of asking for 3: QA Review. Setup Cloud Test Server: Triggers the creation of a Enterprise Edition test server. Setup HA Cloud Test Server: Triggers the creation of a test server that has high availability. Setup Cloud + CWS Test Server: Triggers the creation of a test server that connects to our test Customer Web Server. Setup Upgrade Test Server: Triggers the creation a test server and performs an upgrade. Tests/Done: Required tests have been written. Tests/Not Needed: Does not require tests. Work in Progress: Not yet ready for review. ","permalink":"https://developers.mattermost.com/contribute/more-info/getting-started/labels/","section":"contribute","subsection":"more info","tags":null,"title":"Labels"},{"categories":null,"contents":"The Mattermost Desktop App is an Electron wrapper around the web app project. It lives in the mattermost/desktop repository. The desktop app runs on Windows, Linux, and macOS.\nDesktop app contributor resources GitHub Repository - Get the code, report issues, or submit PRs. Help Wanted - This is a good place to start if you’re looking for a way to contribute code. Many issues are labeled by difficulty level to make it easier to find ways to get involved. Developer Setup - Setup your development environment to start work on the desktop app. Build and CLI Commands - Useful commands to help build, debug, test, and modify the desktop app on your local machine. Debugging - Identify issues in the desktop app and debug the rendering process. Dependencies - Information about including dependencies in the desktop app. Style and Code Quality - Information about linting, type checking, and submitting great PRs Unit and End-to-End (E2E) Tests - Find out how we incorporate unit and end-to-end testing into the desktop app development process. Packaging for Release - Build and package the app into a distributable version. General Contributor Guidelines - Everything you need to know about contributing code to Mattermost. Where to get help If you have any questions related to development of the Desktop App, you can ask us in the Developers: Desktop App channel on our community Mattermost. If you need help deploying, administering, or using Mattermost, refer to our Get Help guide to find all of the resources that are availalbe to support your journey.\n","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/","section":"contribute","subsection":"more info","tags":null,"title":"Desktop app"},{"categories":null,"contents":"Using Redux with React is fairly straightforward thanks to the React Redux library. It provides the connect function to create higher order components that have access to the Redux store to set their props.\nA typical Redux-connected component will be in its own folder with two files: index.jsx containing the code to connect to the Redux store and the file where the component is actually implemented. This helps to keep the Redux logic separate from the rendering for the component which keeps it more easily readable and makes it easier to test since it can be done without the whole Redux store.\n// components/my_component/index.jsx import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import {messageUser} from 'actions/entities/users'; import {getCurrentUser, getUser} from 'selectors/entities/users'; import MyComponent from './my_component'; // mapStateToProps receives the Redux store state and any props passed into the connected // component, and they are used to return any additional data from the Redux store that is // needed to render the component. ownProps will also be passed directly to the component. function mapStateToProps(state, ownProps) { return { currentUser: getCurrentUser(state), otherUser: getUser(state, ownProps.userId), }; } // mapDispatchToProps receives the Redux store's dispatch method so that bindActionCreators // can be used to automatically dispatch those actions as necessary. function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ messageUser, }, dispatch) }; } export default connect(mapStateToProps, mapDispatchToProps)(MyComponent); // components/my_component/my_component.jsx import React from 'react'; function MyComponent(props) { const handleClick = () =\u003e { props.actions.messageUser(props.otherUser, props.currentUser, `Hello, ${props.otherUser.first_name}!`); }; return ( \u003clabel\u003e {`${props.otherUser.first_name} ${props.otherUser.last_name}: `} \u003cbutton onClick={this.handleClick}\u003e{'Say Hi'}\u003c/button\u003e \u003c/label\u003e ); } Both mapStateToProps and mapDispatchToProps are optional and can be omitted as necessary.\nIf you’re using a selector that is produced through a factory, such as makeGetUser, you can instead generate an individual mapStateToProps function for each instance of the component.\n// component/my_component/index.jsx ... import {getCurrentUser, makeGetUser} from 'selectors/entities/users'; // makeMapStateToProps is called once for each instance of the component on the page. Because of this // a separate getUser selector is created for each instance, allowing them to be memoized separately. function makeMapStateToProps() { const getUser = makeGetUser(); return (state, ownProps) =\u003e { return { currentUser: getCurrentUser(state), otherUser: getUser(state, ownProps.userId) }; }; } ... export default connect(makeMapStateToProps, mapDispatchToProps)(MyComponent); Performance considerations Something very important to note when using React with Redux is that every single mapStateToProps function within your application will be called whenever anything in the store changes. If any work being done in mapStateToProps performs any complicated calculations or returns rich objects, it should be moved into a selector so that it can be memoized whenever possible.\n","permalink":"https://developers.mattermost.com/contribute/more-info/webapp/redux/react-redux/","section":"contribute","subsection":"more info","tags":null,"title":"Use Redux with React"},{"categories":null,"contents":"Unit Testing Unit Testing for Actions Tests for both actions and action creators are written using Jest and will often focus on seeing how dispatching an action affects the stored state in Redux. It’ll often look similar to testing a reducer except you’ll be looking at the whole store state instead of a single part of it.\nThere are a few different ways of testing Redux actions used throughout Mattermost, but the most common way involves:\nSetting up an initial store state for the test case.\nOptionally mocking any external operations that may be required for the action. This includes API requests which are mocked using Nock.\nDispatching the result of the action creator.\nLooking at the resulting store state to ensure the required changes are made.\nimport nock from 'nock'; import mockStore from 'tests/test_store'; import {somethingAsyncHappened, somethingHappened} from './actions'; describe('somethingHappened', () =\u003e { const channelId = 'channelId'; test('should update state.somethingCount', () =\u003e { const store = mockStore({ somethingCount: 0, }); // Remember to actually call your action creator since that's very easy to forget to do store.dispatch(somethingHappened(channelId)); expect(store.getState().somethingCount).toBe(1234); }); }); describe('somethingAsyncHappened', () =\u003e { // Initial state may be shared between multiple test cases and may include state that's required for both // testing and for thunk actions const currentUserId = 'currentUserId'; const initialState = { entities: { users: { currentUserId: 'user1', }, }, somethingCount: 0, }; test('should update state.somethingCount on success', async () =\u003e { const store = mockStore(initialState); const expectedResult = {status: 'SomethingHappened'}; nock(Client4.getBaseRoute()). post(`/channels/${channelId}/something`). reply(200, {}); // Remember that tests for async requests need to themselves be async and we need to wait for the dispatch await store.dispatch(somethingAsyncHappened(channelId)); expect(store.getState().somethingCount).toBe(1234); }); test('should update state.somethingCount on failure', async () =\u003e { const store = mockStore(initialState); const expectedResult = {status: 'SomethingHappened'}; nock(Client4.getBaseRoute()). post(`/channels/${channelId}/something`). reply(400, {}); // You can also inspect the result of the action if desired const result = await store.dispatch(somethingAsyncHappened(channelId)); expect(result.error).toBeDefined(); expect(result.data).not.toBeDefined(); expect(store.getState().somethingCount).toBe(0); }); }); Add unit tests to make sure that the action has the intended effects on the store. Test location is adjacent to the file being tested. Example, for src/actions/admin.js, test is located at src/actions/admin.test.js. Add test file if necessary. More information on unit testing reducers is available below.\nSome unit tests found throughout the web app may also test the actions dispatched by a thunk action rather than testing the effects on the changes to the store state. This method isn’t considered as effective.\nUnit Testing for Selectors Unit tests for selectors are located in the same directory, adjacent to the file being tested. For example, the test for src/selectors/admin.js is located at src/selectors/admin.test.js. These tests are written using Jest Testing Framework. In that folder, there are many examples of how those tests should look. Most follow the same general pattern of:\nConstruct the initial test state. Note that this doesn’t need to be shared between tests as it is in many other cases. Pass the state into the selector and check the results. The tests for some more complicated selectors do this multiple times while changing different parts of the store to ensure that the memoization is working correctly since it can be very important in certain areas of the app. End-to-End (E2E) Testing E2E Tests for Actions Sometimes, it’s not easy to test a redux action given it contains complicated async logic or requires a large amount of Redux state to be initialized to test it out. Other times, an action may feel too simple to test, especially if it’s just dispatching an action that dictates specifically how the Redux state should change.\nIn cases where the action will have an effect that’s visible to the end user, it’s possible to rely more on end-to-end testing. While this might not test every code path of the action such as poor network conditions, end-to-end tests are often more valuble since they involve testing that the code as a whole does what is expected rather than testing just that a single piece of code works under artificial conditions which may not be realistic.\n","permalink":"https://developers.mattermost.com/contribute/more-info/webapp/redux/testing/","section":"contribute","subsection":"more info","tags":null,"title":"Redux Testing"},{"categories":null,"contents":"As part of the pull request review process, reviewers may need to test and verify proposed changes. Leveraging our Cloud infrastructure, we can spin up full environments on demand to test code submitted in PRs.\nCore committers and staff can trigger test server creation on a PR by adding one of the following labels to the PR:\nSetup Cloud Test Server: Triggers the creation of a standard test server using the latest commit on the PR and a PostgreSQL database. Setup HA Cloud Test Server: Triggers the creation of a test server that has high availability. Setup Cloud + CWS Test Server: Triggers the creation of a test server that connects to our test Customer Web Server. After adding these labels, a bot will comment on the PR notifying you that a test server is being created. It should take approximately 3-5 minutes for the server to create it. Once it’s ready, the bot will comment on the PR again with a link and credentials to the test server for both an admin and a regular user.\nIf the bot comments that an error has occurred, try removing the label and then re-adding it again. If that still fails, please ask for help in ~Developers: Cloud. If you need urgent help, please mention @sresupport in your message.\nOnce testing is complete, remove the label and the test server will be destroyed.\nTest servers are available on any repositories that have the labels.\nTips and tricks Avoid adding and removing the labels quickly in succession - this can confuse the bot and result in issues. Please be patient. :) Pushing new commits to PRs will trigger the bot to automatically update the test server. When submitting a new PR or pushing an update to a PR, the docker build/push CI step must complete before a test server can be created/updated. Please make sure to remove labels when testing is complete since there is a limited capacity for test servers. If you want a test server to have the changes from two different PRs across the webapp and server repositories, ensure: Both server and webapp branches are named the same and are on the main repos and not from forks. The server build completes before the webapp build runs (you can re-trigger the webapp build if it didn’t). The test server was created after the webapp build is complete (that included the server build) and present on the PR for the web app. ","permalink":"https://developers.mattermost.com/contribute/more-info/getting-started/test-servers/","section":"contribute","subsection":"more info","tags":null,"title":"Test servers"},{"categories":null,"contents":"The Mattermost web app uses Redux as its state management library. Its key features are a centralized data store for the entire app and a pattern for predictably modifying and displaying that application state. Notably, we’re not using Redux Toolkit since a large portion of our Redux code predates its existence.\nIn addition to Redux itself, we also use:\nReact Redux to connect React components to the Redux store using higher-order components like connect or hooks like useSelector. Redux Thunk to write async actions and logic that interacts more closely with the Redux store. Currently, the different packages in the web app use Redux in varying amounts. The bulk of our Redux code is in channels where it’s split between logic that’s more view-oriented, located at the root of its src directory, and logic that’s more server-oriented, located in channels/src/packages/mattermost-redux.\n","permalink":"https://developers.mattermost.com/contribute/more-info/webapp/redux/","section":"contribute","subsection":"more info","tags":null,"title":"Redux"},{"categories":null,"contents":"The Focalboard project is written in TypeScript and Go.\nHere’s the process for contributing to Focalboard:\nFork the Focalboard repository, clone it locally, and follow the steps in the Personal Server Setup Guide to build it. You can read the CHANGELOG to learn about recent updates.\nFind help wanted tickets that are up for grabs in GitHub. Comment to let everyone know you’re working on it and let a core contributor assign the issue to you. If there’s no ticket for what you want to work on, read about contributions without a ticket.\nWhen your changes are checked in to your fork, follow the steps on our contribution checklist. If this will be your first contribution, there is a standard CLA that you will need to sign as part of this checklist.\nSubmit your pull request for a code review and wait for a Focalboard core committer to review it. When in doubt, ask for help in the Focalboard channel on our community server. If you are still stuck, please message Chen Lim (@chenilim on GitHub).\nAfter a noteable bug fix or improvement is merged, submit a pull request to the CHANGELOG under the next release section.\nWe’re glad ❤️ you’re here! Good luck and have fun!\nRepository https://github.com/mattermost/focalboard\nCommunity You can join the public Focalboard channel on our Mattermost community server. You can also file a bug for an issue or start a discussion on the repository.\nHelp wanted You can find help wanted tickets here.\n","permalink":"https://developers.mattermost.com/contribute/more-info/focalboard/","section":"contribute","subsection":"more info","tags":null,"title":"Focalboard"},{"categories":null,"contents":"The Mattermost team wants to proactively improve the quality, security, and stability of the code, and one way to do this is by introducing the usage of type checking. Thus, we have decided to introduce Typescript in our codebase as it’s a mature and feature-rich approach.\nAs a first step, we have migrated the mattermost-redux library to use Typescript, and are now in the process of migrating the web app to use Typescript.\nThis campaign will help with the migration by converting files written in Javascript to type-safe files written in Typescript.\nBy completing this campaign, we’re looking to:\nReduce the errors derived from changes. Increase the consistency of the code. Ensure a more defensive programming in the code. Contribute If you’re interested in contributing, please join the Typescript Migration channel on community.mattermost.com. You can also check out the Contributors channel, where there are several posts mentioning tickets related to this campaign, each containing the hashtag #typescriptmigration. You can work on migrating an individual module to Typescript by claiming a ticket that matches this GitHub issue search.\nComponent migration steps There are a few steps involved with migrating a file to use Typescript. Some of them may not apply to every file and they may change slightly based on the file you’re working on. In general, you can follow these steps as a checklist for work that needs to be done on each file.\nEvery React component’s set of props needs to be converted to a new type. You can use the component’s propTypes as a template for what props a given component can expect. This conversion to Typescript includes maintaining whether a given prop is required or not. An optional property in Typescript is noted by including a question mark at the end of the property’s name. A component’s state also needs to be defined using a type. The initial state assignment and any call to setState will be indicators of what values are present in the component’s state. Once a component’s props and state (if any) have been converted, you can define the component as class MyComponent extends React.PureComponent\u003cProps, State\u003e. You can omit the State portion if the component does not have its own state. Avoid use of the any type except in test files. The components themselves should be as well-defined as possible. Most objects we used are typed in the @mattermost/types library, if you can’t find a type you’re looking for. Check that the types are all correct using the make check-types command. Examples You can see example pull requests here:\nhttps://github.com/mattermost/mattermost/pull/5840 https://github.com/mattermost/mattermost/pull/5244 ","permalink":"https://developers.mattermost.com/contribute/more-info/webapp/migrating-to-typescript/","section":"contribute","subsection":"more info","tags":null,"title":"Migrate to Typescript"},{"categories":null,"contents":"Mattermost Server In the mattermost repository, we are using Docker images and Docker Compose to set up the development enviroment. The following are required images:\nMySQL PostgreSQL MinIO Inbucket OpenLDAP Elasticsearch We also have added optional tools to help with your development:\nDejavu Dejavu is a user interface for Elasticsearch when no UI is provided to visualize or modify the data you’re storing inside Elasticsearch.\nTo use Dejavu, execute docker-compose up -d dejavu. It will run at http://localhost:1358.\n","permalink":"https://developers.mattermost.com/contribute/more-info/server/tooling/","section":"contribute","subsection":"more info","tags":null,"title":"Tools"},{"categories":null,"contents":"make i18n-extract is a command used for localization. It allows you to validate that your strings have been successfully extracted from your source code before you continue.\nThis page demonstrates how to review your results and to verify if your extraction was successful or not. If the extraction was not successful this page also provides a workaround to correct for this.\nNOTE: These steps haven’t been updated since Mattermost has switched to using a monorepo. We’re in the process of updating our I18n workflow and these corresponding docs.\nIn the meantime, these commands can be run from within each package (webapp/boards, webapp/channels, and webapp/playbooks) to update their corresponding translation files.\nAfter you execute make i18n-extract you will need to review the results and validate that the strings were either added or removed in the i18n/en.json file.\nRun git diff and determine if your strings were added or removed in the i18n/en.json file correctly. If this was a successful extraction you will have output similar to below:\nHowever, if you have a string that was not properly extracted you will see an output similar to below. If you executed the make i18n-extract at this point nothing would change because the string \"new-text-id\" is not detected as a string that needs to be translated.\nThe solution is to tag the string. Do this by using the \"t\" function, shown in the example below:\nAt this point you will need to execute the make i18n-extract once again and determine if the extraction was successful. This will generate a message in the i18n/en.json file. However, this is not going to extract the “default message”, you will have to add this yourself. See example below:\nNOTE: Be aware that when you use the \"t\" function, only the translation id is extracted. You have to add the translation string in the i18n/en.json file manually. For further discussion about translations or to ask for help, refer to the following Mattermost channels: Localization and Contributors.\n","permalink":"https://developers.mattermost.com/contribute/more-info/webapp/using-i18n-extract/","section":"contribute","subsection":"more info","tags":null,"title":"Use make i18n-extract"},{"categories":null,"contents":"Plugins are defined by a manifest file and contain at least a server or web app component, or both.\nThe Plugin Starter Template is a starting point and illustrates the different components of a Mattermost plugin.\nA more detailed example is the Demo Plugin, which showcases many of the features of plugins.\nIf you’d like to better understand how plugins work, see the contributor documentation on plugins.\nManifest The plugin manifest provides required metadata about the plugin, such as name and ID. It is defined in JSON or YAML. This is plugin.json in both the sample and demo plugins.\nSee the manifest reference for more information.\nServer The server component of a plugin is written in Go and runs as a subprocess of the Mattermost server process. The Go code extends the MattermostPlugin struct that contains an API and allows for the implementation of Hook methods that enable the plugin to interact with the Mattermost server.\nThe sample plugin implements this simply in plugin.go and the demo plugin splits the API and hook usage throughout multiple files.\nRead more about the server-side of plugins here.\nWeb/desktop app The web app component of a plugin is written in JavaScript with React and Redux. The plugin’s bundled JavaScript is included on the page and runs alongside the web app code as a PluginClass that has initialize and uninitialize methods available for implementation. The initialize function is passed through the registry which allows the plugin to register React components, actions and hooks to modify and interact with the Mattermost web app.\nThe sample plugin has a shell of an implemented PluginClass, while the demo plugin contains a more complete example.\nThe desktop app is a shim of the web app, meaning any plugin that works in the web app will also work in the desktop app.\nRead more about the web app component of plugins here.\nMobile app Currently there is no mobile app component of plugins but it is planned for the near term.\n","permalink":"https://developers.mattermost.com/integrate/plugins/overview/","section":"integrate","subsection":"plugins","tags":null,"title":"Overview"},{"categories":null,"contents":"This is the documentation for the Go github.com/mattermost/mattermost/server/public/plugin package. It can also be found on GoDoc.\nVisit the Plugins section to learn more about developing Mattermost plugins and our recommended developer workflow for Mattermost plugins.\nThe plugin package is used by Mattermost server plugins written in go. It also enables the Mattermost server to manage and interact with the running plugin environment. Note that this package exports a large number of types prefixed with Z_. These are public only to allow their use with Hashicorp's go-plugin (and net/rpc). Do not use these directly. API The API can be used to retrieve data or perform actions on behalf of the plugin. Most methods have direct counterparts in the REST API and very similar behavior. Plugins obtain access to the API by embedding MattermostPlugin and accessing the API member directly. GetSession(sessionID string) (*model.Session, *model.AppError) OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError KVSetWithOptions(key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) PublishWebSocketEvent(event string, payload map[string]any, broadcast *model.WebsocketBroadcast) RolesGrantPermission(roleNames []string, permissionId string) bool SendMail(to, subject, htmlBody string) *model.AppError PluginHTTP(request *http.Request) *http.Response PublishPluginClusterEvent(ev model.PluginClusterEvent, opts model.PluginClusterEventSendOptions) error RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError GetCloudLimits() (*model.ProductLimits, error) EnsureBotUser(bot *model.Bot) (string, error) RegisterCollectionAndTopic(collectionType, topicType string) error SendPushNotification(notification *model.PushNotification, userID string) *model.AppError Audit LogAuditRec(rec *model.AuditRecord) LogAuditRecWithLevel(rec *model.AuditRecord, level mlog.Level) Bot CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model.AppError) GetBots(options *model.BotGetOptions) ([]*model.Bot, *model.AppError) UpdateBotActive(botUserId string, active bool) (*model.Bot, *model.AppError) PermanentDeleteBot(botUserId string) *model.AppError Channel GetUsersInChannel(channelID, sortBy string, page, perPage int) ([]*model.User, *model.AppError) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) DeleteChannel(channelId string) *model.AppError GetPublicChannelsForTeam(teamID string, page, perPage int) ([]*model.Channel, *model.AppError) GetChannel(channelId string) (*model.Channel, *model.AppError) GetChannelByName(teamID, name string, includeDeleted bool) (*model.Channel, *model.AppError) GetChannelByNameForTeamName(teamName, channelName string, includeDeleted bool) (*model.Channel, *model.AppError) GetChannelsForTeamForUser(teamID, userID string, includeDeleted bool) ([]*model.Channel, *model.AppError) GetChannelStats(channelId string) (*model.ChannelStats, *model.AppError) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) SearchChannels(teamID string, term string) ([]*model.Channel, *model.AppError) AddChannelMember(channelId, userID string) (*model.ChannelMember, *model.AppError) AddUserToChannel(channelId, userID, asUserId string) (*model.ChannelMember, *model.AppError) GetChannelMember(channelId, userID string) (*model.ChannelMember, *model.AppError) GetChannelMembers(channelId string, page, perPage int) (model.ChannelMembers, *model.AppError) GetChannelMembersByIds(channelId string, userIds []string) (model.ChannelMembers, *model.AppError) GetChannelMembersForUser(teamID, userID string, page, perPage int) ([]*model.ChannelMember, *model.AppError) UpdateChannelMemberRoles(channelId, userID, newRoles string) (*model.ChannelMember, *model.AppError) UpdateChannelMemberNotifications(channelId, userID string, notifications map[string]string) (*model.ChannelMember, *model.AppError) PatchChannelMembersNotifications(members []*model.ChannelMemberIdentifier, notifyProps map[string]string) *model.AppError DeleteChannelMember(channelId, userID string) *model.AppError GetPostsSince(channelId string, time int64) (*model.PostList, *model.AppError) GetPostsAfter(channelId, postId string, page, perPage int) (*model.PostList, *model.AppError) GetPostsBefore(channelId, postId string, page, perPage int) (*model.PostList, *model.AppError) GetPostsForChannel(channelId string, page, perPage int) (*model.PostList, *model.AppError) UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) HasPermissionToChannel(userID, channelId string, permission *model.Permission) bool ChannelSidebar CreateChannelSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) GetChannelSidebarCategories(userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError) UpdateChannelSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) Command RegisterCommand(command *model.Command) error UnregisterCommand(teamID, trigger string) error ExecuteSlashCommand(commandArgs *model.CommandArgs) (*model.CommandResponse, error) Configuration GetConfig() *model.Config GetUnsanitizedConfig() *model.Config SaveConfig(config *model.Config) *model.AppError Emoji GetEmojiList(sortBy string, page, perPage int) ([]*model.Emoji, *model.AppError) GetEmojiByName(name string) (*model.Emoji, *model.AppError) GetEmoji(emojiId string) (*model.Emoji, *model.AppError) GetEmojiImage(emojiId string) ([]byte, string, *model.AppError) File CopyFileInfos(userID string, fileIds []string) ([]string, *model.AppError) GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) SetFileSearchableContent(fileID string, content string) *model.AppError GetFileInfos(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) GetFile(fileId string) ([]byte, *model.AppError) GetFileLink(fileId string) (string, *model.AppError) ReadFile(path string) ([]byte, *model.AppError) UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) Group GetGroup(groupId string) (*model.Group, *model.AppError) GetGroupByName(name string) (*model.Group, *model.AppError) GetGroupMemberUsers(groupID string, page, perPage int) ([]*model.User, *model.AppError) GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) GetGroupsForUser(userID string) ([]*model.Group, *model.AppError) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) CreateGroup(group *model.Group) (*model.Group, *model.AppError) UpdateGroup(group *model.Group) (*model.Group, *model.AppError) DeleteGroup(groupID string) (*model.Group, *model.AppError) RestoreGroup(groupID string) (*model.Group, *model.AppError) DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) CreateDefaultSyncableMemberships(params model.CreateDefaultMembershipParams) *model.AppError DeleteGroupConstrainedMemberships() *model.AppError KeyValueStore KVSet(key string, value []byte) *model.AppError KVCompareAndSet(key string, oldValue, newValue []byte) (bool, *model.AppError) KVCompareAndDelete(key string, oldValue []byte) (bool, *model.AppError) KVSetWithExpiry(key string, value []byte, expireInSeconds int64) *model.AppError KVGet(key string) ([]byte, *model.AppError) KVDelete(key string) *model.AppError KVDeleteAll() *model.AppError KVList(page, perPage int) ([]string, *model.AppError) Logging LogDebug(msg string, keyValuePairs ...any) LogInfo(msg string, keyValuePairs ...any) LogError(msg string, keyValuePairs ...any) LogWarn(msg string, keyValuePairs ...any) OAuth CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) GetOAuthApp(appID string) (*model.OAuthApp, *model.AppError) UpdateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) DeleteOAuthApp(appID string) *model.AppError Plugin LoadPluginConfiguration(dest any) error GetPluginConfig() map[string]any SavePluginConfig(config map[string]any) *model.AppError GetBundlePath() (string, error) GetPlugins() ([]*model.Manifest, *model.AppError) EnablePlugin(id string) *model.AppError DisablePlugin(id string) *model.AppError RemovePlugin(id string) *model.AppError GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) GetPluginID() string Post SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) ([]*model.Post, *model.AppError) SearchPostsInTeamForUser(teamID string, userID string, searchParams model.SearchParameter) (*model.PostSearchResults, *model.AppError) CreatePost(post *model.Post) (*model.Post, *model.AppError) AddReaction(reaction *model.Reaction) (*model.Reaction, *model.AppError) RemoveReaction(reaction *model.Reaction) *model.AppError GetReactions(postId string) ([]*model.Reaction, *model.AppError) SendEphemeralPost(userID string, post *model.Post) *model.Post UpdateEphemeralPost(userID string, post *model.Post) *model.Post DeleteEphemeralPost(userID, postId string) DeletePost(postId string) *model.AppError GetPostThread(postId string) (*model.PostList, *model.AppError) GetPost(postId string) (*model.Post, *model.AppError) GetPostsSince(channelId string, time int64) (*model.PostList, *model.AppError) GetPostsAfter(channelId, postId string, page, perPage int) (*model.PostList, *model.AppError) GetPostsBefore(channelId, postId string, page, perPage int) (*model.PostList, *model.AppError) GetPostsForChannel(channelId string, page, perPage int) (*model.PostList, *model.AppError) UpdatePost(post *model.Post) (*model.Post, *model.AppError) Preference GetPreferenceForUser(userID, category, name string) (model.Preference, *model.AppError) GetPreferencesForUser(userID string) ([]model.Preference, *model.AppError) UpdatePreferencesForUser(userID string, preferences []model.Preference) *model.AppError DeletePreferencesForUser(userID string, preferences []model.Preference) *model.AppError PropertyField CreatePropertyField(field *model.PropertyField) (*model.PropertyField, error) GetPropertyField(groupID, fieldID string) (*model.PropertyField, error) GetPropertyFields(groupID string, ids []string) ([]*model.PropertyField, error) UpdatePropertyField(groupID string, field *model.PropertyField) (*model.PropertyField, error) DeletePropertyField(groupID, fieldID string) error SearchPropertyFields(groupID, targetID string, opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) GetPropertyFieldByName(groupID, targetID, name string) (*model.PropertyField, error) UpdatePropertyFields(groupID string, fields []*model.PropertyField) ([]*model.PropertyField, error) PropertyGroup RegisterPropertyGroup(name string) (*model.PropertyGroup, error) GetPropertyGroup(name string) (*model.PropertyGroup, error) PropertyValue CreatePropertyValue(value *model.PropertyValue) (*model.PropertyValue, error) GetPropertyValue(groupID, valueID string) (*model.PropertyValue, error) GetPropertyValues(groupID string, ids []string) ([]*model.PropertyValue, error) UpdatePropertyValue(groupID string, value *model.PropertyValue) (*model.PropertyValue, error) UpsertPropertyValue(value *model.PropertyValue) (*model.PropertyValue, error) DeletePropertyValue(groupID, valueID string) error SearchPropertyValues(groupID, targetID string, opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error) UpdatePropertyValues(groupID string, values []*model.PropertyValue) ([]*model.PropertyValue, error) UpsertPropertyValues(values []*model.PropertyValue) ([]*model.PropertyValue, error) DeletePropertyValuesForTarget(groupID, targetType, targetID string) error DeletePropertyValuesForField(groupID, fieldID string) error Server GetLicense() *model.License IsEnterpriseReady() bool GetServerVersion() string GetSystemInstallDate() (int64, *model.AppError) GetDiagnosticId() string GetTelemetryId() string SharedChannels RegisterPluginForSharedChannels(opts model.RegisterPluginOpts) (remoteID string, err error) UnregisterPluginForSharedChannels(pluginID string) error ShareChannel(sc *model.SharedChannel) (*model.SharedChannel, error) UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error) UnshareChannel(channelID string) (unshared bool, err error) UpdateSharedChannelCursor(channelID, remoteID string, cusror model.GetPostsSinceForSyncCursor) error SyncSharedChannel(channelID string) error InviteRemoteToChannel(channelID string, remoteID string, userID string, shareIfNotShared bool) error UninviteRemoteFromChannel(channelID string, remoteID string) error SlashCommand CreateCommand(cmd *model.Command) (*model.Command, error) ListCommands(teamID string) ([]*model.Command, error) ListCustomCommands(teamID string) ([]*model.Command, error) ListPluginCommands(teamID string) ([]*model.Command, error) ListBuiltInCommands() ([]*model.Command, error) GetCommand(commandID string) (*model.Command, error) UpdateCommand(commandID string, updatedCmd *model.Command) (*model.Command, error) DeleteCommand(commandID string) error Team GetUsersInTeam(teamID string, page int, perPage int) ([]*model.User, *model.AppError) GetTeamIcon(teamID string) ([]byte, *model.AppError) SetTeamIcon(teamID string, data []byte) *model.AppError RemoveTeamIcon(teamID string) *model.AppError CreateTeam(team *model.Team) (*model.Team, *model.AppError) DeleteTeam(teamID string) *model.AppError GetTeams() ([]*model.Team, *model.AppError) GetTeam(teamID string) (*model.Team, *model.AppError) GetTeamByName(name string) (*model.Team, *model.AppError) GetTeamsUnreadForUser(userID string) ([]*model.TeamUnread, *model.AppError) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) SearchTeams(term string) ([]*model.Team, *model.AppError) GetTeamsForUser(userID string) ([]*model.Team, *model.AppError) CreateTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) CreateTeamMembers(teamID string, userIds []string, requestorId string) ([]*model.TeamMember, *model.AppError) CreateTeamMembersGracefully(teamID string, userIds []string, requestorId string) ([]*model.TeamMemberWithError, *model.AppError) DeleteTeamMember(teamID, userID, requestorId string) *model.AppError GetTeamMembers(teamID string, page, perPage int) ([]*model.TeamMember, *model.AppError) GetTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) GetTeamMembersForUser(userID string, page int, perPage int) ([]*model.TeamMember, *model.AppError) UpdateTeamMemberRoles(teamID, userID, newRoles string) (*model.TeamMember, *model.AppError) GetPublicChannelsForTeam(teamID string, page, perPage int) ([]*model.Channel, *model.AppError) GetChannelByNameForTeamName(teamName, channelName string, includeDeleted bool) (*model.Channel, *model.AppError) GetChannelsForTeamForUser(teamID, userID string, includeDeleted bool) ([]*model.Channel, *model.AppError) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) ([]*model.Post, *model.AppError) GetTeamStats(teamID string) (*model.TeamStats, *model.AppError) HasPermissionToTeam(userID, teamID string, permission *model.Permission) bool UpdateUserRoles(userID, newRoles string) (*model.User, *model.AppError) Upload CreateUploadSession(us *model.UploadSession) (*model.UploadSession, error) UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, error) GetUploadSession(uploadID string) (*model.UploadSession, error) User CreateUser(user *model.User) (*model.User, *model.AppError) DeleteUser(userID string) *model.AppError GetUsers(options *model.UserGetOptions) ([]*model.User, *model.AppError) GetUsersByIds(userIDs []string) ([]*model.User, *model.AppError) GetUser(userID string) (*model.User, *model.AppError) GetUserByEmail(email string) (*model.User, *model.AppError) GetUserByUsername(name string) (*model.User, *model.AppError) GetUsersByUsernames(usernames []string) ([]*model.User, *model.AppError) GetUsersInTeam(teamID string, page int, perPage int) ([]*model.User, *model.AppError) GetPreferenceForUser(userID, category, name string) (model.Preference, *model.AppError) GetPreferencesForUser(userID string) ([]model.Preference, *model.AppError) UpdatePreferencesForUser(userID string, preferences []model.Preference) *model.AppError DeletePreferencesForUser(userID string, preferences []model.Preference) *model.AppError CreateSession(session *model.Session) (*model.Session, *model.AppError) ExtendSessionExpiry(sessionID string, newExpiry int64) *model.AppError RevokeSession(sessionID string) *model.AppError CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) RevokeUserAccessToken(tokenID string) *model.AppError UpdateUser(user *model.User) (*model.User, *model.AppError) GetUserStatus(userID string) (*model.Status, *model.AppError) GetUserStatusesByIds(userIds []string) ([]*model.Status, *model.AppError) UpdateUserStatus(userID, status string) (*model.Status, *model.AppError) SetUserStatusTimedDND(userId string, endtime int64) (*model.Status, *model.AppError) UpdateUserActive(userID string, active bool) *model.AppError UpdateUserCustomStatus(userID string, customStatus *model.CustomStatus) *model.AppError RemoveUserCustomStatus(userID string) *model.AppError GetUsersInChannel(channelID, sortBy string, page, perPage int) ([]*model.User, *model.AppError) GetLDAPUserAttributes(userID string, attributes []string) (map[string]string, *model.AppError) GetTeamsUnreadForUser(userID string) ([]*model.TeamUnread, *model.AppError) GetTeamsForUser(userID string) ([]*model.Team, *model.AppError) CreateTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) CreateTeamMembers(teamID string, userIds []string, requestorId string) ([]*model.TeamMember, *model.AppError) CreateTeamMembersGracefully(teamID string, userIds []string, requestorId string) ([]*model.TeamMemberWithError, *model.AppError) DeleteTeamMember(teamID, userID, requestorId string) *model.AppError GetTeamMembers(teamID string, page, perPage int) ([]*model.TeamMember, *model.AppError) GetTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) GetTeamMembersForUser(userID string, page int, perPage int) ([]*model.TeamMember, *model.AppError) UpdateTeamMemberRoles(teamID, userID, newRoles string) (*model.TeamMember, *model.AppError) GetChannelsForTeamForUser(teamID, userID string, includeDeleted bool) ([]*model.Channel, *model.AppError) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) SearchUsers(search *model.UserSearch) ([]*model.User, *model.AppError) AddChannelMember(channelId, userID string) (*model.ChannelMember, *model.AppError) AddUserToChannel(channelId, userID, asUserId string) (*model.ChannelMember, *model.AppError) GetChannelMember(channelId, userID string) (*model.ChannelMember, *model.AppError) GetChannelMembers(channelId string, page, perPage int) (model.ChannelMembers, *model.AppError) GetChannelMembersByIds(channelId string, userIds []string) (model.ChannelMembers, *model.AppError) GetChannelMembersForUser(teamID, userID string, page, perPage int) ([]*model.ChannelMember, *model.AppError) UpdateChannelMemberRoles(channelId, userID, newRoles string) (*model.ChannelMember, *model.AppError) UpdateChannelMemberNotifications(channelId, userID string, notifications map[string]string) (*model.ChannelMember, *model.AppError) PatchChannelMembersNotifications(members []*model.ChannelMemberIdentifier, notifyProps map[string]string) *model.AppError GetGroupsForUser(userID string) ([]*model.Group, *model.AppError) DeleteChannelMember(channelId, userID string) *model.AppError GetProfileImage(userID string) ([]byte, *model.AppError) SetProfileImage(userID string, data []byte) *model.AppError CopyFileInfos(userID string, fileIds []string) ([]string, *model.AppError) HasPermissionTo(userID string, permission *model.Permission) bool HasPermissionToTeam(userID, teamID string, permission *model.Permission) bool HasPermissionToChannel(userID, channelId string, permission *model.Permission) bool PublishUserTyping(userID, channelId, parentId string) *model.AppError UpdateUserAuth(userID string, userAuth *model.UserAuth) (*model.UserAuth, *model.AppError) UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) UpdateUserRoles(userID, newRoles string) (*model.User, *model.AppError) Hooks 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. OnActivate() error Implemented() ([]string, error) OnDeactivate() error OnConfigurationChange() error ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) UserHasBeenCreated(c *plugin.Context, user *model.User) UserWillLogIn(c *plugin.Context, user *model.User) string UserHasLoggedIn(c *plugin.Context, user *model.User) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) MessageWillBeUpdated(c *plugin.Context, newPost, oldPost *model.Post) (*model.Post, string) MessageHasBeenPosted(c *plugin.Context, post *model.Post) MessageHasBeenUpdated(c *plugin.Context, newPost, oldPost *model.Post) MessagesWillBeConsumed(posts []*model.Post) []*model.Post MessageHasBeenDeleted(c *plugin.Context, post *model.Post) ChannelHasBeenCreated(c *plugin.Context, channel *model.Channel) UserHasJoinedChannel(c *plugin.Context, channelMember *model.ChannelMember, actor *model.User) UserHasLeftChannel(c *plugin.Context, channelMember *model.ChannelMember, actor *model.User) UserHasJoinedTeam(c *plugin.Context, teamMember *model.TeamMember, actor *model.User) UserHasLeftTeam(c *plugin.Context, teamMember *model.TeamMember, actor *model.User) FileWillBeUploaded(c *plugin.Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) ReactionHasBeenAdded(c *plugin.Context, reaction *model.Reaction) ReactionHasBeenRemoved(c *plugin.Context, reaction *model.Reaction) OnPluginClusterEvent(c *plugin.Context, ev model.PluginClusterEvent) OnWebSocketConnect(webConnID, userID string) OnWebSocketDisconnect(webConnID, userID string) WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest) RunDataRetention(nowTime, batchSize int64) (int64, error) OnInstall(c *plugin.Context, event model.OnInstallEvent) error OnSendDailyTelemetry() OnCloudLimitsUpdated(limits *model.ProductLimits) ConfigurationWillBeSaved(newCfg *model.Config) (*model.Config, error) NotificationWillBePushed(pushNotification *model.PushNotification, userID string) (*model.PushNotification, string) UserHasBeenDeactivated(c *plugin.Context, user *model.User) ServeMetrics(c *plugin.Context, w http.ResponseWriter, r *http.Request) OnSharedChannelsSyncMsg(msg *model.SyncMsg, rc *model.RemoteCluster) (model.SyncResponse, error) OnSharedChannelsPing(rc *model.RemoteCluster) bool PreferencesHaveChanged(c *plugin.Context, preferences []model.Preference) OnSharedChannelsAttachmentSyncMsg(fi *model.FileInfo, post *model.Post, rc *model.RemoteCluster) error OnSharedChannelsProfileImageSyncMsg(user *model.User, rc *model.RemoteCluster) error GenerateSupportData(c *plugin.Context) ([]*model.FileData, error) OnSAMLLogin(c *plugin.Context, user *model.User, assertion *gosaml2.AssertionInfo) error Helpers Examples HelloWorld HelpPlugin API (API) LoadPluginConfiguration LoadPluginConfiguration(dest any) error LoadPluginConfiguration loads the plugin's configuration. dest should be a pointer to a struct that the configuration JSON can be unmarshalled to. @tag Plugin Minimum server version: 5.2 (API) RegisterCommand RegisterCommand(command *model.Command) error RegisterCommand registers a custom slash command. When the command is triggered, your plugin can fulfill it via the ExecuteCommand hook. @tag Command Minimum server version: 5.2 (API) UnregisterCommand UnregisterCommand(teamID, trigger string) error UnregisterCommand unregisters a command previously register via RegisterCommand. @tag Command Minimum server version: 5.2 (API) ExecuteSlashCommand ExecuteSlashCommand(commandArgs *model.CommandArgs) (*model.CommandResponse, error) ExecuteSlashCommand executes a slash command with the given parameters. @tag Command Minimum server version: 5.26 (API) GetConfig GetConfig() *model.Config GetConfig fetches the currently persisted config @tag Configuration Minimum server version: 5.2 (API) GetUnsanitizedConfig GetUnsanitizedConfig() *model.Config GetUnsanitizedConfig fetches the currently persisted config without removing secrets. @tag Configuration Minimum server version: 5.16 (API) SaveConfig SaveConfig(config *model.Config) *model.AppError SaveConfig sets the given config and persists the changes @tag Configuration Minimum server version: 5.2 (API) GetPluginConfig GetPluginConfig() map[string]any GetPluginConfig fetches the currently persisted config of plugin @tag Plugin Minimum server version: 5.6 (API) SavePluginConfig SavePluginConfig(config map[string]any) *model.AppError SavePluginConfig sets the given config for plugin and persists the changes @tag Plugin Minimum server version: 5.6 (API) GetBundlePath GetBundlePath() (string, error) GetBundlePath returns the absolute path where the plugin's bundle was unpacked. @tag Plugin Minimum server version: 5.10 (API) GetLicense GetLicense() *model.License GetLicense returns the current license used by the Mattermost server. Returns nil if the server does not have a license. @tag Server Minimum server version: 5.10 (API) IsEnterpriseReady IsEnterpriseReady() bool IsEnterpriseReady returns true if the Mattermost server is configured as Enterprise Ready. @tag Server Minimum server version: 5.10 (API) GetServerVersion GetServerVersion() string GetServerVersion return the current Mattermost server version @tag Server Minimum server version: 5.4 (API) GetSystemInstallDate GetSystemInstallDate() (int64, *model.AppError) GetSystemInstallDate returns the time that Mattermost was first installed and ran. @tag Server Minimum server version: 5.10 (API) GetDiagnosticId GetDiagnosticId() string GetDiagnosticId returns a unique identifier used by the server for diagnostic reports. @tag Server Minimum server version: 5.10 (API) GetTelemetryId GetTelemetryId() string GetTelemetryId returns a unique identifier used by the server for telemetry reports. @tag Server Minimum server version: 5.28 (API) CreateUser CreateUser(user *model.User) (*model.User, *model.AppError) CreateUser creates a user. @tag User Minimum server version: 5.2 (API) DeleteUser DeleteUser(userID string) *model.AppError DeleteUser deletes a user. @tag User Minimum server version: 5.2 (API) GetUsers GetUsers(options *model.UserGetOptions) ([]*model.User, *model.AppError) GetUsers a list of users based on search options. Not all fields in UserGetOptions are supported by this API. @tag User Minimum server version: 5.10 (API) GetUsersByIds GetUsersByIds(userIDs []string) ([]*model.User, *model.AppError) GetUsersByIds gets a list of users by their IDs. @tag User Minimum server version: 9.8 (API) GetUser GetUser(userID string) (*model.User, *model.AppError) GetUser gets a user. @tag User Minimum server version: 5.2 (API) GetUserByEmail GetUserByEmail(email string) (*model.User, *model.AppError) GetUserByEmail gets a user by their email address. @tag User Minimum server version: 5.2 (API) GetUserByUsername GetUserByUsername(name string) (*model.User, *model.AppError) GetUserByUsername gets a user by their username. @tag User Minimum server version: 5.2 (API) GetUsersByUsernames GetUsersByUsernames(usernames []string) ([]*model.User, *model.AppError) GetUsersByUsernames gets users by their usernames. @tag User Minimum server version: 5.6 (API) GetUsersInTeam GetUsersInTeam(teamID string, page int, perPage int) ([]*model.User, *model.AppError) GetUsersInTeam gets users in team. @tag User @tag Team Minimum server version: 5.6 (API) GetPreferenceForUser GetPreferenceForUser(userID, category, name string) (model.Preference, *model.AppError) GetPreferenceForUser gets a single preference for a user. An error is returned if the user has no preference set with the given category and name, an error is returned. @tag User @tag Preference Minimum server version: 9.5 (API) GetPreferencesForUser GetPreferencesForUser(userID string) ([]model.Preference, *model.AppError) GetPreferencesForUser gets a user's preferences. @tag User @tag Preference Minimum server version: 5.26 (API) UpdatePreferencesForUser UpdatePreferencesForUser(userID string, preferences []model.Preference) *model.AppError UpdatePreferencesForUser updates a user's preferences. @tag User @tag Preference Minimum server version: 5.26 (API) DeletePreferencesForUser DeletePreferencesForUser(userID string, preferences []model.Preference) *model.AppError DeletePreferencesForUser deletes a user's preferences. @tag User @tag Preference Minimum server version: 5.26 (API) GetSession GetSession(sessionID string) (*model.Session, *model.AppError) GetSession returns the session object for the Session ID Minimum server version: 5.2 (API) CreateSession CreateSession(session *model.Session) (*model.Session, *model.AppError) CreateSession creates a new user session. @tag User Minimum server version: 6.2 (API) ExtendSessionExpiry ExtendSessionExpiry(sessionID string, newExpiry int64) *model.AppError ExtendSessionExpiry extends the duration of an existing session. @tag User Minimum server version: 6.2 (API) RevokeSession RevokeSession(sessionID string) *model.AppError RevokeSession revokes an existing user session. @tag User Minimum server version: 6.2 (API) CreateUserAccessToken CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) CreateUserAccessToken creates a new access token. @tag User Minimum server version: 5.38 (API) RevokeUserAccessToken RevokeUserAccessToken(tokenID string) *model.AppError RevokeUserAccessToken revokes an existing access token. @tag User Minimum server version: 5.38 (API) GetTeamIcon GetTeamIcon(teamID string) ([]byte, *model.AppError) GetTeamIcon gets the team icon. @tag Team Minimum server version: 5.6 (API) SetTeamIcon SetTeamIcon(teamID string, data []byte) *model.AppError SetTeamIcon sets the team icon. @tag Team Minimum server version: 5.6 (API) RemoveTeamIcon RemoveTeamIcon(teamID string) *model.AppError RemoveTeamIcon removes the team icon. @tag Team Minimum server version: 5.6 (API) UpdateUser UpdateUser(user *model.User) (*model.User, *model.AppError) UpdateUser updates a user. @tag User Minimum server version: 5.2 (API) GetUserStatus GetUserStatus(userID string) (*model.Status, *model.AppError) GetUserStatus will get a user's status. @tag User Minimum server version: 5.2 (API) GetUserStatusesByIds GetUserStatusesByIds(userIds []string) ([]*model.Status, *model.AppError) GetUserStatusesByIds will return a list of user statuses based on the provided slice of user IDs. @tag User Minimum server version: 5.2 (API) UpdateUserStatus UpdateUserStatus(userID, status string) (*model.Status, *model.AppError) UpdateUserStatus will set a user's status until the user, or another integration/plugin, sets it back to online. The status parameter can be: \"online\", \"away\", \"dnd\", or \"offline\". @tag User Minimum server version: 5.2 (API) SetUserStatusTimedDND SetUserStatusTimedDND(userId string, endtime int64) (*model.Status, *model.AppError) SetUserStatusTimedDND will set a user's status to dnd for given time until the user, or another integration/plugin, sets it back to online. @tag User Minimum server version: 5.35 (API) UpdateUserActive UpdateUserActive(userID string, active bool) *model.AppError UpdateUserActive deactivates or reactivates an user. @tag User Minimum server version: 5.8 (API) UpdateUserCustomStatus UpdateUserCustomStatus(userID string, customStatus *model.CustomStatus) *model.AppError UpdateUserCustomStatus will set a user's custom status until the user, or another integration/plugin, clear it or update the custom status. The custom status have two parameters: emoji icon and custom text. @tag User Minimum server version: 6.2 (API) RemoveUserCustomStatus RemoveUserCustomStatus(userID string) *model.AppError RemoveUserCustomStatus will remove a user's custom status. @tag User Minimum server version: 6.2 (API) GetUsersInChannel GetUsersInChannel(channelID, sortBy string, page, perPage int) ([]*model.User, *model.AppError) GetUsersInChannel returns a page of users in a channel. Page counting starts at 0. The sortBy parameter can be: \"username\" or \"status\". @tag User @tag Channel Minimum server version: 5.6 (API) GetLDAPUserAttributes GetLDAPUserAttributes(userID string, attributes []string) (map[string]string, *model.AppError) GetLDAPUserAttributes will return LDAP attributes for a user. The attributes parameter should be a list of attributes to pull. Returns a map with attribute names as keys and the user's attributes as values. Requires an enterprise license, LDAP to be configured and for the user to use LDAP as an authentication method. @tag User Minimum server version: 5.3 (API) CreateTeam CreateTeam(team *model.Team) (*model.Team, *model.AppError) CreateTeam creates a team. @tag Team Minimum server version: 5.2 (API) DeleteTeam DeleteTeam(teamID string) *model.AppError DeleteTeam deletes a team. @tag Team Minimum server version: 5.2 (API) GetTeams GetTeams() ([]*model.Team, *model.AppError) GetTeam gets all teams. @tag Team Minimum server version: 5.2 (API) GetTeam GetTeam(teamID string) (*model.Team, *model.AppError) GetTeam gets a team. @tag Team Minimum server version: 5.2 (API) GetTeamByName GetTeamByName(name string) (*model.Team, *model.AppError) GetTeamByName gets a team by its name. @tag Team Minimum server version: 5.2 (API) GetTeamsUnreadForUser GetTeamsUnreadForUser(userID string) ([]*model.TeamUnread, *model.AppError) GetTeamsUnreadForUser gets the unread message and mention counts for each team to which the given user belongs. @tag Team @tag User Minimum server version: 5.6 (API) UpdateTeam UpdateTeam(team *model.Team) (*model.Team, *model.AppError) UpdateTeam updates a team. @tag Team Minimum server version: 5.2 (API) SearchTeams SearchTeams(term string) ([]*model.Team, *model.AppError) SearchTeams search a team. @tag Team Minimum server version: 5.8 (API) GetTeamsForUser GetTeamsForUser(userID string) ([]*model.Team, *model.AppError) GetTeamsForUser returns list of teams of given user ID. @tag Team @tag User Minimum server version: 5.6 (API) CreateTeamMember CreateTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) CreateTeamMember creates a team membership. @tag Team @tag User Minimum server version: 5.2 (API) CreateTeamMembers CreateTeamMembers(teamID string, userIds []string, requestorId string) ([]*model.TeamMember, *model.AppError) CreateTeamMembers creates a team membership for all provided user ids. @tag Team @tag User Minimum server version: 5.2 (API) CreateTeamMembersGracefully CreateTeamMembersGracefully(teamID string, userIds []string, requestorId string) ([]*model.TeamMemberWithError, *model.AppError) CreateTeamMembersGracefully creates a team membership for all provided user ids and reports the users that were not added. @tag Team @tag User Minimum server version: 5.20 (API) DeleteTeamMember DeleteTeamMember(teamID, userID, requestorId string) *model.AppError DeleteTeamMember deletes a team membership. @tag Team @tag User Minimum server version: 5.2 (API) GetTeamMembers GetTeamMembers(teamID string, page, perPage int) ([]*model.TeamMember, *model.AppError) GetTeamMembers returns the memberships of a specific team. @tag Team @tag User Minimum server version: 5.2 (API) GetTeamMember GetTeamMember(teamID, userID string) (*model.TeamMember, *model.AppError) GetTeamMember returns a specific membership. @tag Team @tag User Minimum server version: 5.2 (API) GetTeamMembersForUser GetTeamMembersForUser(userID string, page int, perPage int) ([]*model.TeamMember, *model.AppError) GetTeamMembersForUser returns all team memberships for a user. @tag Team @tag User Minimum server version: 5.10 (API) UpdateTeamMemberRoles UpdateTeamMemberRoles(teamID, userID, newRoles string) (*model.TeamMember, *model.AppError) UpdateTeamMemberRoles updates the role for a team membership. @tag Team @tag User Minimum server version: 5.2 (API) CreateChannel CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) CreateChannel creates a channel. @tag Channel Minimum server version: 5.2 (API) DeleteChannel DeleteChannel(channelId string) *model.AppError DeleteChannel deletes a channel. @tag Channel Minimum server version: 5.2 (API) GetPublicChannelsForTeam GetPublicChannelsForTeam(teamID string, page, perPage int) ([]*model.Channel, *model.AppError) GetPublicChannelsForTeam gets a list of all channels. @tag Channel @tag Team Minimum server version: 5.2 (API) GetChannel GetChannel(channelId string) (*model.Channel, *model.AppError) GetChannel gets a channel. @tag Channel Minimum server version: 5.2 (API) GetChannelByName GetChannelByName(teamID, name string, includeDeleted bool) (*model.Channel, *model.AppError) GetChannelByName gets a channel by its name, given a team id. @tag Channel Minimum server version: 5.2 (API) GetChannelByNameForTeamName GetChannelByNameForTeamName(teamName, channelName string, includeDeleted bool) (*model.Channel, *model.AppError) GetChannelByNameForTeamName gets a channel by its name, given a team name. @tag Channel @tag Team Minimum server version: 5.2 (API) GetChannelsForTeamForUser GetChannelsForTeamForUser(teamID, userID string, includeDeleted bool) ([]*model.Channel, *model.AppError) GetChannelsForTeamForUser gets a list of channels for given user ID in given team ID, including DMs. If an empty string is passed as the team ID, the user's channels on all teams and their DMs will be returned. @tag Channel @tag Team @tag User Minimum server version: 5.6 (API) GetChannelStats GetChannelStats(channelId string) (*model.ChannelStats, *model.AppError) GetChannelStats gets statistics for a channel. @tag Channel Minimum server version: 5.6 (API) GetDirectChannel GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) GetDirectChannel gets a direct message channel. If the channel does not exist it will create it. @tag Channel @tag User Minimum server version: 5.2 (API) GetGroupChannel GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) GetGroupChannel gets a group message channel. If the channel does not exist it will create it. @tag Channel @tag User Minimum server version: 5.2 (API) UpdateChannel UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) UpdateChannel updates a channel. @tag Channel Minimum server version: 5.2 (API) SearchChannels SearchChannels(teamID string, term string) ([]*model.Channel, *model.AppError) SearchChannels returns the channels on a team matching the provided search term. @tag Channel Minimum server version: 5.6 (API) CreateChannelSidebarCategory CreateChannelSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, *model.AppError) CreateChannelSidebarCategory creates a new sidebar category for a set of channels. @tag ChannelSidebar Minimum server version: 5.38 (API) GetChannelSidebarCategories GetChannelSidebarCategories(userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError) GetChannelSidebarCategories returns sidebar categories. @tag ChannelSidebar Minimum server version: 5.38 (API) UpdateChannelSidebarCategories UpdateChannelSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) UpdateChannelSidebarCategories updates the channel sidebar categories. @tag ChannelSidebar Minimum server version: 5.38 (API) SearchUsers SearchUsers(search *model.UserSearch) ([]*model.User, *model.AppError) SearchUsers returns a list of users based on some search criteria. @tag User Minimum server version: 5.6 (API) SearchPostsInTeam SearchPostsInTeam(teamID string, paramsList []*model.SearchParams) ([]*model.Post, *model.AppError) SearchPostsInTeam returns a list of posts in a specific team that match the given params. @tag Post @tag Team Minimum server version: 5.10 (API) SearchPostsInTeamForUser SearchPostsInTeamForUser(teamID string, userID string, searchParams model.SearchParameter) (*model.PostSearchResults, *model.AppError) SearchPostsInTeamForUser returns a list of posts by team and user that match the given search parameters. @tag Post Minimum server version: 5.26 (API) AddChannelMember AddChannelMember(channelId, userID string) (*model.ChannelMember, *model.AppError) AddChannelMember joins a user to a channel (as if they joined themselves) This means the user will not receive notifications for joining the channel. @tag Channel @tag User Minimum server version: 5.2 (API) AddUserToChannel AddUserToChannel(channelId, userID, asUserId string) (*model.ChannelMember, *model.AppError) AddUserToChannel adds a user to a channel as if the specified user had invited them. This means the user will receive the regular notifications for being added to the channel. @tag User @tag Channel Minimum server version: 5.18 (API) GetChannelMember GetChannelMember(channelId, userID string) (*model.ChannelMember, *model.AppError) GetChannelMember gets a channel membership for a user. @tag Channel @tag User Minimum server version: 5.2 (API) GetChannelMembers GetChannelMembers(channelId string, page, perPage int) (model.ChannelMembers, *model.AppError) GetChannelMembers gets a channel membership for all users. @tag Channel @tag User Minimum server version: 5.6 (API) GetChannelMembersByIds GetChannelMembersByIds(channelId string, userIds []string) (model.ChannelMembers, *model.AppError) GetChannelMembersByIds gets a channel membership for a particular User @tag Channel @tag User Minimum server version: 5.6 (API) GetChannelMembersForUser GetChannelMembersForUser(teamID, userID string, page, perPage int) ([]*model.ChannelMember, *model.AppError) GetChannelMembersForUser returns all channel memberships on a team for a user. @tag Channel @tag User Minimum server version: 5.10 (API) UpdateChannelMemberRoles UpdateChannelMemberRoles(channelId, userID, newRoles string) (*model.ChannelMember, *model.AppError) UpdateChannelMemberRoles updates a user's roles for a channel. @tag Channel @tag User Minimum server version: 5.2 (API) UpdateChannelMemberNotifications UpdateChannelMemberNotifications(channelId, userID string, notifications map[string]string) (*model.ChannelMember, *model.AppError) UpdateChannelMemberNotifications updates a user's notification properties for a channel. @tag Channel @tag User Minimum server version: 5.2 (API) PatchChannelMembersNotifications PatchChannelMembersNotifications(members []*model.ChannelMemberIdentifier, notifyProps map[string]string) *model.AppError PatchChannelMembersNotifications updates the notification properties for multiple channel members. Other changes made to the channel memberships will be ignored. A maximum of 200 members can be updated at once. @tag Channel @tag User Minimum server version: 9.5 (API) GetGroup GetGroup(groupId string) (*model.Group, *model.AppError) GetGroup gets a group by ID. @tag Group Minimum server version: 5.18 (API) GetGroupByName GetGroupByName(name string) (*model.Group, *model.AppError) GetGroupByName gets a group by name. @tag Group Minimum server version: 5.18 (API) GetGroupMemberUsers GetGroupMemberUsers(groupID string, page, perPage int) ([]*model.User, *model.AppError) GetGroupMemberUsers gets a page of users belonging to the given group. @tag Group Minimum server version: 5.35 (API) GetGroupsBySource GetGroupsBySource(groupSource model.GroupSource) ([]*model.Group, *model.AppError) GetGroupsBySource gets a list of all groups for the given source. @tag Group Minimum server version: 5.35 (API) GetGroupsForUser GetGroupsForUser(userID string) ([]*model.Group, *model.AppError) GetGroupsForUser gets the groups a user is in. @tag Group @tag User Minimum server version: 5.18 (API) DeleteChannelMember DeleteChannelMember(channelId, userID string) *model.AppError DeleteChannelMember deletes a channel membership for a user. @tag Channel @tag User Minimum server version: 5.2 (API) CreatePost CreatePost(post *model.Post) (*model.Post, *model.AppError) CreatePost creates a post. @tag Post Minimum server version: 5.2 (API) AddReaction AddReaction(reaction *model.Reaction) (*model.Reaction, *model.AppError) AddReaction add a reaction to a post. @tag Post Minimum server version: 5.3 (API) RemoveReaction RemoveReaction(reaction *model.Reaction) *model.AppError RemoveReaction remove a reaction from a post. @tag Post Minimum server version: 5.3 (API) GetReactions GetReactions(postId string) ([]*model.Reaction, *model.AppError) GetReaction get the reactions of a post. @tag Post Minimum server version: 5.3 (API) SendEphemeralPost SendEphemeralPost(userID string, post *model.Post) *model.Post SendEphemeralPost creates an ephemeral post. @tag Post Minimum server version: 5.2 (API) UpdateEphemeralPost UpdateEphemeralPost(userID string, post *model.Post) *model.Post UpdateEphemeralPost updates an ephemeral message previously sent to the user. EXPERIMENTAL: This API is experimental and can be changed without advance notice. @tag Post Minimum server version: 5.2 (API) DeleteEphemeralPost DeleteEphemeralPost(userID, postId string) DeleteEphemeralPost deletes an ephemeral message previously sent to the user. EXPERIMENTAL: This API is experimental and can be changed without advance notice. @tag Post Minimum server version: 5.2 (API) DeletePost DeletePost(postId string) *model.AppError DeletePost deletes a post. @tag Post Minimum server version: 5.2 (API) GetPostThread GetPostThread(postId string) (*model.PostList, *model.AppError) GetPostThread gets a post with all the other posts in the same thread. @tag Post Minimum server version: 5.6 (API) GetPost GetPost(postId string) (*model.Post, *model.AppError) GetPost gets a post. @tag Post Minimum server version: 5.2 (API) GetPostsSince GetPostsSince(channelId string, time int64) (*model.PostList, *model.AppError) GetPostsSince gets posts created after a specified time as Unix time in milliseconds. @tag Post @tag Channel Minimum server version: 5.6 (API) GetPostsAfter GetPostsAfter(channelId, postId string, page, perPage int) (*model.PostList, *model.AppError) GetPostsAfter gets a page of posts that were posted after the post provided. @tag Post @tag Channel Minimum server version: 5.6 (API) GetPostsBefore GetPostsBefore(channelId, postId string, page, perPage int) (*model.PostList, *model.AppError) GetPostsBefore gets a page of posts that were posted before the post provided. @tag Post @tag Channel Minimum server version: 5.6 (API) GetPostsForChannel GetPostsForChannel(channelId string, page, perPage int) (*model.PostList, *model.AppError) GetPostsForChannel gets a list of posts for a channel. @tag Post @tag Channel Minimum server version: 5.6 (API) GetTeamStats GetTeamStats(teamID string) (*model.TeamStats, *model.AppError) GetTeamStats gets a team's statistics @tag Team Minimum server version: 5.8 (API) UpdatePost UpdatePost(post *model.Post) (*model.Post, *model.AppError) UpdatePost updates a post. @tag Post Minimum server version: 5.2 (API) GetProfileImage GetProfileImage(userID string) ([]byte, *model.AppError) GetProfileImage gets user's profile image. @tag User Minimum server version: 5.6 (API) SetProfileImage SetProfileImage(userID string, data []byte) *model.AppError SetProfileImage sets a user's profile image. @tag User Minimum server version: 5.6 (API) GetEmojiList GetEmojiList(sortBy string, page, perPage int) ([]*model.Emoji, *model.AppError) GetEmojiList returns a page of custom emoji on the system. The sortBy parameter can be: \"name\". @tag Emoji Minimum server version: 5.6 (API) GetEmojiByName GetEmojiByName(name string) (*model.Emoji, *model.AppError) GetEmojiByName gets an emoji by it's name. @tag Emoji Minimum server version: 5.6 (API) GetEmoji GetEmoji(emojiId string) (*model.Emoji, *model.AppError) GetEmoji returns a custom emoji based on the emojiId string. @tag Emoji Minimum server version: 5.6 (API) CopyFileInfos CopyFileInfos(userID string, fileIds []string) ([]string, *model.AppError) CopyFileInfos duplicates the FileInfo objects referenced by the given file ids, recording the given user id as the new creator and returning the new set of file ids. The duplicate FileInfo objects are not initially linked to a post, but may now be passed to CreatePost. Use this API to duplicate a post and its file attachments without actually duplicating the uploaded files. @tag File @tag User Minimum server version: 5.2 (API) GetFileInfo GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) GetFileInfo gets a File Info for a specific fileId @tag File Minimum server version: 5.3 (API) SetFileSearchableContent SetFileSearchableContent(fileID string, content string) *model.AppError SetFileSearchableContent update the File Info searchable text for full text search @tag File Minimum server version: 9.1 (API) GetFileInfos GetFileInfos(page, perPage int, opt *model.GetFileInfosOptions) ([]*model.FileInfo, *model.AppError) GetFileInfos gets File Infos with options @tag File Minimum server version: 5.22 (API) GetFile GetFile(fileId string) ([]byte, *model.AppError) GetFile gets content of a file by it's ID @tag File Minimum server version: 5.8 (API) GetFileLink GetFileLink(fileId string) (string, *model.AppError) GetFileLink gets the public link to a file by fileId. @tag File Minimum server version: 5.6 (API) ReadFile ReadFile(path string) ([]byte, *model.AppError) ReadFile reads the file from the backend for a specific path @tag File Minimum server version: 5.3 (API) GetEmojiImage GetEmojiImage(emojiId string) ([]byte, string, *model.AppError) GetEmojiImage returns the emoji image. @tag Emoji Minimum server version: 5.6 (API) UploadFile UploadFile(data []byte, channelId string, filename string) (*model.FileInfo, *model.AppError) UploadFile will upload a file to a channel using a multipart request, to be later attached to a post. @tag File @tag Channel Minimum server version: 5.6 (API) OpenInteractiveDialog OpenInteractiveDialog(dialog model.OpenDialogRequest) *model.AppError OpenInteractiveDialog will open an interactive dialog on a user's client that generated the trigger ID. Used with interactive message buttons, menus and slash commands. Minimum server version: 5.6 (API) GetPlugins GetPlugins() ([]*model.Manifest, *model.AppError) GetPlugins will return a list of plugin manifests for currently active plugins. @tag Plugin Minimum server version: 5.6 (API) EnablePlugin EnablePlugin(id string) *model.AppError EnablePlugin will enable an plugin installed. @tag Plugin Minimum server version: 5.6 (API) DisablePlugin DisablePlugin(id string) *model.AppError DisablePlugin will disable an enabled plugin. @tag Plugin Minimum server version: 5.6 (API) RemovePlugin RemovePlugin(id string) *model.AppError RemovePlugin will disable and delete a plugin. @tag Plugin Minimum server version: 5.6 (API) GetPluginStatus GetPluginStatus(id string) (*model.PluginStatus, *model.AppError) GetPluginStatus will return the status of a plugin. @tag Plugin Minimum server version: 5.6 (API) InstallPlugin InstallPlugin(file io.Reader, replace bool) (*model.Manifest, *model.AppError) InstallPlugin will upload another plugin with tar.gz file. Previous version will be replaced on replace true. @tag Plugin Minimum server version: 5.18 (API) KVSet KVSet(key string, value []byte) *model.AppError KVSet stores a key-value pair, unique per plugin. Provided helper functions and internal plugin code will use the prefix `mmi_` before keys. Do not use this prefix. @tag KeyValueStore Minimum server version: 5.2 (API) KVCompareAndSet KVCompareAndSet(key string, oldValue, newValue []byte) (bool, *model.AppError) KVCompareAndSet updates a key-value pair, unique per plugin, but only if the current value matches the given oldValue. Inserts a new key if oldValue == nil. Returns (false, err) if DB error occurred Returns (false, nil) if current value != oldValue or key already exists when inserting Returns (true, nil) if current value == oldValue or new key is inserted @tag KeyValueStore Minimum server version: 5.12 (API) KVCompareAndDelete KVCompareAndDelete(key string, oldValue []byte) (bool, *model.AppError) KVCompareAndDelete deletes a key-value pair, unique per plugin, but only if the current value matches the given oldValue. Returns (false, err) if DB error occurred Returns (false, nil) if current value != oldValue or key does not exist when deleting Returns (true, nil) if current value == oldValue and the key was deleted @tag KeyValueStore Minimum server version: 5.16 (API) KVSetWithOptions KVSetWithOptions(key string, value []byte, options model.PluginKVSetOptions) (bool, *model.AppError) KVSetWithOptions stores a key-value pair, unique per plugin, according to the given options. Returns (false, err) if DB error occurred Returns (false, nil) if the value was not set Returns (true, nil) if the value was set Minimum server version: 5.20 (API) KVSetWithExpiry KVSetWithExpiry(key string, value []byte, expireInSeconds int64) *model.AppError KVSet stores a key-value pair with an expiry time, unique per plugin. @tag KeyValueStore Minimum server version: 5.6 (API) KVGet KVGet(key string) ([]byte, *model.AppError) KVGet retrieves a value based on the key, unique per plugin. Returns nil for non-existent keys. @tag KeyValueStore Minimum server version: 5.2 (API) KVDelete KVDelete(key string) *model.AppError KVDelete removes a key-value pair, unique per plugin. Returns nil for non-existent keys. @tag KeyValueStore Minimum server version: 5.2 (API) KVDeleteAll KVDeleteAll() *model.AppError KVDeleteAll removes all key-value pairs for a plugin. @tag KeyValueStore Minimum server version: 5.6 (API) KVList KVList(page, perPage int) ([]string, *model.AppError) KVList lists all keys for a plugin. @tag KeyValueStore Minimum server version: 5.6 (API) PublishWebSocketEvent PublishWebSocketEvent(event string, payload map[string]any, broadcast *model.WebsocketBroadcast) PublishWebSocketEvent sends an event to WebSocket connections. event is the type and will be prepended with \"custom_\u003cpluginid\u003e_\". payload is the data sent with the event. Interface values must be primitive Go types or mattermost-server/model types. broadcast determines to which users to send the event. Minimum server version: 5.2 (API) HasPermissionTo HasPermissionTo(userID string, permission *model.Permission) bool HasPermissionTo check if the user has the permission at system scope. @tag User Minimum server version: 5.3 (API) HasPermissionToTeam HasPermissionToTeam(userID, teamID string, permission *model.Permission) bool HasPermissionToTeam check if the user has the permission at team scope. @tag User @tag Team Minimum server version: 5.3 (API) HasPermissionToChannel HasPermissionToChannel(userID, channelId string, permission *model.Permission) bool HasPermissionToChannel check if the user has the permission at channel scope. @tag User @tag Channel Minimum server version: 5.3 (API) RolesGrantPermission RolesGrantPermission(roleNames []string, permissionId string) bool RolesGrantPermission check if the specified roles grant the specified permission Minimum server version: 6.3 (API) LogDebug LogDebug(msg string, keyValuePairs ...any) LogDebug writes a log message to the Mattermost server log file. Appropriate context such as the plugin name will already be added as fields so plugins do not need to add that info. @tag Logging Minimum server version: 5.2 (API) LogInfo LogInfo(msg string, keyValuePairs ...any) LogInfo writes a log message to the Mattermost server log file. Appropriate context such as the plugin name will already be added as fields so plugins do not need to add that info. @tag Logging Minimum server version: 5.2 (API) LogError LogError(msg string, keyValuePairs ...any) LogError writes a log message to the Mattermost server log file. Appropriate context such as the plugin name will already be added as fields so plugins do not need to add that info. @tag Logging Minimum server version: 5.2 (API) LogWarn LogWarn(msg string, keyValuePairs ...any) LogWarn writes a log message to the Mattermost server log file. Appropriate context such as the plugin name will already be added as fields so plugins do not need to add that info. @tag Logging Minimum server version: 5.2 (API) SendMail SendMail(to, subject, htmlBody string) *model.AppError SendMail sends an email to a specific address Minimum server version: 5.7 (API) CreateBot CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) CreateBot creates the given bot and corresponding user. @tag Bot Minimum server version: 5.10 (API) PatchBot PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) PatchBot applies the given patch to the bot and corresponding user. @tag Bot Minimum server version: 5.10 (API) GetBot GetBot(botUserId string, includeDeleted bool) (*model.Bot, *model.AppError) GetBot returns the given bot. @tag Bot Minimum server version: 5.10 (API) GetBots GetBots(options *model.BotGetOptions) ([]*model.Bot, *model.AppError) GetBots returns the requested page of bots. @tag Bot Minimum server version: 5.10 (API) UpdateBotActive UpdateBotActive(botUserId string, active bool) (*model.Bot, *model.AppError) UpdateBotActive marks a bot as active or inactive, along with its corresponding user. @tag Bot Minimum server version: 5.10 (API) PermanentDeleteBot PermanentDeleteBot(botUserId string) *model.AppError PermanentDeleteBot permanently deletes a bot and its corresponding user. @tag Bot Minimum server version: 5.10 (API) PluginHTTP PluginHTTP(request *http.Request) *http.Response PluginHTTP allows inter-plugin requests to plugin APIs. Minimum server version: 5.18 (API) PublishUserTyping PublishUserTyping(userID, channelId, parentId string) *model.AppError PublishUserTyping publishes a user is typing WebSocket event. The parentId parameter may be an empty string, the other parameters are required. @tag User Minimum server version: 5.26 (API) CreateCommand CreateCommand(cmd *model.Command) (*model.Command, error) CreateCommand creates a server-owned slash command that is not handled by the plugin itself, and which will persist past the life of the plugin. The command will have its CreatorId set to \"\" and its PluginId set to the id of the plugin that created it. @tag SlashCommand Minimum server version: 5.28 (API) ListCommands ListCommands(teamID string) ([]*model.Command, error) ListCommands returns the list of all slash commands for teamID. E.g., custom commands (those created through the integrations menu, the REST api, or the plugin api CreateCommand), plugin commands (those created with plugin api RegisterCommand), and builtin commands (those added internally through RegisterCommandProvider). @tag SlashCommand Minimum server version: 5.28 (API) ListCustomCommands ListCustomCommands(teamID string) ([]*model.Command, error) ListCustomCommands returns the list of slash commands for teamID that where created through the integrations menu, the REST api, or the plugin api CreateCommand. @tag SlashCommand Minimum server version: 5.28 (API) ListPluginCommands ListPluginCommands(teamID string) ([]*model.Command, error) ListPluginCommands returns the list of slash commands for teamID that were created with the plugin api RegisterCommand. @tag SlashCommand Minimum server version: 5.28 (API) ListBuiltInCommands ListBuiltInCommands() ([]*model.Command, error) ListBuiltInCommands returns the list of slash commands that are builtin commands (those added internally through RegisterCommandProvider). @tag SlashCommand Minimum server version: 5.28 (API) GetCommand GetCommand(commandID string) (*model.Command, error) GetCommand returns the command definition based on a command id string. @tag SlashCommand Minimum server version: 5.28 (API) UpdateCommand UpdateCommand(commandID string, updatedCmd *model.Command) (*model.Command, error) UpdateCommand updates a single command (commandID) with the information provided in the updatedCmd model.Command struct. The following fields in the command cannot be updated: Id, Token, CreateAt, DeleteAt, and PluginId. If updatedCmd.TeamId is blank, it will be set to commandID's TeamId. @tag SlashCommand Minimum server version: 5.28 (API) DeleteCommand DeleteCommand(commandID string) error DeleteCommand deletes a slash command (commandID). @tag SlashCommand Minimum server version: 5.28 (API) CreateOAuthApp CreateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) CreateOAuthApp creates a new OAuth App. @tag OAuth Minimum server version: 5.38 (API) GetOAuthApp GetOAuthApp(appID string) (*model.OAuthApp, *model.AppError) GetOAuthApp gets an existing OAuth App by id. @tag OAuth Minimum server version: 5.38 (API) UpdateOAuthApp UpdateOAuthApp(app *model.OAuthApp) (*model.OAuthApp, *model.AppError) UpdateOAuthApp updates an existing OAuth App. @tag OAuth Minimum server version: 5.38 (API) DeleteOAuthApp DeleteOAuthApp(appID string) *model.AppError DeleteOAuthApp deletes an existing OAuth App by id. @tag OAuth Minimum server version: 5.38 (API) PublishPluginClusterEvent PublishPluginClusterEvent(ev model.PluginClusterEvent, opts model.PluginClusterEventSendOptions) error PublishPluginClusterEvent broadcasts a plugin event to all other running instances of the calling plugin that are present in the cluster. This method is used to allow plugin communication in a High-Availability cluster. The receiving side should implement the OnPluginClusterEvent hook to receive events sent through this method. Minimum server version: 5.36 (API) RequestTrialLicense RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) *model.AppError RequestTrialLicense requests a trial license and installs it in the server Minimum server version: 5.36 (API) GetCloudLimits GetCloudLimits() (*model.ProductLimits, error) GetCloudLimits gets limits associated with a cloud workspace, if any Minimum server version: 7.0 (API) EnsureBotUser EnsureBotUser(bot *model.Bot) (string, error) EnsureBotUser updates the bot if it exists, otherwise creates it. Minimum server version: 7.1 (API) RegisterCollectionAndTopic RegisterCollectionAndTopic(collectionType, topicType string) error RegisterCollectionAndTopic is no longer supported. Minimum server version: 7.6 (API) CreateUploadSession CreateUploadSession(us *model.UploadSession) (*model.UploadSession, error) CreateUploadSession creates and returns a new (resumable) upload session. @tag Upload Minimum server version: 7.6 (API) UploadData UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, error) UploadData uploads the data for a given upload session. @tag Upload Minimum server version: 7.6 (API) GetUploadSession GetUploadSession(uploadID string) (*model.UploadSession, error) GetUploadSession returns the upload session for the provided id. @tag Upload Minimum server version: 7.6 (API) SendPushNotification SendPushNotification(notification *model.PushNotification, userID string) *model.AppError SendPushNotification will send a push notification to all of user's sessions. It is the responsibility of the plugin to respect the server's configuration and licence, especially related to `cfg.EmailSettings.PushNotificationContents`, particularly `model.IdLoadedNotification` and the generic settings. Refer to `app.sendPushNotificationSync` for the logic used to construct push notifications. Note: the NotificationWillBePushed hook will be run after SendPushNotification is called. Minimum server version: 9.0 (API) UpdateUserAuth UpdateUserAuth(userID string, userAuth *model.UserAuth) (*model.UserAuth, *model.AppError) UpdateUserAuth updates a user's auth data. It is not currently possible to use this to set a user's auth to e-mail with a hashed password. It is meant to be used exclusively in setting a non-email auth service. @tag User Minimum server version: 9.3 (API) RegisterPluginForSharedChannels RegisterPluginForSharedChannels(opts model.RegisterPluginOpts) (remoteID string, err error) RegisterPluginForSharedChannels registers the plugin as a `Remote` for SharedChannels. The plugin will receive synchronization messages via the `OnSharedChannelsSyncMsg` hook. This API is idempotent - when called repeatedly with the same `RegisterPluginOpts.PluginID` it will return the same remoteID. @tag SharedChannels Minimum server version: 9.5 (API) UnregisterPluginForSharedChannels UnregisterPluginForSharedChannels(pluginID string) error UnregisterPluginForSharedChannels unregisters the plugin as a `Remote` for SharedChannels. The plugin will no longer receive synchronization messages via the `OnSharedChannelsSyncMsg` hook. @tag SharedChannels Minimum server version: 9.5 (API) ShareChannel ShareChannel(sc *model.SharedChannel) (*model.SharedChannel, error) ShareChannel marks a channel for sharing via shared channels. Note, this does not automatically invite any remote clusters to the channel - use `InviteRemote` to invite a remote , or this plugin, to the shared channel and start synchronization. @tag SharedChannels Minimum server version: 9.5 (API) UpdateSharedChannel UpdateSharedChannel(sc *model.SharedChannel) (*model.SharedChannel, error) UpdateSharedChannel updates a shared channel. This can be used to change the share name, display name, purpose, header, etc. @tag SharedChannels Minimum server version: 9.5 (API) UnshareChannel UnshareChannel(channelID string) (unshared bool, err error) UnshareChannel unmarks a channel for sharing. The channel will no longer be shared and all remotes will be uninvited to the channel. @tag SharedChannels Minimum server version: 9.5 (API) UpdateSharedChannelCursor UpdateSharedChannelCursor(channelID, remoteID string, cusror model.GetPostsSinceForSyncCursor) error UpdateSharedChannelCursor updates the cursor for the specified channel and RemoteID (passed by the plugin when registering). This can be used to manually set the point of last sync, either forward to skip older posts, or backward to re-sync history. This call by itself does not force a re-sync - a change to channel contents or a call to SyncSharedChannel are needed to force a sync. @tag SharedChannels Minimum server version: 9.5 (API) SyncSharedChannel SyncSharedChannel(channelID string) error SyncSharedChannel forces a shared channel to send any changed content to all remotes. @tag SharedChannels Minimum server version: 9.5 (API) InviteRemoteToChannel InviteRemoteToChannel(channelID string, remoteID string, userID string, shareIfNotShared bool) error InviteRemoteToChannel invites a remote, or this plugin, as a target for synchronizing. Once invited, the remote will start to receive synchronization messages for any changed content in the specified channel. If `shareIfNotShared` is true, the channel's shared flag will be set, if not already. @tag SharedChannels Minimum server version: 9.5 (API) UninviteRemoteFromChannel UninviteRemoteFromChannel(channelID string, remoteID string) error UninviteRemoteFromChannel uninvites a remote, or this plugin, such that it will stop receiving sychronization messages for the channel. @tag SharedChannels Minimum server version: 9.5 (API) UpsertGroupMember UpsertGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) UpsertGroupMember adds a user to a group or updates their existing membership. @tag Group @tag User Minimum server version: 10.7 (API) UpsertGroupMembers UpsertGroupMembers(groupID string, userIDs []string) ([]*model.GroupMember, *model.AppError) UpsertGroupMembers adds multiple users to a group or updates their existing memberships. @tag Group @tag User Minimum server version: 10.7 (API) GetGroupByRemoteID GetGroupByRemoteID(remoteID string, groupSource model.GroupSource) (*model.Group, *model.AppError) GetGroupByRemoteID gets a group by its remote ID. @tag Group Minimum server version: 10.7 (API) CreateGroup CreateGroup(group *model.Group) (*model.Group, *model.AppError) CreateGroup creates a new group. @tag Group Minimum server version: 10.7 (API) UpdateGroup UpdateGroup(group *model.Group) (*model.Group, *model.AppError) UpdateGroup updates a group. @tag Group Minimum server version: 10.7 (API) DeleteGroup DeleteGroup(groupID string) (*model.Group, *model.AppError) DeleteGroup soft deletes a group. @tag Group Minimum server version: 10.7 (API) RestoreGroup RestoreGroup(groupID string) (*model.Group, *model.AppError) RestoreGroup restores a soft deleted group. @tag Group Minimum server version: 10.7 (API) DeleteGroupMember DeleteGroupMember(groupID string, userID string) (*model.GroupMember, *model.AppError) DeleteGroupMember removes a user from a group. @tag Group @tag User Minimum server version: 10.7 (API) GetGroupSyncable GetGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) GetGroupSyncable gets a group syncable. @tag Group Minimum server version: 10.7 (API) GetGroupSyncables GetGroupSyncables(groupID string, syncableType model.GroupSyncableType) ([]*model.GroupSyncable, *model.AppError) GetGroupSyncables gets all group syncables for the given group. @tag Group Minimum server version: 10.7 (API) UpsertGroupSyncable UpsertGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) UpsertGroupSyncable creates or updates a group syncable. @tag Group Minimum server version: 10.7 (API) UpdateGroupSyncable UpdateGroupSyncable(groupSyncable *model.GroupSyncable) (*model.GroupSyncable, *model.AppError) UpdateGroupSyncable updates a group syncable. @tag Group Minimum server version: 10.7 (API) DeleteGroupSyncable DeleteGroupSyncable(groupID string, syncableID string, syncableType model.GroupSyncableType) (*model.GroupSyncable, *model.AppError) DeleteGroupSyncable deletes a group syncable. @tag Group Minimum server version: 10.7 (API) UpdateUserRoles UpdateUserRoles(userID, newRoles string) (*model.User, *model.AppError) UpdateUserRoles updates the role for a user. @tag Team @tag User Minimum server version: 9.8 (API) GetPluginID GetPluginID() string GetPluginID returns the plugin ID. @tag Plugin Minimum server version: 10.1 (API) GetGroups GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, *model.AppError) GetGroups returns a list of all groups with the given options and restrictions. @tag Group Minimum server version: 10.7 (API) CreateDefaultSyncableMemberships CreateDefaultSyncableMemberships(params model.CreateDefaultMembershipParams) *model.AppError CreateDefaultSyncableMemberships creates default syncable memberships based off the provided parameters. @tag Group Minimum server version: 10.9 (API) DeleteGroupConstrainedMemberships DeleteGroupConstrainedMemberships() *model.AppError DeleteGroupConstrainedMemberships deletes team and channel memberships of users who aren't members of the allowed groups of all group-constrained teams and channels. @tag Group Minimum server version: 10.9 (API) CreatePropertyField CreatePropertyField(field *model.PropertyField) (*model.PropertyField, error) CreatePropertyField creates a new property field. @tag PropertyField Minimum server version: 10.10 (API) GetPropertyField GetPropertyField(groupID, fieldID string) (*model.PropertyField, error) GetPropertyField gets a property field by groupID and fieldID. @tag PropertyField Minimum server version: 10.10 (API) GetPropertyFields GetPropertyFields(groupID string, ids []string) ([]*model.PropertyField, error) GetPropertyFields gets multiple property fields by groupID and a list of IDs. @tag PropertyField Minimum server version: 10.10 (API) UpdatePropertyField UpdatePropertyField(groupID string, field *model.PropertyField) (*model.PropertyField, error) UpdatePropertyField updates an existing property field. @tag PropertyField Minimum server version: 10.10 (API) DeletePropertyField DeletePropertyField(groupID, fieldID string) error DeletePropertyField deletes a property field (soft delete). @tag PropertyField Minimum server version: 10.10 (API) SearchPropertyFields SearchPropertyFields(groupID, targetID string, opts model.PropertyFieldSearchOpts) ([]*model.PropertyField, error) SearchPropertyFields searches for property fields with filtering options. @tag PropertyField Minimum server version: 10.10 (API) CreatePropertyValue CreatePropertyValue(value *model.PropertyValue) (*model.PropertyValue, error) CreatePropertyValue creates a new property value. @tag PropertyValue Minimum server version: 10.10 (API) GetPropertyValue GetPropertyValue(groupID, valueID string) (*model.PropertyValue, error) GetPropertyValue gets a property value by groupID and valueID. @tag PropertyValue Minimum server version: 10.10 (API) GetPropertyValues GetPropertyValues(groupID string, ids []string) ([]*model.PropertyValue, error) GetPropertyValues gets multiple property values by groupID and a list of IDs. @tag PropertyValue Minimum server version: 10.10 (API) UpdatePropertyValue UpdatePropertyValue(groupID string, value *model.PropertyValue) (*model.PropertyValue, error) UpdatePropertyValue updates an existing property value. @tag PropertyValue Minimum server version: 10.10 (API) UpsertPropertyValue UpsertPropertyValue(value *model.PropertyValue) (*model.PropertyValue, error) UpsertPropertyValue creates a new property value or updates if it already exists. @tag PropertyValue Minimum server version: 10.10 (API) DeletePropertyValue DeletePropertyValue(groupID, valueID string) error DeletePropertyValue deletes a property value (soft delete). @tag PropertyValue Minimum server version: 10.10 (API) SearchPropertyValues SearchPropertyValues(groupID, targetID string, opts model.PropertyValueSearchOpts) ([]*model.PropertyValue, error) SearchPropertyValues searches for property values with filtering options. @tag PropertyValue Minimum server version: 10.10 (API) RegisterPropertyGroup RegisterPropertyGroup(name string) (*model.PropertyGroup, error) RegisterPropertyGroup registers a new property group. @tag PropertyGroup Minimum server version: 10.10 (API) GetPropertyGroup GetPropertyGroup(name string) (*model.PropertyGroup, error) GetPropertyGroup gets a property group by name. @tag PropertyGroup Minimum server version: 10.10 (API) GetPropertyFieldByName GetPropertyFieldByName(groupID, targetID, name string) (*model.PropertyField, error) GetPropertyFieldByName gets a property field by groupID, targetID and name. @tag PropertyField Minimum server version: 10.10 (API) UpdatePropertyFields UpdatePropertyFields(groupID string, fields []*model.PropertyField) ([]*model.PropertyField, error) UpdatePropertyFields updates multiple property fields in a single operation. @tag PropertyField Minimum server version: 10.10 (API) UpdatePropertyValues UpdatePropertyValues(groupID string, values []*model.PropertyValue) ([]*model.PropertyValue, error) UpdatePropertyValues updates multiple property values in a single operation. @tag PropertyValue Minimum server version: 10.10 (API) UpsertPropertyValues UpsertPropertyValues(values []*model.PropertyValue) ([]*model.PropertyValue, error) UpsertPropertyValues creates or updates multiple property values in a single operation. @tag PropertyValue Minimum server version: 10.10 (API) DeletePropertyValuesForTarget DeletePropertyValuesForTarget(groupID, targetType, targetID string) error DeletePropertyValuesForTarget deletes all property values for a specific target. @tag PropertyValue Minimum server version: 10.10 (API) DeletePropertyValuesForField DeletePropertyValuesForField(groupID, fieldID string) error DeletePropertyValuesForField deletes all property values for a specific field. @tag PropertyValue Minimum server version: 10.10 (API) LogAuditRec LogAuditRec(rec *model.AuditRecord) LogAuditRec logs an audit record using the default audit logger. @tag Audit Minimum server version: 10.10 (API) LogAuditRecWithLevel LogAuditRecWithLevel(rec *model.AuditRecord, level mlog.Level) LogAuditRecWithLevel logs an audit record with a specific log level. @tag Audit Minimum server version: 10.10 Hooks (Hooks) OnActivate OnActivate() error 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 (Hooks) Implemented Implemented() ([]string, 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 (Hooks) OnDeactivate OnDeactivate() 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 (Hooks) OnConfigurationChange OnConfigurationChange() 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 (Hooks) ServeHTTP ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) 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 (Hooks) ExecuteCommand ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) ExecuteCommand executes a command that has been previously registered via the RegisterCommand API. Minimum server version: 5.2 (Hooks) UserHasBeenCreated UserHasBeenCreated(c *plugin.Context, user *model.User) UserHasBeenCreated is invoked after a user was created. Minimum server version: 5.10 (Hooks) UserWillLogIn UserWillLogIn(c *plugin.Context, user *model.User) string 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 (Hooks) UserHasLoggedIn UserHasLoggedIn(c *plugin.Context, user *model.User) UserHasLoggedIn is invoked after a user has logged in. Minimum server version: 5.2 (Hooks) MessageWillBePosted MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) 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 (Hooks) MessageWillBeUpdated MessageWillBeUpdated(c *plugin.Context, newPost, oldPost *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 (Hooks) MessageHasBeenPosted MessageHasBeenPosted(c *plugin.Context, post *model.Post) 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 (Hooks) MessageHasBeenUpdated MessageHasBeenUpdated(c *plugin.Context, newPost, oldPost *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 (Hooks) MessagesWillBeConsumed MessagesWillBeConsumed(posts []*model.Post) []*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 (Hooks) MessageHasBeenDeleted MessageHasBeenDeleted(c *plugin.Context, 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 (Hooks) ChannelHasBeenCreated ChannelHasBeenCreated(c *plugin.Context, channel *model.Channel) ChannelHasBeenCreated is invoked after the channel has been committed to the database. Minimum server version: 5.2 (Hooks) UserHasJoinedChannel UserHasJoinedChannel(c *plugin.Context, channelMember *model.ChannelMember, actor *model.User) 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 (Hooks) UserHasLeftChannel UserHasLeftChannel(c *plugin.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 (Hooks) UserHasJoinedTeam UserHasJoinedTeam(c *plugin.Context, teamMember *model.TeamMember, 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 (Hooks) UserHasLeftTeam UserHasLeftTeam(c *plugin.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 (Hooks) FileWillBeUploaded FileWillBeUploaded(c *plugin.Context, info *model.FileInfo, file io.Reader, output io.Writer) (*model.FileInfo, string) 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 (Hooks) ReactionHasBeenAdded ReactionHasBeenAdded(c *plugin.Context, reaction *model.Reaction) 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 (Hooks) ReactionHasBeenRemoved ReactionHasBeenRemoved(c *plugin.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 (Hooks) OnPluginClusterEvent OnPluginClusterEvent(c *plugin.Context, ev model.PluginClusterEvent) 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 (Hooks) OnWebSocketConnect OnWebSocketConnect(webConnID, userID string) 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 (Hooks) OnWebSocketDisconnect OnWebSocketDisconnect(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 (Hooks) WebSocketMessageHasBeenPosted WebSocketMessageHasBeenPosted(webConnID, userID string, req *model.WebSocketRequest) WebSocketMessageHasBeenPosted is invoked when a websocket message is received. Minimum server version: 6.0 (Hooks) RunDataRetention RunDataRetention(nowTime, batchSize int64) (int64, error) RunDataRetention is invoked during a DataRetentionJob. Minimum server version: 6.4 (Hooks) OnInstall OnInstall(c *plugin.Context, event model.OnInstallEvent) 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 (Hooks) OnSendDailyTelemetry OnSendDailyTelemetry() OnSendDailyTelemetry is invoked when the server send the daily telemetry data. Minimum server version: 6.5 (Hooks) OnCloudLimitsUpdated OnCloudLimitsUpdated(limits *model.ProductLimits) OnCloudLimitsUpdated is invoked product limits change, for example when plan tiers change Minimum server version: 7.0 (Hooks) ConfigurationWillBeSaved ConfigurationWillBeSaved(newCfg *model.Config) (*model.Config, error) 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 (Hooks) NotificationWillBePushed NotificationWillBePushed(pushNotification *model.PushNotification, userID string) (*model.PushNotification, 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 (Hooks) UserHasBeenDeactivated UserHasBeenDeactivated(c *plugin.Context, user *model.User) UserHasBeenDeactivated is invoked when a user is deactivated. Minimum server version: 9.1 (Hooks) ServeMetrics ServeMetrics(c *plugin.Context, w http.ResponseWriter, r *http.Request) 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 (Hooks) OnSharedChannelsSyncMsg OnSharedChannelsSyncMsg(msg *model.SyncMsg, rc *model.RemoteCluster) (model.SyncResponse, error) 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 (Hooks) OnSharedChannelsPing OnSharedChannelsPing(rc *model.RemoteCluster) bool 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 (Hooks) PreferencesHaveChanged PreferencesHaveChanged(c *plugin.Context, preferences []model.Preference) 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 (Hooks) OnSharedChannelsAttachmentSyncMsg OnSharedChannelsAttachmentSyncMsg(fi *model.FileInfo, post *model.Post, rc *model.RemoteCluster) error 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 (Hooks) OnSharedChannelsProfileImageSyncMsg OnSharedChannelsProfileImageSyncMsg(user *model.User, 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 (Hooks) GenerateSupportData GenerateSupportData(c *plugin.Context) ([]*model.FileData, 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 (Hooks) OnSAMLLogin OnSAMLLogin(c *plugin.Context, user *model.User, assertion *gosaml2.AssertionInfo) error OnSAMLLogin is invoked after a successful SAML login. Minimum server version: 10.7 Helpers Examples (Example) HelloWorld This example demonstrates a plugin that handles HTTP requests which respond by greeting the world. package main import ( \"fmt\" \"net/http\" \"github.com/mattermost/mattermost/server/public/plugin\" ) // HelloWorldPlugin implements the interface expected by the Mattermost server to communicate // between the server and plugin processes. type HelloWorldPlugin struct { plugin.MattermostPlugin } // ServeHTTP demonstrates a plugin that handles HTTP requests by greeting the world. func (p *HelloWorldPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, \"Hello, world!\") } // This example demonstrates a plugin that handles HTTP requests which respond by greeting the // world. func main() { plugin.ClientMain(\u0026HelloWorldPlugin{}) } (Example) HelpPlugin package main import ( \"strings\" \"sync\" \"github.com/pkg/errors\" \"github.com/mattermost/mattermost/server/public/model\" \"github.com/mattermost/mattermost/server/public/plugin\" ) // configuration represents the configuration for this plugin as exposed via the Mattermost // server configuration. type configuration struct { TeamName\tstring ChannelName\tstring // channelID is resolved when the public configuration fields above change channelID\tstring } // HelpPlugin implements the interface expected by the Mattermost server to communicate // between the server and plugin processes. type HelpPlugin struct { plugin.MattermostPlugin // configurationLock synchronizes access to the configuration. configurationLock\tsync.RWMutex // configuration is the active plugin configuration. Consult getConfiguration and // setConfiguration for usage. configuration\t*configuration } // getConfiguration retrieves the active configuration under lock, making it safe to use // concurrently. The active configuration may change underneath the client of this method, but // the struct returned by this API call is considered immutable. func (p *HelpPlugin) getConfiguration() *configuration { p.configurationLock.RLock() defer p.configurationLock.RUnlock() if p.configuration == nil { return \u0026configuration{} } return p.configuration } // setConfiguration replaces the active configuration under lock. // // Do not call setConfiguration while holding the configurationLock, as sync.Mutex is not // reentrant. func (p *HelpPlugin) setConfiguration(configuration *configuration) { // Replace the active configuration under lock. p.configurationLock.Lock() defer p.configurationLock.Unlock() p.configuration = configuration } // OnConfigurationChange updates the active configuration for this plugin under lock. func (p *HelpPlugin) OnConfigurationChange() error { var configuration = new(configuration) // Load the public configuration fields from the Mattermost server configuration. if err := p.API.LoadPluginConfiguration(configuration); err != nil { return errors.Wrap(err, \"failed to load plugin configuration\") } team, err := p.API.GetTeamByName(configuration.TeamName) if err != nil { return errors.Wrapf(err, \"failed to find team %s\", configuration.TeamName) } channel, err := p.API.GetChannelByName(team.Id, configuration.ChannelName, false) if err != nil { return errors.Wrapf(err, \"failed to find channel %s\", configuration.ChannelName) } configuration.channelID = channel.Id p.setConfiguration(configuration) return nil } // MessageHasBeenPosted automatically replies to posts that plea for help. func (p *HelpPlugin) MessageHasBeenPosted(c *plugin.Context, post *model.Post) { configuration := p.getConfiguration() // Ignore posts not in the configured channel if post.ChannelId != configuration.channelID { return } // Ignore posts this plugin made. if sentByPlugin, _ := post.GetProp(\"sent_by_plugin\").(bool); sentByPlugin { return } // Ignore posts without a plea for help. if !strings.Contains(post.Message, \"help\") { return } p.API.SendEphemeralPost(post.UserId, \u0026model.Post{ ChannelId:\tconfiguration.channelID, Message:\t\"You asked for help? Checkout https://support.mattermost.com/hc/en-us\", Props: map[string]any{ \"sent_by_plugin\": true, }, }) } func main() { plugin.ClientMain(\u0026HelpPlugin{}) } ","permalink":"https://developers.mattermost.com/integrate/reference/server/server-reference/","section":"integrate","subsection":null,"tags":null,"title":"Server plugin SDK reference"},{"categories":null,"contents":"Server plugins are subprocesses invoked by the server that communicate with Mattermost using remote procedure calls (RPC).\nLooking for a quick start? See our “Hello, world!” tutorial.\nWant the Server SDK reference doc? Find it here.\nFeatures RPC API Use the RPC API to execute create, read, update and delete (CRUD) operations on server data models.\nFor example, your plugin can consume events from a third-party webhook and create corresponding posts in Mattermost, without having to host your code outside Mattermost.\nHooks Register for hooks and get alerted when certain events occur.\nFor example, consume the OnConfigurationChange hook to respond to server configuration changes, or the MessageHasBeenPosted hook to respond to posts.\nREST API Implement the ServeHTTP hook to extend the existing Mattermost REST API.\nPlugins with both a web app and server component can leverage this REST API to exchange data. Alternatively, expose your REST API to services and developers connecting from outside Mattermost.\nHow it works When starting a plugin, the server consults the plugin’s manifest to determine if a server component was included. If found, the server launches a new process using the executable included with the plugin.\nThe server will trigger the OnActivate hook if the plugin is successfully started, allowing you to perform startup events. If the plugin is disabled, the server will trigger the OnDeactivate hook. While running, the server plugin can consume hook events, make API calls, launch threads or subprocesses of its own, interact with third-party services or do anything else a regular program can do.\nHigh availability Considerations for plugins in a high availability configuration are documented here: High Availability\nBest practices Some best practices for working with the server component of a plugin are documented here: Best Practices\nDebug Server plugins Guidelines for debugging the server-side of plugins are documented here: Debug Server plugins\n","permalink":"https://developers.mattermost.com/integrate/plugins/components/server/","section":"integrate","subsection":"plugins","tags":null,"title":"Server plugins"},{"categories":null,"contents":"Visit the Plugins section to learn more about developing Mattermost plugins and our recommended developer workflow for Mattermost plugins.\nTable of contents PluginClass Example Registry Theme Exported Libraries and Functions post-utils formatText(text, options) messageHtmlToComponent(html, isRHS, options) Usage Example PluginClass The PluginClass interface defines two methods used by the Mattermost Web App to initialize and uninitialize your plugin:\nclass PluginClass { /** * initialize is called by the webapp when the plugin is first loaded. * Receives the following: * - registry - an instance of the registry tied to your plugin id * - store - the Redux store of the web app. */ initialize(registry, store) /** * uninitialize is called by the webapp if your plugin is uninstalled */ uninitialize() } Your plugin should implement this class and register it using the global registerPlugin method defined on the window by the webapp:\nwindow.registerPlugin('myplugin', new PluginClass()); Use the provided registry to register components, post type overrides and callbacks. Use the store to access the global state of the web app, but note that you should use the registry to register any custom reducers your plugin might require.\nExample The entry point index.js of your application might contain:\nimport UserPopularity from './components/profile_popover/user_popularity'; import SomePost from './components/some_post'; import MenuIcon from './components/menu_icon'; import {openExampleModal} from './actions'; class PluginClass { initialize(registry, store) { registry.registerPopoverUserAttributesComponent( UserPopularity, ); registry.registerPostTypeComponent( 'custom_somepost', SomePost, ); registry.registerMainMenuAction( 'Plugin Menu Item', () =\u003e store.dispatch(openExampleModal()), mobile_icon: MenuIcon, ); } uninitialize() { // No clean up required. } } window.registerPlugin('myplugin', new PluginClass()); This will add a custom UserPopularity component to the profile popover, render a custom SomePost component for any post with the type custom_somepost, and insert a custom main menu item.\nRegistry An instance of the plugin registry is passed to each plugin via the initialize callback.\nregisterRootComponent([component]) registerPopoverUserAttributesComponent([component]) registerPopoverUserActionsComponent([component]) registerLeftSidebarHeaderComponent([component]) registerBottomTeamSidebarComponent([component]) registerPostMessageAttachmentComponent([component]) registerLinkTooltipComponent([component]) registerActionAfterChannelCreation([component action]) registerChannelHeaderIcon([component]) registerChannelHeaderButtonAction([icon action dropdownText tooltipText]) registerChannelIntroButtonAction([icon action text]) registerCallButtonAction([button dropdownButton action icon dropdownText]) registerPostTypeComponent([type component]) registerPostCardTypeComponent([type component]) registerPostWillRenderEmbedComponent([match component toggleable]) registerMainMenuAction([text action mobileIcon]) registerChannelHeaderMenuAction([text action shouldRender]) registerFileDropdownMenuAction([match text action]) registerUserGuideDropdownMenuAction([text action]) registerPostActionComponent([component]) registerPostEditorActionComponent([component]) registerAIActionMenuItemComponent([icon text sortOrder component action]) registerCodeBlockActionComponent([component]) registerNewMessagesSeparatorActionComponent([component]) registerPostDropdownMenuAction([text action filter]) registerPostDropdownSubMenuAction([text action filter]) registerPostDropdownMenuComponent([component]) registerFileUploadMethod([icon action text]) registerFilesWillUploadHook([hook]) unregisterComponent([componentId]) unregisterPostTypeComponent([componentId]) registerReducer([reducer]) registerWebSocketEventHandler([event handler]) unregisterWebSocketEventHandler([event]) registerReconnectHandler([handler]) unregisterReconnectHandler() registerMessageWillBePostedHook([hook]) registerSlashCommandWillBePostedHook([hook]) registerMessageWillFormatHook([hook]) registerFilePreviewComponent([override component]) registerTranslations([getTranslationsForLocale]) registerAdminConsolePlugin([func]) unregisterAdminConsolePlugin() registerAdminConsoleCustomSetting([key component options]) registerAdminConsoleCustomSection([key component]) registerRightHandSidebarComponent([component title]) registerNeedsTeamRoute([route component]) registerCustomRoute([route component]) registerProduct([baseURL switcherIcon switcherText switcherLinkURL mainComponent headerCentreComponent headerRightComponent showTeamSidebar showAppBar wrapped publicComponent]) registerMessageWillBeUpdatedHook([hook]) registerSidebarChannelLinkLabelComponent([component]) registerSidebarBrowseOrAddChannelMenuAction([text action icon]) registerChannelToastComponent([component]) registerGlobalComponent([component]) registerAppBarComponent([iconUrl action tooltipText supportedProductIds rhsComponent rhsTitle]) registerSiteStatisticsHandler([handler]) registerDesktopNotificationHook([hook]) registerUserSettings([setting]) registerSystemConsoleGroupTable([component]) registerRHSPluginPopoutListener([pluginId onPopoutOpened]) registerRootComponent /** */ registerRootComponent([component]) registerPopoverUserAttributesComponent /** */ registerPopoverUserAttributesComponent([component]) registerPopoverUserActionsComponent /** */ registerPopoverUserActionsComponent([component]) registerLeftSidebarHeaderComponent /** */ registerLeftSidebarHeaderComponent([component]) registerBottomTeamSidebarComponent /** */ registerBottomTeamSidebarComponent([component]) registerPostMessageAttachmentComponent /** */ registerPostMessageAttachmentComponent([component]) registerLinkTooltipComponent /** */ registerLinkTooltipComponent([component]) registerActionAfterChannelCreation /** */ registerActionAfterChannelCreation([component action]) registerChannelHeaderIcon /** */ registerChannelHeaderIcon([component]) registerChannelHeaderButtonAction /** */ registerChannelHeaderButtonAction([icon action dropdownText tooltipText]) registerChannelIntroButtonAction /** */ registerChannelIntroButtonAction([icon action text]) registerCallButtonAction /** */ registerCallButtonAction([button dropdownButton action icon dropdownText]) registerPostTypeComponent /** */ registerPostTypeComponent([type component]) registerPostCardTypeComponent /** */ registerPostCardTypeComponent([type component]) registerPostWillRenderEmbedComponent /** */ registerPostWillRenderEmbedComponent([match component toggleable]) registerMainMenuAction /** */ registerMainMenuAction([text action mobileIcon]) registerChannelHeaderMenuAction /** */ registerChannelHeaderMenuAction([text action shouldRender]) registerFileDropdownMenuAction /** */ registerFileDropdownMenuAction([match text action]) registerUserGuideDropdownMenuAction /** */ registerUserGuideDropdownMenuAction([text action]) registerPostActionComponent /** */ registerPostActionComponent([component]) registerPostEditorActionComponent /** */ registerPostEditorActionComponent([component]) registerAIActionMenuItemComponent /** */ registerAIActionMenuItemComponent([icon text sortOrder component action]) registerCodeBlockActionComponent /** */ registerCodeBlockActionComponent([component]) registerNewMessagesSeparatorActionComponent /** */ registerNewMessagesSeparatorActionComponent([component]) registerPostDropdownMenuAction /** */ registerPostDropdownMenuAction([text action filter]) registerPostDropdownSubMenuAction /** */ registerPostDropdownSubMenuAction([text action filter]) registerPostDropdownMenuComponent /** */ registerPostDropdownMenuComponent([component]) registerFileUploadMethod /** */ registerFileUploadMethod([icon action text]) registerFilesWillUploadHook /** */ registerFilesWillUploadHook([hook]) unregisterComponent /** */ unregisterComponent([componentId]) unregisterPostTypeComponent /** */ unregisterPostTypeComponent([componentId]) registerReducer /** */ registerReducer([reducer]) registerWebSocketEventHandler /** */ registerWebSocketEventHandler([event handler]) unregisterWebSocketEventHandler /** */ unregisterWebSocketEventHandler([event]) registerReconnectHandler /** */ registerReconnectHandler([handler]) unregisterReconnectHandler /** */ unregisterReconnectHandler() registerMessageWillBePostedHook /** */ registerMessageWillBePostedHook([hook]) registerSlashCommandWillBePostedHook /** */ registerSlashCommandWillBePostedHook([hook]) registerMessageWillFormatHook /** */ registerMessageWillFormatHook([hook]) registerFilePreviewComponent /** */ registerFilePreviewComponent([override component]) registerTranslations /** */ registerTranslations([getTranslationsForLocale]) registerAdminConsolePlugin /** */ registerAdminConsolePlugin([func]) unregisterAdminConsolePlugin /** */ unregisterAdminConsolePlugin() registerAdminConsoleCustomSetting /** */ registerAdminConsoleCustomSetting([key component options]) registerAdminConsoleCustomSection /** */ registerAdminConsoleCustomSection([key component]) registerRightHandSidebarComponent /** */ registerRightHandSidebarComponent([component title]) registerNeedsTeamRoute /** */ registerNeedsTeamRoute([route component]) registerCustomRoute /** */ registerCustomRoute([route component]) registerProduct /** */ registerProduct([baseURL switcherIcon switcherText switcherLinkURL mainComponent headerCentreComponent headerRightComponent showTeamSidebar showAppBar wrapped publicComponent]) registerMessageWillBeUpdatedHook /** */ registerMessageWillBeUpdatedHook([hook]) registerSidebarChannelLinkLabelComponent /** */ registerSidebarChannelLinkLabelComponent([component]) registerSidebarBrowseOrAddChannelMenuAction /** */ registerSidebarBrowseOrAddChannelMenuAction([text action icon]) registerChannelToastComponent /** */ registerChannelToastComponent([component]) registerGlobalComponent /** */ registerGlobalComponent([component]) registerAppBarComponent /** */ registerAppBarComponent([iconUrl action tooltipText supportedProductIds rhsComponent rhsTitle]) registerSiteStatisticsHandler /** */ registerSiteStatisticsHandler([handler]) registerDesktopNotificationHook /** */ registerDesktopNotificationHook([hook]) registerUserSettings /** */ registerUserSettings([setting]) registerSystemConsoleGroupTable /** */ registerSystemConsoleGroupTable([component]) registerRHSPluginPopoutListener /** */ registerRHSPluginPopoutListener([pluginId onPopoutOpened]) Theme In Mattermost, users are able to set custom themes that change the color scheme of the UI. It’s important that plugins have access to a user’s theme so that they can set their styling to match and not look out of place.\nEvery pluggable component in the web app will have the theme object as a prop.\nThe colors are exposed via CSS variables as well.\nThe theme object has the following properties:\nProperty CSS Variable Description sidebarBg –sidebar-bg Background color of the left-hand sidebar sidebarText –sidebar-text Color of text in the left-hand sidebar sidebarUnreadText –sidebar-unread-text Color of text for unread channels in the left-hand sidebar sidebarTextHoverBg –sidebar-text-hover-bg Background color of channels when hovered in the left-hand sidebar sidebarTextActiveBorder –sidebar-text-active-border Color of the selected indicator channel indicator in the left-hand siebar sidebarTextActiveColor –sidebar-text-active-color Color of the text for the selected channel in the left-hand sidebar sidebarHeaderBg –sidebar-header-bg Background color of the left-hand sidebar header sidebarHeaderTextColor –sidebar-header-text-color Color of text in the left-hand sidebar header onlineIndicator –online-indicator Color of the online status indicator awayIndicator –away-indicator Color of the away status indicator dndIndicator –dnd-indicator Color of the do not disturb status indicator mentionBg –mention-bg Background color for mention jewels in the left-hand sidebar mentionColor –mention-color Color of text for mention jewels in the left-hand sidebar centerChannelBg –center-channel-bg Background color of channels, right-hand sidebar and modals/popovers centerChannelColor –center-channel-color Color of text in channels, right-hand sidebar and modals/popovers newMessageSeparator –new-message-separator Color of the new message separator in channels linkColor –link-color Color of text for links buttonBg –button-bg Background color of buttons buttonColor –button-color Color of text for buttons errorTextColor –error-text Color of text for errors mentionHighlightBg –mention-highlight-bg Background color of mention highlights in posts mentionHighlightLink –mention-highlight-link Color of text for mention links in posts codeTheme NA Code block theme, either ‘github’, ‘monokai’, ‘solarized-dark’ or ‘solarized-light’ Exported libraries and functions The web app exposes a number of exported libraries and functions on the window object for plugins to use. To avoid bloating your plugin, we recommend depending on these using Webpack externals or importing them manually from the window. Below is a list of the exposed libraries and functions:\nLibrary Exported Name Description react window.React ReactJS react-dom window.ReactDOM ReactDOM redux window.Redux Redux react-redux window.ReactRedux React bindings for Redux react-bootstrap window.ReactBootstrap Bootstrap for React prop-types window.PropTypes PropTypes post-utils window.PostUtils Mattermost post utility functions (see below) Note: Some sets of functions like “Functions exposed on window for plugin to use” and “Components exposed on window for internal plugin use only” are not listed here. You can refer to export.js file which contains all the exports. post-utils Contains the following post utility functions:\nformatText(text, options) Performs formatting of text including Markdown, highlighting mentions and search terms and converting URLs, hashtags, @mentions and ~channels to links by taking a string and returning a string of formatted HTML.\ntext - String of text to format, e.g. a post’s message. options - (Optional) An object containing the following formatting options searchTerm - If specified, this word is highlighted in the resulting HTML. Defaults to nothing. mentionHighlight - Specifies whether or not to highlight mentions of the current user. Defaults to true. mentionKeys - A list of mention keys for the current user to highlight. singleline - Specifies whether or not to remove newlines. Defaults to false. emoticons - Enables emoticon parsing with a data-emoticon attribute. Defaults to true. markdown - Enables markdown parsing. Defaults to true. siteURL - The origin of this Mattermost instance. If provided, links to channels and posts will be replaced with internal links that can be handled by a special click handler. atMentions - Whether or not to render “@” mentions into spans with a data-mention attribute. Defaults to false. channelNamesMap - An object mapping channel display names to channels. If channelNamesMap and team are provided, ~channel mentions will be replaced with links to the relevant channel. team - The current team object. proxyImages - If specified, images are proxied. Defaults to false. messageHtmlToComponent(html, isRHS, options) Converts HTML to React components.\nhtml - String of HTML to convert to React components. isRHS - Boolean indicating if the resulting components are to be displayed in the right-hand sidebar. Has some minor effects on how UI events are triggered for components in the RHS. options - (Optional) An object containing options mentions - If set, mentions are replaced with the AtMention component. Defaults to true. emoji - If set, emoji text is replaced with the PostEmoji component. Defaults to true. images - If set, markdown images are replaced with the PostMarkdown component. Defaults to true. latex - If set, latex is replaced with the LatexBlock component. Defaults to true. Usage example A short usage example of a PostType component using the post utility functions to format text.\nimport React from 'react'; // accessed through webpack externals import PropTypes from 'prop-types'; const PostUtils = window.PostUtils; // must be accessed through `window` export default class PostTypeFormatted extends React.PureComponent { // ... render() { const post = this.props.post; const formattedText = PostUtils.formatText(post.message); // format the text return ( \u003cdiv\u003e {'Formatted text: '} {PostUtils.messageHtmlToComponent(formattedText)} // convert the html to components \u003c/div\u003e ); } } ","permalink":"https://developers.mattermost.com/integrate/reference/webapp/webapp-reference/","section":"integrate","subsection":null,"tags":null,"title":"Web app plugin SDK reference"},{"categories":null,"contents":"Mattermost plugins are isolated pieces of code written in Go and/or React. They’re separate from the main repositories and are used to extend the functionality of the Mattermost server and webapp.\nThe Go portions run directly on the Mattermost server, and are managed by the server at runtime. The React portions run in each user’s browser, allowing developers to modify the user interface in several ways. The plugin Help Wanted tickets are located in each plugin’s respective GitHub repository. In order to browse all of the open tickets, see the plugin Help Wanted tickets page with links to specific plugin repositories, as well as queries for Help Wanted tickets in all repositories. The All Plugins Up for Grabs link is useful to browse all repositories at once.\nThe plugin developer setup and developer workflow pages are useful to learn about the plugin development environment. You can find more information about plugins in general here.\nNote: The make commands listed in the developer workflow page (specifically make test and make check-style) should be used locally to run certain tests before submitting a PR. This makes the PR review process much more streamlined overall. ","permalink":"https://developers.mattermost.com/contribute/more-info/plugins/","section":"contribute","subsection":"more info","tags":null,"title":"Plugins"},{"categories":null,"contents":"Each Mattermost installation comes with some built-in slash commands that are ready to use. See the Mattermost product documentation for details on available slash commands available in the latest Mattermost release\nTo create a custom slash command, see the Custom slash commands developer documentation.\n","permalink":"https://developers.mattermost.com/integrate/slash-commands/built-in/","section":"integrate","subsection":"slash commands","tags":null,"title":"Built-in commands"},{"categories":null,"contents":"Mattermost offers a wealth of methods to add functionality and customize the experience to suit your needs, whether you want to add new user capabilities with slash commands, build an advanced chatbot, or completely change the functionality of your server.\nWebhooks Webhooks provide a simple way to post messages to a channel and trigger external actions.\nCreate your Webhook now\nSlash commands Slash commands are messages that begin with / and trigger an HTTP request to a web service that can in turn post one or more messages in response.\nCreate your Slash command now\nPlugins Plugins are the most comprehensive way to add new features and customization, but come with additional development overhead and must be written in Go. They’re for developers who need tightly integrated services or want to improve the server, mobile, desktop, and web apps without making contributions to the core codebase.\nGet started with plugins\nTip: See the Mattermost Server SDK Reference and Mattermost Client UI SDK Reference documentation for details on available server API endpoints and client methods. API Interact with users, channels, and everything else that happens on your Mattermost server via a modern REST API that meets the OpenAPI specification. The API is for developers who want to build bots and other interactions that don’t rely on customizing the Mattermost user experience.\nView the REST API Reference\nOther ways to integrate and extend Embed - Learn how to use the Mattermost API to embed Mattermost into web browsers and web applications. Customize - Modify the source code for the server or web app to make basic changes and customization. Interactive Messages - Create messages that include interactive functionality. ","permalink":"https://developers.mattermost.com/integrate/getting-started/","section":"integrate","subsection":null,"tags":null,"title":"Get started"},{"categories":null,"contents":"Create an incoming webhook Let’s learn how to create a simple incoming webhook that posts the following message to Mattermost.\nIn Mattermost, go to Product menu \u003e Integrations \u003e Incoming Webhook. If you don’t have the Integrations option, incoming webhooks may not be enabled on your Mattermost server or may be disabled for non-admins. They can be enabled by a System Admin from System Console \u003e Integrations \u003e Integration Management. Once incoming webhooks are enabled, continue with the steps below. Select Add Incoming Webhook and add a name and description for the webhook. The description can be up to 500 characters. Select the channel to receive webhook payloads, then select Add to create the webhook. You will end up with a webhook endpoint that looks like so:\nhttps://your-mattermost-server.com/hooks/xxx-generatedkey-xxx Treat this endpoint as a secret. Anyone who has it will be able to post messages to your Mattermost instance.\nUse an incoming webhook To use the endpoint, have your application make the following request:\nPOST /hooks/xxx-generatedkey-xxx HTTP/1.1 Host: your-mattermost-server.com Content-Type: application/json Content-Length: 63 { \"text\": \"Hello, this is some text\\nThis is more text. 🎉\" } For example, here is the same request using cURL:\ncurl -i -X POST -H 'Content-Type: application/json' -d '{\"text\": \"Hello, this is some text\\nThis is more text. 🎉\"}' https://your-mattermost-server.com/hooks/xxx-generatedkey-xxx For compatibility with Slack incoming webhooks, if no Content-Type header is set then the request body must be prefixed with payload=, like so:\npayload={\"text\": \"Hello, this is some text\\nThis is more text. 🎉\"} A successful request will get the following response:\nHTTP/1.1 200 OK Content-Type: text/plain X-Request-Id: hoan6o9ws7rp5xj7wu9rmysrte X-Version-Id: 4.7.1.dev.12799cd77e172e8a2eba0f9091ec1471.false Date: Sun, 04 Mar 2018 17:19:09 GMT Content-Length: 2 ok All webhook posts will display a BOT indicator next to the username in Mattermost clients to help prevent against phishing attacks.\nParameters Incoming webhooks support more than just the text field. Here is a full list of supported parameters.\nParameter Description Required text Markdown-formatted message to display in the post.To trigger notifications, use @\u003cusername\u003e, @channel, and @here like you would in other Mattermost messages. If attachments is not set, yes channel Overrides the channel the message posts in. Use the channel’s name and not the display name, e.g. use town-square, not Town Square.Use an “@” followed by a username to send to a Direct Message.Defaults to the channel set during webhook creation.The webhook can post to any Public channel and Private channel the webhook creator is in.Posts to Direct Messages will appear in the Direct Message between the targeted user and the webhook creator. No username Overrides the username the message posts as.Defaults to the username set during webhook creation; if no username was set during creation, webhook is used.The Enable integrations to override usernames configuration setting must be enabled for the username override to take effect. No icon_url Overrides the profile picture the message posts with.Defaults to the URL set during webhook creation; if no icon was set during creation, the standard webhook icon () is displayed.The Enable integrations to override profile picture icons configuration setting must be enabled for the icon override to take effect. No icon_emoji Overrides the profile picture and icon_url parameter.Defaults to none and is not set during webhook creation.The expected value is an emoji name as typed in a message, either with or without colons (:).The Enable integrations to override profile picture icons configuration setting must be enabled for the override to take effect.. No attachments Message attachments used for richer formatting options. If text is not set, yes type Sets the post type, mainly for use by plugins.If not blank, must begin with custom_ unless set to burn_on_read. No props Sets the post props, a JSON property bag for storing extra or meta data on the post.Mainly used by other integrations accessing posts through the REST API.The following keys are reserved: from_webhook, override_username, override_icon_url, override_icon_emoji, webhook_display_name, card, and attachments.Props card allows for extra information (Markdown-formatted text) to be sent to Mattermost that will only be displayed in the RHS panel after a user selects the info icon displayed alongside the post.The info icon cannot be customized and is only rendered visible to the user if there is card data passed into the message.This property is available from Mattermost v5.14.There is currently no Mobile support for card functionality. No priority Set the priority of the message. See Message Priority No An example request using more parameters would look like this:\nPOST /hooks/xxx-generatedkey-xxx HTTP/1.1 Host: your-mattermost-server.com Content-Type: application/json Content-Length: 630 { \"channel\": \"town-square\", \"username\": \"test-automation\", \"icon_url\": \"https://mattermost.com/wp-content/uploads/2022/02/icon.png\", \"text\": \"#### Test results for July 27th, 2017\\n@channel please review failed tests.\\n\\n| Component | Tests Run | Tests Failed |\\n|:-----------|:-----------:|:-----------------------------------------------|\\n| Server | 948 | ✅ 0 |\\n| Web Client | 123 | ⚠️ 2 [(see details)](https://linktologs) |\\n| iOS Client | 78 | ⚠️ 3 [(see details)](https://linktologs) |\" } This content will be displayed in the Town Square channel:\nAn example request displaying additional data in the right-hand side panel, by passing Markdown text into the card field of the props object would look like this:\nPOST /hooks/xxx-generatedkey-xxx HTTP/1.1 Host: your-mattermost-server.com Content-Type: application/json { \"channel\": \"town-square\", \"username\": \"Winning-bot\", \"text\": \"#### We won a new deal!\", \"props\": { \"card\": \"Salesforce Opportunity Information:\\n\\n [Opportunity Name](https://salesforce.com/OPPORTUNITY_ID)\\n\\n-Salesperson: **Bob McKnight** \\n\\n Amount: **$300,020.00**\" } } When there is a props object with a card property attached to the webhook payload, the posted message displays a small info icon next to the timestamp. Clicking this icon expands the right-hand side panel to display the Markdown included in the card property:\nSlack compatibility Mattermost makes it easy to migrate integrations written for Slack to Mattermost. Using the Slack icon_emoji parameter overrides the profile icon and the icon_url parameter and is supported from Mattermost v5.14.\nTranslate Slack’s data format to Mattermost Mattermost automatically translates the data coming from Slack:\nJSON payloads written for Slack, that contain the following, are translated to Mattermost markdown and rendered equivalently to Slack:\n\u003c\u003e to denote a URL link, such as {\"text\": \"\u003chttps://mattermost.com/\u003e\"} | within a \u003c\u003e to define linked text, such as {\"text\": \"Click \u003chttps://mattermost.com/|here\u003e for a link.\"} \u003cuserid\u003e to trigger a mention to a user, such as {\"text\": \"\u003c5fb5f7iw8tfrfcwssd1xmx3j7y\u003e this is a notification.\"} \u003c!channel\u003e, \u003c!here\u003e, or \u003c!all\u003e to trigger a mention to a channel, such as {\"text\": \"\u003c!channel\u003e this is a notification.\"} You can override the channel name with a @username, such as payload={\"text\": \"Hi\", channel: \"@jim\"} to send a direct message like in Slack.\nYou can prepend a channel name with # and the message will still be sent to the correct channel like in Slack.\nMattermost webhooks in GitLab using Slack UI GitLab is the leading open-source alternative to GitHub and offers built-in integrations with Slack. You can use the Slack interface in GitLab to add Mattermost webhooks directly without changing code:\nIn GitLab, go to Settings \u003e Services and select Slack. Paste the incoming webhook URL provided by Mattermost from Main Menu \u003e Integrations \u003e Incoming Webhooks. Optionally set the Username you’d like displayed when the notification is made. Leave the Channel field blank. Select Save, then test the settings to confirm messages are sent successfully to Mattermost. Known Slack compatibility issues Referencing channels using \u003c#CHANNEL_ID\u003e does not link to the channel. \u003c!everyone\u003e and \u003c!group\u003e are not supported. Parameters “mrkdwn”, “parse”, and “link_names” are not supported. Mattermost converts Markdown by default and automatically links @mentions. Bold formatting as *bold* is not supported (must be done as **bold**). Webhooks cannot direct message the user who created the webhook. Tips and best practices If the text is longer than the allowable character limit per post, the message is split into multiple consecutive posts, each within the character limit. From Mattermost Server v5.0, posts up to 16383 characters are supported.\nYour webhook integration may be written in any programming language as long as it supports sending an HTTP POST request.\nBoth application/x-www-form-urlencoded and multipart/form-data are supported Content-Type headers. If no Content-Type is provided, application/json is assumed.\nTo send a message to a direct message channel, add an “@” symbol followed by the username to the channel parameter. You can add your own username to send the webhook posts to a direct message channel with yourself.\npayload={\"channel\": \"@username\", \"text\": \"Hello, this is some text\\nThis is more text. 🎉\"} This will send a message from the account that has set up the incoming webhook to the username after the “@” symbol. For example, if you create a webhook with the user alice and send a direct message to bob using a webhook, it will show up as a direct message from alice to bob regardless of other settings such as username.\nTo send a message to a different direct message channel between two other users, you can specify the channel with the user IDs for the users separated with two underscore (_) symbols. To find the user ID you can use mmctl user search.\npayload={\"channel\": \"6w41z1q367dujfaxr1nrykr5oc__94dzjnkd8igafdraw66syi1cde\", \"text\": \"Hello, this is some text\\nThis is more text. 🎉\"} Troubleshoot incoming webhooks To debug incoming webhooks in System Console \u003e Logs, set System Console \u003e Logging \u003e Enable Webhook Debugging to true, and set System Console \u003e Logging \u003e Console Log Level to DEBUG.\nSome common error messages include:\nCouldn't find the channel: Indicates that the channel doesn’t exist or is invalid. Please modify the channel parameter before sending another request.\nCouldn't find the user: Indicates that the user doesn’t exist or is invalid. Please modify the user parameter before sending another request.\nUnable to parse incoming data: Indicates that the request received is malformed. Try reviewing that the JSON payload is in a correct format and doesn’t have typos such as extra \".\ncurl: (3) [globbing] unmatched close brace/bracket in column N: Typically an error when using cURL on Windows, when:\nYou have space around JSON separator colons, payload={\"Hello\" : \"test\"} or You are using single quotes to wrap the -d data, -d 'payload={\"Hello\":\"test\"}' If your integration prints the JSON payload data instead of rendering the generated message, make sure your integration is returning the application/json content-type.\nWhy aren’t my message attachments rendering? Make sure attachments is a top-level field in your webhook JSON payload, not nested inside props. While the REST API uses props.attachments, incoming webhooks expect attachments at the top level.\nFor further assistance, review the Troubleshooting forum for previously reported errors, or join the Mattermost user community for troubleshooting help.\n","permalink":"https://developers.mattermost.com/integrate/webhooks/incoming/","section":"integrate","subsection":"webhooks","tags":null,"title":"Incoming webhooks"},{"categories":null,"contents":"When building web app plugins, it’s common to perform actions or access the state that web and mobile apps already support. The majority of these actions exist in mattermost-redux, which is our library of shared code between Mattermost JavaScript clients. The mattermost-redux library exports types and functions that are imported by the web application. These functions can be imported by plugins and used the same way. There are a few different kinds of functions exported by the library:\nactions: Actions perform API requests and can change the state of Mattermost. client: The client package can be used to instantiate a Client4 object to interact with the Mattermost API directly. This is useful in plugins as well as JavaScript server applications communicating with Mattermost. constants: An assortment of constants within Mattermost’s data model. selectors: Selectors return certain data from the Redux store, such as getPost which allows you get a post by id. store: Functions related to the Redux store itself. types: Various types of objects in Mattermost’s data model. These are useful for plugins written in Typescript. utils: Various utility functions shared across the web application. Prerequisites It’s assumed you have already set up your plugin development environment for web app plugins to match mattermost-plugin-starter-template. If not, follow the README instructions of that repository first, or see the Hello, World! guide.\nBasic example Here’s an example of a web app plugin making use of mattermost-redux's selectors:\nimport {useSelector} from 'react-redux'; import {getPost} from 'mattermost-redux/selectors/entities/posts'; import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams'; const MyComponent = ({postId}) =\u003e { const post = useSelector((state) =\u003e getPost(state, postId)); const currentUser = useSelector(getCurrentUser); const currentChannel = useSelector(getCurrentChannel); const currentTeam = useSelector(getCurrentTeam); // ... }; Some common actions We’ve listed out some of the commonly-used actions that you can use in your web app plugin. You can find all the actions are available for your plugin to import in the source code for mattermost-redux.\ncreateChannel(channel: Channel, userId: string) Dispatch this action to create a new channel.\ngetCustomEmoji(emojiId: string) Dispatch this action to fetch a specific emoji associated with the emojiId provided.\ncreatePost(post: Post, files: any[] = []) Dispatch this action to create a new post.\ngetMyTeams() Dispatch this action to fetch all the team types associated with the current user.\ncreateUser(user: UserProfile, token: string, inviteId: string, redirect: string) Dispatch this action to create a new user profile.\nSome common selectors Here are some examples of commonly-used selectors you can use in your web app plugin. You can find all the selectors that are available for your plugin to import in the source code for mattermost-redux.\ngetCurrentUserId(state) Retrieves the userId of the current user from the Redux store.\ngetCurrentUser(state: GlobalState): UserProfile Retrieves the user profile of the current user from the Redux store.\ngetUsers(state: GlobalState): IDMappedObjects\u003cUserProfile\u003e Retrieves all user profiles from the Redux store.\ngetChannel(state: GlobalState, id: string) Retrieves a channel as it exists in the store without filling in any additional details such as the display_name for Direct Messages/Group Messages.\ngetCurrentChannelId(state: GlobalState) Retrieves the channel ID of the current channel from the Redux store.\ngetCurrentChannel: (state: GlobalState) Retrieves the complete channel info of the current channel from the Redux store.\ngetPost(state: GlobalState, postId: $ID\u003cPost\u003e) Retrieves the specific post associated with the supplied postID from the Redux store.\ngetCurrentTeamId(state: GlobalState) Retrieves the teamId of the current team from the Redux store.\ngetCurrentTeam: (state: GlobalState) Retrieves the team info of the current team from the Redux store.\ngetCustomEmojisByName: (state: GlobalState) Retrieves the the specific emoji associated with the supplied customEmojiName from the Redux store.\nSome common client functions We’ve listed out some of the commonly-used client functions you can use in your web app plugin. You can find all the client functions that are available for your plugin to import in the source code for mattermost-redux.\ngetUser = (userId: string) Routes to the user profile of the specified userId from the Mattermost Server.\ngetUserByUsername = (username: string) Routes to the user profile of the specified username from the Mattermost Server.\ngetChannel = (channelId: string) Routes to the channel of the specified channelId from the Mattermost Server.\ngetChannelByName = (teamId: string, channelName: string, includeDeleted = false) Routes to the channel of the specified channelName from the Mattermost Server.\ngetTeam = (teamId: string) Routes to the team of the specified teamId from the Mattermost Server.\ngetTeamByName = (teamName: string) Routes to the team of the specified teamName from the Mattermost Server.\nexecuteCommand = (command: string, commandArgs: CommandArgs) Executes the specified command with the arguments provided and fetches the response.\ngetOptions(options: Options) {const newOptions: Options = {...options} Get the client options to make requests to the server. Use this to create your own custom requests.\nCustom reducers and actions Reducers in Redux are pure functions that describe how the data in the store changes after any given action. Reducers will always produce the same resulting state for a given state and action. You can register a custom reducer for your plugin against the Redux store with the registerReducer function.\nregisterReducer(reducer) Registers a reducer against the Redux store. It will be accessible in Redux state under state['plugins-\u003cyourpluginid\u003e']. It generally accepts a reducer and returns undefined.\nWhen building web app plugins, it is common to perform actions that web and mobile apps already support. The majority of these actions exist in mattermost-redux, our library of shared code between Mattermost JavaScript clients.\nHere we’ll show how to use Redux actions with a plugin. To learn more about these actions, see the contributor documentation.\nPrerequisites This guide assumes you have already set up your plugin development environment for web app plugins to match mattermost-plugin-starter-template. If not, follow the README instructions of that repository first, or see the Hello, World! guide.\nImport mattermost-redux First, you’ll need to add mattermost-redux as a dependency of your web app plugin.\ncd /path/to/plugin/webapp npm install mattermost-redux That will add mattermost-redux as a dependency in your package.json file, allowing it to be imported into any of your plugin’s JavaScript files.\nUse an action Actions are used as part of components. To give components access to these actions, we pass them in as React props from the component’s container index.js file. To demonstrate this, we’ll create a new component.\nIn the webapp directory, let’s create a component folder called action_example and switch into it.\nmkdir -p src/components/action_example cd src/components/action_example In there, create two files: index.js and action_example.jsx. If you’re not familiar with why we’re creating these directories and files, read the contributor documentation on using React with Redux.\nOpen up action_example.jsx and add the following:\nimport React from 'react'; import PropTypes from 'prop-types'; export default class ActionExample extends React.PureComponent { static propTypes = { user: PropTypes.object.isRequired, patchUser: PropTypes.func.isRequired, // here we define the action as a prop } updateFirstName = () =\u003e { const patchedUser = { id: this.props.user.id, first_name: 'Jim', }; this.props.patchUser(patchedUser); // here we use the action } render() { return ( \u003cdiv\u003e {'First name: ' + this.props.user.first_name} \u003ca href='#' onClick={this.updateFirstName} \u003e Click me to update the first name! \u003c/a\u003e \u003c/div\u003e ); } } This component will display a user’s first name and then, when the link is clicked, use an action to update that user’s first name to “Jim”.\nThe action patchUser is from mattermost-redux. It takes in a subset of a user object and updates the user on the server, using the `PUT /users/{user_id}/patch` endpoint.\nWe must now use our container to import this action and pass it our component. Open up the index.js file and add:\nimport {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import {patchUser} from 'mattermost-redux/actions'; // importing the action import ActionExample from './action_example.jsx'; const mapStateToProps = (state) =\u003e { const currentUserId = state.entities.users.currentUserId; return { user: state.entities.users.profiles[currentUserId], }; }; const mapDispatchToProps = (dispatch) =\u003e bindActionCreators({ patchUser, // passing the action as a prop }, dispatch); export default connect(mapStateToProps, mapDispatchToProps)(ActionExample); The container is doing two things. First, it’s grabbing the current (logged in) user from the Redux state and passing it in as a prop. Anytime the Redux state updates, for example when we use the patchUser action, our component will get the updated copy of the current user. Second, the container is importing the patchUser action from mattermost-redux and passing it in as an action prop to our component.\nNow we can use this.props.patchUser() to update a user. The example component we made uses it to patch the current user’s first name.\nTo use our component in our plugin we would then use the registry in the initialization function of the plugin to register the component somewhere in the Mattermost UI. That is beyond the scope of this guide, but you can read more about that here.\nAvailable actions The actions that are available for your plugin to import can be found in the source code for mattermost-redux.\n","permalink":"https://developers.mattermost.com/integrate/plugins/components/webapp/actions/","section":"integrate","subsection":"plugins","tags":null,"title":"Redux actions"},{"categories":null,"contents":"The Mattermost REST API is documented here: Mattermost REST API\n","permalink":"https://developers.mattermost.com/integrate/reference/rest-api/","section":"integrate","subsection":null,"tags":null,"title":"REST API reference"},{"categories":null,"contents":"Once you have your server and webapp set up, you can start developing on plugins.\nNote: Plugin development doesn’t require a development build of Mattermost. Development builds of Mattermost are only required if you want to develop for Mattermost internally. For developing on Mattermost-managed plugins, each plugin’s setup instructions can be found in the plugin repository’s README. Some plugins do not have external dependencies and require little to no setup, like the Todo Plugin while others require an external service to be set up, like the Jira Plugin and GitHub Plugin.\nSet up your environment to deploy plugins Deploy with local mode Note: Deploying with local mode will only work for plugins that have been updated with the functionality from the Plugin Starter Template. If your Mattermost server is running locally, you can enable local mode and plugin uploads to streamline deploying your plugin. Edit your server configuration as follows:\n{ \"ServiceSettings\": { // ... \"EnableLocalMode\": true, \"LocalModeSocketLocation\": \"/var/tmp/mattermost_local.socket\" }, \"PluginSettings\": { // ... \"Enable\": true, \"EnableUploads\": true } } and then deploy your plugin:\nmake deploy You may also customize the Unix socket path:\nexport MM_LOCALSOCKETPATH=/var/tmp/alternate_local.socket make deploy Deploy with authentication credentials Alternatively, you can authenticate with the server’s API with credentials:\nexport MM_SERVICESETTINGS_SITEURL=http://localhost:8065 export MM_ADMIN_USERNAME=admin export MM_ADMIN_PASSWORD=password make deploy or with a personal access token:\nexport MM_SERVICESETTINGS_SITEURL=http://localhost:8065 export MM_ADMIN_TOKEN=j44acwd8obn78cdcx7koid4jkr make deploy Tip: Different plugin projects may require specific versions of Node.js. The recommended version for the given project is always defined in a file in the root of the repository called .nvmrc. You can use the tool Node Version Manager to install and switch between node versions in your terminal. If you have nvm installed, you can run nvm install anywhere in the plugin repository, and it will automatically find the .nvmrc file in the root, and install and use that version. If you already have that version installed, you can run nvm use, though nvm install can be easier since you don’t need to check if you already have the specific Node version installed. ","permalink":"https://developers.mattermost.com/integrate/plugins/developer-setup/","section":"integrate","subsection":"plugins","tags":null,"title":"Developer setup"},{"categories":null,"contents":"The self-managed releases are cut based off of the Mattermost Cloud release tags (e.g Mattermost Server v6.3 release was based off of cloud-2021-12-08-1 Cloud release tag) in the server, webapp, enterprise, and api-reference repos. See the Handbook release process for more details.\nThe Mobile and Desktop app release branches are based off of master branch.\nDeveloper process When your PR is required on a release branch (e.g. for a dot release or to fix a regression for an upcoming release), you will follow the cherry-picking process.\nMake a PR to ‘master’ like normal.\nAdd the appropriate milestone and the CherryPick/Approved label.\nWhen your PR is approved, it will be assigned back to you to perform the merge and any cherry picking if necessary.\nMerge the PR.\nAn automated cherry-pick process will try to cherry-pick the PR. If the automatic process succeeds, a new PR pointing to the correct release branch will open with all the appropriate labels. If there are no additional changes from the original PR for the cherry-pick, it can be merged without further review.\nIf the automated cherry-pick fails, the developer will need to cherry-pick the PR manually. Cherry-pick the master commit back to the appropriate releases. If the release branches have not been cut yet, leave the labels as-is and cherry-pick once the branch has been cut. The release manager will remind you to finish your cherry-pick.\nSet the CherryPick/Done label when completed.\nIf the cherry-pick fails, the developer needs to apply the cherry-pick manually. Cherry-pick the commit from master to the affected releases. See the steps below: Run the checks for lint and tests.\nPush your changes directly to the remote branch if the check style and tests passed.\nNo new pull request is required unless there are substantial merge conflicts.\nRemove the CherryPick/Approved label and apply the CherryPick/Done label.\nNote: If the PR needs to go to other release branches, you can run the command /cherry-pick release-x.yz in the PR comments and it will try to cherry-pick it to the branch you specified. Manual cherry-pick If conflicts appear between your pull request (PR) and the cherry-pick target branch, the automated cherry-pick process will fail and will let you know that you need to do a manual cherry-pick. Here are the steps to do so:\nFetch the latest updates from origin: git fetch origin Create a new branch starting at the release branch on origin. git checkout -b manual-cherry-pick-pr-[PR_NUMBER] origin/release-[VERSION] Find the SHA of the pull request merge commit, and cherry-pick this commit in your new branch: git log origin/master git cherry-pick [SHA] You’re likely to face the conflict that prevented the automated cherry-pick now. Fix the conflict, and then run the following: git add [path/to/conflicted/files] git cherry-pick --continue Finally, push your new branch as usual and create a PR. Make sure you select release-[VERSION] as the base branch, and not the default (master). git push -u origin manual-cherry-pick-pr-[PR_NUMBER] Reviewer process If you are the second reviewer reviewing a PR that needs to be cherry-picked, do not merge the PR. If the submitter is a core team member, you should set the Reviews Complete label and assign it to the submitter to cherry-pick. If the submitter is a community member who is not available to cherry-pick their PR or can not do it themselves, you should follow the cherry-pick process above.\n","permalink":"https://developers.mattermost.com/contribute/more-info/getting-started/branching/","section":"contribute","subsection":"more info","tags":null,"title":"Mattermost cherry-pick process"},{"categories":null,"contents":"Web app plugins extend and modify the Mattermost web and desktop apps, without having to fork and rebase on every Mattermost release.\nLooking for a quick start? See our “Hello, world!” tutorial.\nWant the web app SDK reference doc? Find it here.\nFeatures Extend existing components Register your own React components to be added to the channel header, sidebars, user details popover, main menu and other supported components. Multiple plugins can add to the same component simultaneously. This API focuses on ease of use and maintaining compatibility with future Mattermost releases.\nFor example, the Mattermost Zoom Plugin registers a button in the channel header to trigger a Zoom call and post a link to the call in the current channel.\nAdd new root components Register your own React components alongside other root components like the sidebars and modals. This enables whole new interactions that aren’t constrained by the set of existing components. This API is geared towards power and flexibility, but may require fine tuning with future Mattermost releases.\nCustom post type components Web app plugins can also render different post components based on the post’s type. Any time the web app encounters a post with this post type, it replaces the default rendering of the post component with your own custom implementation. Only one plugin can own the rendering for a given custom post type at a time: the last plugin to register will own the rendering for that custom post type.\nFor example, you can register a custom post type custom_poll using registerPostTypeComponent. Then, any time the web app sees that post type, it replaces the regular rendering of the post component with your own custom implementation.\nUse this in conjunction with setting the post type in webhooks or slash commands, through the REST API or with a server plugin, and you can deeply integrate or extend Mattermost posts to fit your needs.\nHow it works When a plugin is uploaded to a Mattermost server and activated, the server checks to see if there is a webapp portion included as part of the plugin by looking at the plugin’s manifest. If one is found, the server copies the bundled JavaScript included with the plugin into the static directory for serving. A WebSocket event is then fired off to the connected clients signalling the activation of a new plugin.\nOn web app launch, a request is made to the server to get a list of plugins that contain web app components. The web app then proceeds to download and execute the JavaScript bundles for each plugin. A similar process happens if an already launched web app receives a WebSocket event for a newly activated plugin.\nOnce downloaded and executed, each plugin should have registered itself via the global registerPlugin. The web app then invokes the initialize function defined on the plugin class, passing a registry and store. The registry passed allows the plugin to register (and unregister) components, event callbacks and Redux reducers to track plugin state. The store passed is the same Redux store used by the web app, giving the plugin access to the full state of the web app.\nComponents registered by the plugin via the registry are tracked in the Redux store and used by Pluggable components throughout the web app. Pluggable components with a pluggableName attribute can render multiple such components registered by plugins.\nCustom post types work similarly, but are registered slightly differently, use a separate reducer and have more of a custom implementation.\nRedux actions Further information on the available Redux Actions is documented here: Redux Actions\nBest practices Some best practices for working with the webapp component of a plugin are documented here: Best Practices\n","permalink":"https://developers.mattermost.com/integrate/plugins/components/webapp/","section":"integrate","subsection":"plugins","tags":null,"title":"Web app plugins"},{"categories":null,"contents":"Suppose you want to write an external application that is able to check the weather for certain cities. By creating a custom slash command and setting up the application to handle the HTTP POST or GET from the command, you can let your users check the weather in their city for the week using your command, say /weather toronto week.\nYou can follow these general guidelines to set up a custom Mattermost slash command for your application.\nOpen Product menu \u003e Integrations \u003e Slash Commands. If you don’t have the Integrations option in your Main Menu, slash commands may not be enabled on your Mattermost Server or may be disabled for non-admins. Enable them from System Console \u003e Integrations \u003e Integration Management or ask your Mattermost System Admin to do so.\nSelect Add Slash Command; the Add dialog will appear. Use the following guidelines to configure the slash command:\nSet the Title and Description for the command.\nSet the Command Trigger Word. The trigger word must be unique and cannot begin with a slash or contain any spaces. It also cannot be one of the built-in commands.\nSet the Request URL and Request Method. The request URL is the endpoint that Mattermost hits to reach your application, and the request method is either POST or GET and specifies the type of request sent to the request URL.\n(Optional) Set the Response Username and Response Icon the command will post messages as in Mattermost. If not set, the command will use your username and profile picture.\n(Optional) Select the Autocomplete option to include the slash command in the command autocomplete list, displayed when typing / in an empty input box. Use it to make your command easier to discover by your teammates. You can also provide a hint listing the arguments of your command and a short description displayed in the autocomplete list.\nNote: Enable integrations to override usernames must be set to true in config.json to override usernames. Enable them from System Console \u003e Integrations \u003e Integration Management, or ask your System Admin to do so. If not enabled, the username is set to webhook.\nSimilarly, Enable integrations to override profile picture icons must be set to true in config.json to override profile picture icons. Enable them from System Console \u003e Integrations \u003e Integration Management, or ask your System Admin to do so. If not enabled, the icon of the creator of the webhook URL is used to post messages.\nSelect Save. On the next page, copy the Token value. This will be used in a later step.\nNext, write your external application. Include a function which receives HTTP POST or HTTP GET requests from Mattermost. The request will look something like this:\nPOST /weather HTTP/1.1 Host: weather-service:4000 Accept: application/json Accept-Encoding: gzip Authorization: Token qzgakf1nx3yt9dr4n8585ihbxy Content-Length: 567 Content-Type: application/x-www-form-urlencoded User-Agent: Mattermost-Bot/1.1 channel_id=fukxanjgjbnp7ng383at53k1sy\u0026 channel_name=town-square\u0026 command=%2Fweather\u0026 response_url=http%3A%2F%2Flocalhost%3A8066%2Fhooks%2Fcommands%2Fi11f6nnfgfyk8eg56x9omc6dpa\u0026 team_domain=team-awesome\u0026 team_id=wx4zz8t4ttgmtxqiwfohijayzc\u0026 text=toronto+week\u0026 token=qzgakf1nx3yt9dr4n8585ihbxy\u0026 trigger_id=ZWZ5ZjRndzR4YmJxOHJlZWh4MXpkaHozbnI6ZXJqNnFjazNyZmd0dWpzODZ3NXI2cmNremg6MTY2MjA0MTY5Njg5NjpNRVFDSUQ5cTZ3MkRHU1RaNjhyaDh1TGl1STlSVHh2R1czSXZ5aGVRYjhkWThuZnlBaUI2YnlPR2ZpWlczR1FmVkdIODlreEp4MmlVT0UxMm9LMjlkZ1d0RC8xbjZRPT0%3D\u0026 user_id=erj6qck3rfgtujs86w5r6rckzh\u0026 user_name=alan If your integration sends back a JSON response, make sure it returns the application/json content-type.\nThe HTTP POST or GET request will contain an Authorization header with a bearer token. The bearer token should match the Token value from step 3 for a request to be considered valid.\nTo have your application post a message back to town-square, it can respond to the HTTP POST or GET request with a JSON payload.\nMattermost supports several parameters in the response to fine-tune the user’s experience. For instance, you can override the username and profile picture the messages post as, or specify a custom post type when sending a webhook message for use by plugins. Messages with advanced formatting can be created by including an attachment array and interactive message buttons in the response payload.\nOur external weather application could respond with a JSON payload like so:\n{\"response_type\": \"in_channel\", \"text\": \" --- #### Weather in Toronto, Ontario for the Week of February 16th, 2016 | Day | Description | High | Low | |:--------------------|:---------------------------------|:-------|:-------| | Monday, Feb. 15 | Cloudy with a chance of flurries | 3 °C | -12 °C | | Tuesday, Feb. 16 | Sunny | 4 °C | -8 °C | | Wednesday, Feb. 17 | Partly cloudly | 4 °C | -14 °C | | Thursday, Feb. 18 | Cloudy with a chance of rain | 2 °C | -13 °C | | Friday, Feb. 19 | Overcast | 5 °C | -7 °C | | Saturday, Feb. 20 | Sunny with cloudy patches | 7 °C | -4 °C | | Sunday, Feb. 21 | Partly cloudy | 6 °C | -9 °C | --- \"} The JSON response would render in Mattermost as:\nResponse parameters Slash command responses support more than just the text field. Here is a full list of supported parameters.\nParameter Description Required text Markdown-formatted message to display in the post. If attachments is not set, yes attachments Message attachments used for richer formatting options. If text is not set, yes response_type Set to blank or ephemeral to reply with a message that only the user can see. Set to in_channel to create a regular message. Defaults to ephemeral. No username Overrides the username the message posts as. Defaults to the username set during webhook creation or the webhook creator’s username if the former was not set. Must be enabled in the configuration. No channel_id Overrides the channel to which the message gets posted. Defaults to the channel in which the command was issued. No icon_url Overrides the profile picture the message posts with. Defaults to the URL set during webhook creation or the webhook creator’s profile picture if the former was not set. Must be enabled in the configuration. No goto_location A URL to redirect the user to. Supports many protocols, including http://, https://, ftp://, ssh:// and mailto://. No type Sets the post type, mainly for use by plugins. If not blank, must begin with custom_. Passing attachments will ignore this field and set the type to slack_attachment. No extra_responses An array of responses used to send more than one post in your response. Each item in this array takes the shape of its own command response, so it can include any of the other parameters listed here, except goto_location and extra_responses itself. Available from Mattermost v5.6. No skip_slack_parsing If set to true Mattermost will skip the Slack compatibility handling. Useful if the post contains text or code which is incorrectly handled by the Slack compatibility logic. Available from Mattermost v5.20. No props Sets the post props, a JSON property bag for storing extra or meta data on the post. Mainly used by other integrations accessing posts through the REST API.The following keys are reserved: from_webhook, override_username, override_icon_url and attachments. No An response payload using several parameters could look like this:\n{ \"response_type\": \"in_channel\", \"text\": \"\\n#### Test results for July 27th, 2017\\n@channel here are the requested test results.\\n\\n| Component | Tests Run | Tests Failed |\\n| ---------- | ----------- | ---------------------------------------------- |\\n| Server | 948 | ✅ 0 |\\n| Web Client | 123 | ⚠️ 2 [(see details)](https://linktologs) |\\n| iOS Client | 78 | ⚠️ 3 [(see details)](https://linktologs) |\\n\\t\\t \", \"username\": \"test-automation\", \"icon_url\": \"https://mattermost.com/wp-content/uploads/2022/02/icon.png\", \"props\": { \"test_data\": { \"ios\": 78, \"server\": 948, \"web\": 123 } }, } Delayed and multiple responses You can use the response_url parameter to supply multiple responses or a delayed response to a slash command. Response URLs can be used to send five additional messages within a 30-minute time period from the original command invocation.\nDelayed responses are useful when the action takes more than three seconds to perform. For instance:\nRetrieval of data from external third-party services, where the response time may take longer than three seconds. Report generation, batch processing or other long-running processes that take longer than three seconds to respond. Any requests that are made to the response URL should either be a plain text or JSON-encoded body. The JSON-encoded message supports both Markdown formatting and message attachments.\nFor information on the Slack compatibility of slash commands, see the Slack compatibility page.\n","permalink":"https://developers.mattermost.com/integrate/slash-commands/custom/","section":"integrate","subsection":"slash commands","tags":null,"title":"Custom commands"},{"categories":null,"contents":"Create an outgoing webhook Suppose you want to write an external application, which executes software tests after someone posts a message starting with the word #build in the town-square channel.\nYou can follow these general guidelines to set up a Mattermost outgoing webhook for your application.\nFirst, go to Product menu \u003e Integrations \u003e Outgoing Webhook. If you don’t have the Integrations option available, outgoing webhooks may not be enabled on your Mattermost server or may be disabled for non-admins. Enable them from System Console \u003e Integrations \u003e Integration Management or ask your System Admin to do so.\nSelect Add Outgoing Webhook and add name and description for the webhook. The description can be up to 500 characters.\nChoose the content type by which the request will be sent.\nIf application/x-www-form-urlencoded is chosen, the server will encode the parameters in a URL format in the request body. If application/json is chosen, the server will format the request body as JSON. Select the public channel to receive webhook responses, or specify one or more trigger words that send an HTTP POST request to your application. You may configure either the channel or the trigger words for the outgoing webhook, or both. If both are specified, then the message must match both values.\nIn our example, we would set the channel to town-square and specify #build as the trigger word.\nNote: If you leave the channel field blank, the webhook will respond to trigger words in all public channels of your team. Similarly, if you don’t specify trigger words, then the webhook will respond to all messages in the selected public channel. If you specified one or more trigger words on the previous step, choose when to trigger the outgoing webhook.\nIf the first word of a message matches one of the trigger words exactly, or If the first word of a message starts with one of the trigger words. Finally, set one or more callback URLs that HTTP POST requests will be sent to, then select Save. If the URL is private, add it as a trusted internal connection.\nOn the next page, copy the Token value. This will be used in a later step.\nUse an outgoing webhook Include a function in your application which receives HTTP POST requests from Mattermost. The POST request should look something like this:\nPOST /my-endpoint HTTP/1.1 Content-Length: 244 User-Agent: Go 1.1 package http Host: localhost:5000 Accept: application/json Content-Type: application/x-www-form-urlencoded channel_id=hawos4dqtby53pd64o4a4cmeoo\u0026 channel_name=town-square\u0026 team_domain=someteam\u0026 team_id=kwoknj9nwpypzgzy78wkw516qe\u0026 post_id=axdygg1957njfe5pu38saikdho\u0026 text=some+text+here\u0026 timestamp=1445532266\u0026 token=zmigewsanbbsdf59xnmduzypjc\u0026 trigger_word=some\u0026 user_id=rnina9994bde8mua79zqcg5hmo\u0026 user_name=somename If your integration sends back a JSON response, make sure it returns the application/json content-type.\nAdd a configurable MATTERMOST_TOKEN variable to your application and set it to the Token value from step 7. This value will be used by your application to confirm the HTTP POST request came from Mattermost.\nTo have your application post a message back to town-square, it can respond to the HTTP POST request with a JSON response such as:\n{\"text\": \" | Component | Tests Run | Tests Failed | |:-----------|:----------|:-----------------------------------------------| | Server | 948 | ✅ 0 | | Web Client | 123 | ⚠️ [2 (see details)](http://linktologs) | | iOS Client | 78 | ⚠️ [3 (see details)](http://linktologs) | \"} which would render in Mattermost as:\nYou’re all set!\nParameters Outgoing webhooks support more than just the text field. Here is a full list of supported parameters.\nParameter Description Required text Markdown-formatted message to display in the post.To trigger notifications, use @\u003cusername\u003e, @channel, and @here like you would in other Mattermost messages. If attachments is not set, yes response_type Set to comment to reply to the message that triggered it.Set to blank or post to create a regular message.Defaults to post. No username Overrides the username the message posts as.Defaults to the username set during webhook creation; if no username was set during creation, webhook is used.The Enable integrations to override usernames configuration setting must be enabled for the username override to take effect. No icon_url Overrides the profile picture the message posts with.Defaults to the URL set during webhook creation; if no icon was set during creation, the standard webhook icon () is displayed.The Enable integrations to override profile picture icons configuration setting must be enabled for the icon override to take effect. No attachments Message attachments used for richer formatting options. If text is not set, yes type Sets the post type, mainly for use by plugins.If not blank, must begin with “custom_”.Specifying a value for the attachments property will cause this field to be ignored, and the type value set to slack_attachment. No props Sets the post props, a JSON property bag for storing extra or meta data on the post.Mainly used by other integrations accessing posts through the REST API.The following keys are reserved: from_webhook, override_username, override_icon_url, webhook_display_name, and attachments. No priority Set the priority of the message. See Message Priority No An example response using more parameters would look like this:\nHTTP/1.1 200 OK Content-Type: application/json Content-Length: 755 { \"response_type\": \"comment\", \"username\": \"test-automation\", \"icon_url\": \"https://mattermost.com/wp-content/uploads/2022/02/icon.png\", \"text\": \"\\n#### Test results for July 27th, 2017\\n@channel here are the requested test results.\\n\\n| Component | Tests Run | Tests Failed |\\n| ---------- | ----------- | ---------------------------------------------- |\\n| Server | 948 | ✅ 0 |\\n| Web Client | 123 | ⚠️ 2 [(see details)](http://linktologs) |\\n| iOS Client | 78 | ⚠️ 3 [(see details)](http://linktologs) |\\n\\t\\t \", \"props\": { \"test_data\": { \"server\": 948, \"web\": 123, \"ios\": 78 } } } The response would produce a message like the following:\nMessages with advanced formatting can be created by including an attachment array and interactive message buttons in the JSON payload.\n","permalink":"https://developers.mattermost.com/integrate/webhooks/outgoing/","section":"integrate","subsection":"webhooks","tags":null,"title":"Outgoing webhooks"},{"categories":null,"contents":"Personal access tokens function similar to session tokens and can be used by integrations to authenticate against the REST API. It is the most commonly used type of token for integrations.\nCreate a personal access token Enable personal access tokens in System Console \u003e Integrations \u003e Integration Management.\nIdentify the account you want to create a personal access token with. You may optionally create a new user account for your integration, such as for a bot account. By default, only System Admins have permissions to create a personal access token.\nTo create an access token with a non-admin account, you must first give it the appropriate permissions. Go to System Console \u003e User Management \u003e Users, search for the user account, then select Manage Roles from the dropdown.\nSelect Allow this account to generate personal access tokens.\nYou may optionally allow the account to post to any channel in your Mattermost server, including direct messages by choosing the post:all role. post:channels role allows the account to post to any public channel in the Mattermost server.\nThen select Save.\nSign in to the user account to create a personal access token.\nGo to Profile \u003e Security \u003e Personal Access Tokens, then select Create Token.\nEnter a description for the token, so you remember what it’s used for. Then select Save.\nNote: If you create a personal access token for a System Admin account, be extra careful who you share it with. The token enables a user to have full access to the account, including System Admin privileges. It’s recommended to create a personal access token for non-admin accounts. Copy the access token now for your integration and store it in a secure location. You won’t be able to see it again!\nYou’re all set! You can now use the personal access token for integrations to interact with your Mattermost server and authenticate against the REST API.\nRevoke a personal access token A personal access token can be revoked by deleting the token from either the user’s profile settings or from the System Console. Once deleted, all sessions using the token are deleted, and any attempts to use the token to interact with the Mattermost server are blocked.\nTokens can also be temporarily deactivated from the user’s profile. Once deactivated, all sessions using the token are deleted, and any attempts to use the token to interact with the Mattermost server are blocked. However, the token can be reactivated at any time.\nUser’s profile Sign in to the user account, select the user avatar, then select Profile \u003e Security \u003e Personal Access Tokens.\nIdentify the access token you want to revoke, then select Delete and confirm the deletion.\nSystem Console Go to System Console \u003e User Management \u003e Users, search for the user account which the token belongs to, then select Manage Tokens from the dropdown.\nIdentify the access token you want to revoke, then select Delete and confirm the deletion.\nFrequently asked questions (FAQ) How do personal access tokens differ from regular session tokens? Personal access tokens do not expire. As a result, you can more easily integrate with Mattermost, bypassing the session length limits set in the System Console. Personal access tokens can be used to authenticate against the API more easily, including with AD/LDAP and SAML accounts. You can optionally assign additional roles for the account creating personal access tokens. This lets the account post to any channel in Mattermost, including direct messages. Besides the above differences, personal access tokens are exactly the same as regular session tokens. They are cryptic random IDs and are not different from a user’s regular session token created after logging in to Mattermost.\nCan I set personal access tokens to expire? Not in Mattermost, but you can automate your integration to cycle its token through the REST API.\nHow do I identify a badly behaving personal access token? The best option is to go to System Console \u003e Logs and finding error messages relating to a particular token ID. Once identified, you can search which user account the token ID belongs to in System Console \u003e Users and revoke it through the Manage Tokens dropdown option.\nDo personal access tokens continue to work if the user is deactivated? No. The session used by the personal access token is revoked immediately after a user is deactivated, and a new session won’t be created. The tokens are preserved and continue to function if the user account is re-activated. This is useful when a bot account is temporarily deactivated for troubleshooting, for instance.\n","permalink":"https://developers.mattermost.com/integrate/reference/personal-access-token/","section":"integrate","subsection":null,"tags":null,"title":"Personal access tokens"},{"categories":null,"contents":"Mattermost supports webhooks to easily integrate external applications into the server.\nIncoming webhooks Use incoming webhooks to post messages to Mattermost public channels, private channels, and direct messages. Messages are sent via an HTTP POST request to a Mattermost URL generated for each application and contain a specifically formatted JSON payload in the request body.\nCreate an incoming webhook\nOutgoing webhooks Outgoing webhooks will send an HTTP POST request to a web service and process a response back to Mattermost when a message matches one or both of the following conditions:\nIt’s posted in a specified channel. The first word matches or starts with one of the defined trigger words, such as gif. Outgoing webhooks are supported in public channels only. If you need a trigger that works in a private channel or a direct message, consider using a slash command instead.\nNote: To prevent malicious users from trying to perform phishing attacks a BOT indicator appears next to posts coming from webhooks regardless of what username is specified. Create an outgoing webhook\n","permalink":"https://developers.mattermost.com/integrate/webhooks/","section":"integrate","subsection":"webhooks","tags":null,"title":"Webhooks"},{"categories":null,"contents":"Common make commands for working with plugins make test - Runs the plugin’s server tests and webapp tests make check-style - Runs linting checks on the plugin’s server and webapp folders make deploy - Compiles the plugin using the make dist command, then automatically deploys the plugin to the Mattermost server. Enabling Local Mode on your server is the easiest way to use this command. make watch - Uses webpack’s watch feature to re-compile and deploy the webapp portion of your plugin on any change to the webapp/src folder. make dist - Compile the plugin into a g-zipped file, ready to upload to a Mattermost server. The file is saved in the plugin repo’s dist folder. make enable - Enables the plugin on the Mattermost server make disable - Disables the plugin on the Mattermost server. make reset - Disables and re-enables the plugin on the Mattermost server. make attach-headless - Starts a delve process and attaches it to your running plugin. make clean - Force deletes the content of build-related files. Use when running into build issues. You can run the development build of the plugin by setting the environment variable MM_DEBUG=1, or prefixing the variable at the beginning of the make command. For example, MM_DEBUG=1 make deploy will deploy the development build of the plugin to your server, allowing you to have a more fluid debugging experience. To use the production build of the plugin instead, unset the MM_DEBUG environment variable before running the make commands.\nDevelop in the plugin’s webapp folder In order for your IDE to know the root directory of the plugin’s webapp code, it is advantageous to open the IDE in the webapp folder itself when working on the webapp portion of the plugin. This way, the IDE is aware of files such as webpack.config.js and tsconfig.json.\nExpose the Mattermost server using ngrok When a plugin integrates with an external service, webhooks and/or authentication redirects are necessary, which requires your local server to be available on the web. In order for your Mattermost server to be available to process webhook requests, it needs to expose its port to an external address. A common way to do this is to use the command line tool ngrok. Follow these steps to set up ngrok with your server:\nDownload the ngrok tool from here. Put the executable somewhere within your shell’s PATH. With your Mattermost server already running, use the command ngrok http 8065 to make your Mattermost server available for webhook requests. Visit the https URL from the ngrok command’s output, and log into Mattermost. Set your Mattermost server’s Site URL to the https address given from the ngrok command output. Monitor incoming webhook requests with ngrok’s request inspector. Visit http://localhost:4040 once you have your tunnel open. You can analyze the contents of the HTTP request from the external service, and the response from your plugin. If you’re using a free ngrok account, the URL given by the output of the ngrok http command will be different each time you run the command. As a result, you’ll need to adjust the webhook URL on Mattermost’s side and the external service’s side (e.g. GitHub) each time you run the command.\nWith this setup, many integrations require you to be logged into Mattermost using your ngrok URL. After logging into your ngrok URL pointed to your Mattermost server, in most cases you can continue using your localhost address in your browser for quicker network requests to your server. If you receive an error like unauthorized or enable third-party cookies when connecting to an external service, make sure you’re logged into your ngrok URL in the same browser.\nUse localhost.run instead of ngrok If you would like to avoid using ngrok, there is another free option that you can run from your terminal, called localhost.run. Use this command to expose your server:\nssh -R 80:localhost:8065 ssh.localhost.run An http URL pointing to your server should show in the terminal. The https version of this same URL should also work, which is what you will want to use for your webhook URLs. One disadvantage of using localhost.run is there is no request/response logging dashboard that is available with ngrok.\nDebug server-side plugins using delve Using the delve debugger, we can step through code for a running plugin on our local Mattermost server. There are a few steps for setup to make this work properly.\nConfigure Mattermost server for debugging plugins In order to allow the debugger to pause code execution, we need to disable Mattermost’s “health check” for plugins and the Hashicorp go-plugin package’s “keep alive” feature for its RPC connection. We’ll configure the server with the following steps:\nIn the server’s config.json, set PluginSettings.EnableHealthCheck to false Run the script below with ./patch_go_plugin.sh $GO_PLUGIN_PACKAGE_VERSION where GO_PLUGIN_PACKAGE_VERSION is the version of go-plugin that your Mattermost server is using. This can be found in the monorepo at server/go.mod on your local server. Restart the server More details on this are explained below:\nDisable Mattermost plugin health check job To disable the Mattermost plugin health check job, go into your config.json and set PluginSettings.EnableHealthCheck to false. Note that this will make it so if your plugin panics/crashes for any reason during your development, the Mattermost server will not restart the plugin or notice that it crashed. It will remain with the status of “running” in the plugin management page, even though it has crashed. Because of this, you’ll need to watch server logs for any information related to plugin panics during your debugging.\nDisable go-plugin RPC client “keep alive” We’ll be editing external library source code directly, so we only want to do this in a development environment.\nBy default, the go-plugin package runs plugins with a “keep alive” feature enabled, which essentially pings the plugin RPC connection every 30 seconds, and if the plugin doesn’t respond, the RPC connection will be terminated. There is a way to disable this, though the go-plugin currently doesn’t expose a way to configure this setting, so we need to edit the go-plugin package’s source code to have the right configuration for our debugging use case.\nIn the script below, we automatically modify the file located at ${GOPATH}/pkg/mod/github.com/hashicorp/go-plugin@${GO_PLUGIN_PACKAGE_VERSION}/rpc_client.go, where GO_PLUGIN_PACKAGE_VERSION is the version of go-plugin that your Mattermost server is using. This can be found in your local copy of the monorepo at server/go.mod. This script essentially replaces the line in go-plugin/rpc_client.go to have a custom configuration for the RPC client connection, that disables the “keep alive” feature. This makes it so the debugger can be paused for long amounts of time, and the Mattermost server will keep the connection with the plugin open.\n# patch_go_plugin.sh GO_PLUGIN_PACKAGE_VERSION=$1 GO_PLUGIN_RPC_CLIENT_PATH=${GOPATH}/pkg/mod/github.com/hashicorp/go-plugin@${GO_PLUGIN_PACKAGE_VERSION}/rpc_client.go echo \"Patching $GO_PLUGIN_RPC_CLIENT_PATH for debugging Mattermost plugins\" if ! grep -q 'mux, err := yamux.Client(conn, nil)' \"$GO_PLUGIN_RPC_CLIENT_PATH\"; then echo \"The file has already been patched or the target line was not found.\" exit 0 fi sudo sudo sed -i '' '/import (/a\\ \"time\" ' $GO_PLUGIN_RPC_CLIENT_PATH sudo sed -i '' '/mux, err := yamux.Client(conn, nil)/c\\ sessionConfig := yamux.DefaultConfig()\\ sessionConfig.EnableKeepAlive = false\\ sessionConfig.ConnectionWriteTimeout = time.Minute * 5\\ mux, err := yamux.Client(conn, sessionConfig) ' $GO_PLUGIN_RPC_CLIENT_PATH echo \"Patched go-plugin's rpc_client.go for debugging Mattermost plugins\" Then run the script like so:\nchmod +x patch_go_plugin.sh ./patch_go_plugin.sh v1.6.0 # as of writing, this is the version of `go-plugin` being used by the server Configure VSCode for debugging This section assumes you are using VSCode to debug your plugin. If you want to use a different IDE, the process will be mostly the same. If you want to debug in your terminal directly with delve instead of using an IDE, you can run make attach instead of make attach-headless below, which will launch a delve process as an interactive terminal.\nInclude this configuration in your VSCode instance’s launch.json:\n{ \"name\": \"Attach to Mattermost plugin\", \"type\": \"go\", \"request\": \"attach\", \"mode\": \"remote\", \"port\": 2346, \"host\": \"127.0.0.1\", \"apiVersion\": 2 } Attach headless delve process to the running plugin Build the plugin and deploy to your local Mattermost server:\nmake deploy In a separate terminal, open a delve process for VSCode to connect to:\nmake attach-headless This starts a headless delve process for your IDE to connect to. The process listens on port 2346, which is the port defined in our launch.json configuration. Somewhat related, the Mattermost server’s Makefile has a command debug-server-headless, which starts a headless delve process for the Mattermost server, listening on port 2345. So you can create a similar launch.json configuration in the server directory of the monorepo to connect to your server by using that port.\nAttach the debugger to the delve process Run the debugger in VSCode by navigating to the “Run and Debug” tab on the left side of the IDE, selecting your launch configuration, and clicking the green play button. This should bring up a debugging widget that looks like this:\nYour IDE’s debugger is now running and ready to pause your plugin’s execution at any breakpoints you set in the IDE.\nTroubleshooting If you run into issues with debugging, first make sure you’ve stopped any active debugging sessions by clicking the red disconnect button in the VSCode debugging widget.\nYou can then use the make reset command in the plugin repository to do the following:\nDisable and re-enable the plugin Terminate any running delve processes running for this plugin For more discussion on this, please join the Toolkit channel on our community server: https://community.mattermost.com/core/channels/developer-toolkit\n","permalink":"https://developers.mattermost.com/integrate/plugins/developer-workflow/","section":"integrate","subsection":"plugins","tags":null,"title":"Developer workflow"},{"categories":null,"contents":"Server plugins may use interactive dialogs and interactive messages to build cross platform interactions that are compatible with mobile.\nIt isn’t possible to customize the mobile user experience in the same way the webapp can be extended.\n","permalink":"https://developers.mattermost.com/integrate/plugins/components/mobile/","section":"integrate","subsection":"plugins","tags":null,"title":"Mobile plugins"},{"categories":null,"contents":"Slash commands are messages that begin with / and trigger an HTTP request to a web service that can in turn post one or more messages in response.\nSlash commands have an additional feature, autocomplete, that displays a list of possible commands based on what has been typed in the message box. Typing / in an empty message box will display a list of all slash commands. As the slash command is typed in the message box, autocomplete will also display possible arguments and flags for the command.\nUnlike outgoing webhooks, slash commands work in private channels and direct messages in addition to public channels, and can be configured to auto-complete when typing. Mattermost includes a number of built-in slash commands. You can also create custom slash commands.\nTips and best practices Slash commands are designed to easily allow you to post messages. For other actions such as channel creation, you must also use the Mattermost APIs. Posts size is limited to 16383 characters for servers running Mattermost Server v5.0 or later. Use the extra_responses field to reply to a triggered slash command with more than one post. You can restrict who can create slash commands in System Console \u003e Integrations \u003e Integration Management. Mattermost outgoing webhooks are Slack-compatible. You can copy-and-paste code used for a Slack outgoing webhook to create Mattermost integrations. Mattermost automatically translates Slack’s proprietary JSON payload format. The external application may be written in any programming language. It needs to provide a URL which receives the request sent by your Mattermost Server and responds with in the required JSON format. FAQ How do I debug slash commands? To debug slash commands in System Console \u003e Logs, set System Console \u003e Logging \u003e Enable Webhook Debugging to true and set System Console \u003e Logging \u003e Console Log Level to DEBUG.\nHow do I send multiple responses from a slash command? You can send multiple responses with an extra_responses parameter as follows.\nHTTP/1.1 200 OK Content-Type: application/json Content-Length: 696 { \"response_type\": \"in_channel\", \"text\": \"\\n#### Test results for July 27th, 2017\\n@channel here are the requested test results.\\n\\n| Component | Tests Run | Tests Failed |\\n| ---------- | ----------- | ---------------------------------------------- |\\n| Server | 948 | ✅ 0 |\\n| Web Client | 123 | ⚠️ 2 [(see details)](http://linktologs) |\\n| iOS Client | 78 | ⚠️ 3 [(see details)](http://linktologs) |\\n\\t\\t \", \"username\": \"test-automation\", \"icon_url\": \"https://www.mattermost.org/wp-content/uploads/2016/04/icon.png\", \"props\": { \"test_data\": { \"ios\": 78, \"server\": 948, \"web\": 123 } }, \"extra_responses\": [ { \"text\": \"message 2\", \"username\": \"test-automation\" }, { \"text\": \"message 3\", \"username\": \"test-automation\" } ] } What if my slash command takes time to build a response? Reply immediately with an ephemeral message to confirm response of the command, and then use the response_url to send up to five additional messages within a 30-minute time period from the original command invocation.\nWhy does my slash command fail to connect to localhost? By default, Mattermost prohibits outgoing connections that resolve to certain common IP ranges, including the loopback (127.0.0.0/8) and various private-use subnets.\nDuring development, you may override this behaviour by setting ServiceSettings.AllowedUntrustedInternalConnections to \"127.0.0.0/8\" in your config.json or via System Console \u003e Advanced \u003e Developer. See the configuration settings documentation for more details.\nShould I configure my slash command to use POST or GET? Best practice suggests using GET only if a request is considered idempotent. This means that the request can be repeated safely and can be expected to return the same response for a given input. Some servers hosting your slash command may also impose a limit to the amount of data passed through the query string of a GET request.\nUltimately, however, the choice is yours. If in doubt, configure your slash command to use a POST request.\nMattermost releases prior to 5.0.0 Note that slash commands configured to use a GET request were broken prior to Mattermost release 5.0.0. The payload was incorrectly encoded in the body of the GET request instead of in the query string. While it was still technically possible to extract the payload, this was non-standard and broke some development stacks. Why does my slash command always fail with returned an empty response? If you are emitting the Content-Type: application/json header, your body must be valid JSON. Any JSON decoding failure will result in this error message.\nAlso, you must provide a response_type. Mattermost does not assume a default if this field is missing.\nWhy does my slash command print the JSON data instead of formatting it? Ensure you are emitting the Content-Type: application/json header, otherwise your body will be treated as plain text and posted as such.\nAre slash commands Slack-compatible? See the Slack compatibility page.\nHow do I use Bot Accounts to reply to slash commands? If you are developing an integration Set up a Personal Access Token for the Bot Account you want to reply with. Use the REST API to create a post with the Access Token. If you are developing a plugin Use CreatePost plugin API. Make sure to set the UserId of the post to the UserId of the Bot Account. If you want to create an ephemeral post, use SendEphemeralPost plugin API instead.\nTroubleshoot slash commands Join the Mattermost user community for help troubleshooting your slash command.\nShare your integration If you’ve built an integration for Mattermost, please consider sharing your work in our Marketplace.\nThe Marketplace lists open source integrations developed by the Mattermost community and are available for download, customization, and deployment to your private cloud or self-hosted infrastructure.\n","permalink":"https://developers.mattermost.com/integrate/slash-commands/","section":"integrate","subsection":"slash commands","tags":null,"title":"Slash commands"},{"categories":null,"contents":"Bot accounts access the Mattermost RESTful API on behalf of a bot through the use of the personal access tokens feature. Bot accounts are just like user accounts, except they:\nCan’t be logged into. Can’t be used to create other bot accounts. Can’t be used to upload files to a channel, unless they are also added to the channel as a channel member. Don’t count as a registered user and therefore don’t count towards the total number of users for an Enterprise Edition license. Additional benefits include:\nSystem Admins can enable bot accounts to post to any channel in the system, including private teams, Private channels, or Direct Messages. Integrations created by a user and tied to a bot account no longer break if the user leaves the company. Once created, bot accounts behave just like regular user accounts and can be added to teams and channels similar to users. Bot accounts are a safer way to integrate with Mattermost through the RESTful API and Plugin API because there is no need to manage shared logins with these accounts. A BOT tag is used throughout Mattermost where bot accounts are referenced, including messages and user lists. Note that currently:\nOnly System Admins or plugins can create or manage bot accounts. In Mattermost Enterprise Edition, service accounts without an email address pulled from LDAP or SAML systems are not yet supported. If you would like to see improvements to bot accounts, let us know in the Feature Proposal Forum.\nConfiguration settings By default, plugins can create and manage bot accounts. To enable bot account creation through the user interface or the RESTful API:\nGo to System Console \u003e Integrations \u003e Bot Accounts. Set Enable Bot Account Creation to true. Once set, System Admin can create bot accounts for integrations using the Integrations \u003e Bot Accounts link in the description provided.\nBot account creation Below are different ways to create bot accounts. After the bot account is created, make sure to:\nCopy the generated bot access token for your integration. To add the bot account to teams and channels you want it to interact in, select the team drop-down menu, then select Invite People. Next, select Invite Member and enter the bot account in the Add or Invite People field. Then select Invite Members. You should now be able to add the bot account to channels like any other user. User interface (UI) Go to the Product menu and select Integrations \u003e Bot Accounts. Select Add Bot Account. Set the Username of the bot. The username must begin with a letter, and contain between 3 and 22 lowercase characters made up of numbers, letters, and symbols including “.”, “-”, or “_”. (Optional) Upload an image for the Bot Icon. This will be used as the profile image of the bot throughout Mattermost. (Optional) Set a Display Name and Description. (Optional) Choose what role the bot should have. Defaults to Member. If you assign System Admin, the bot will have read and write access for any Public channels, Private channels, and Direct Messages. (Optional) Select additional permissions for the account. Enable the bot to post to all Mattermost channels, or post to all Mattermost Public channels. Select Create Bot Account. Copy the token displayed on the Setup Successful page before you select Done as you won’t have access to the token again once you close the screen. RESTful API Use the RESTful API POST /bots to create a bot. You must have permissions to create bots.\nSee our API documentation to learn more about creating and managing bots through the API.\nTo authorize your bot via RESTful API use curl -i -H 'authorization: Bearer \u003cAccess Token\u003e' http://localhost:8065/api/v4/users/me. Access Token is not the Token ID and won’t be visible again once created.\nCommand line interface (CLI) You can use the following CLI command to convert an existing user account to a bot:\nuser convert user@example.com --bot In addition to email, you can identify the user by its username or user ID.\nBot accounts which were converted from user accounts will have their authentication data cleared if they were email/password accounts. Those synchronized from LDAP/SAML will not have their authentication data cleared so that LDAP/SAML synchronization performs correctly.\nPlugins Plugins can create bot accounts through an EnsureBot helper function. For an example, see the Demo Plugin.\nBots created by a plugin use the plugin’s ID as the creator, unless otherwise specified by the plugin.\nTechnical notes Data model Each bot account has a row in the Users table and the Bots table. The entries are tied together by User.Id = Bot.UserId.\nThe Bots table schema is described as follows:\nField Description Type Required UserId User ID of the bot user string Y Username Username of the bot account string Y DisplayName Display name of the bot account string N Description Description of the bot account string N OwnerId User ID of the owner of the bot string Y CreateAt Unix timestamp of creation time int64 Y UpdateAt Unix timestamp of update time int64 Y DeleteAt Unix timestamp of deletion time int64 Y Frequently asked questions Should I migrate all my integrations to use bot accounts? For your integrations using RESTful API and plugins, yes. To do so, you can either convert an existing account to a bot, or create a new bot account using the steps outlined above.\nOnce you create a bot account, use the generated token to access the RESTful API on behalf of a bot and interact in the Mattermost server.\nFor your webhook and slash command integrations, you cannot migrate them to use bot accounts, as they require a user account at this time. However, an option is to migrate the webhooks or slash commands to a plugin, which in turn can use bot accounts.\nWhat happens if a plugin is using a bot account that already exists as a user account? For a concrete example, suppose you enable the Mattermost GitHub plugin, which uses a github bot account, while an existing github user account was created for webhook integrations.\nOnce the plugin is enabled, the plugin posts as the github account but without a BOT tag. An error message is logged to the server logs recommending the System Admin to convert the github user to a bot account by running mattermost user convert \u003cusername\u003e --bot in the CLI.\nIf the user is an existing user account you want to preserve, change its username and restart the Mattermost server, after which the plugin will create a bot account with the name github.\nHow do I convert an existing account to a bot account? Use the following CLI command to convert an existing user account to a bot:\nuser convert user@example.com --bot\nIn addition to email, you may identify the user by its username or user ID.\nBot accounts which were converted from user accounts will have their authentication data cleared if they were email/password accounts. Those synchronized from LDAP/SAML will not have their authentication data cleared so that LDAP/SAML synchronization performs correctly.\nHow can I quickly test if my bot account is working? Add the bot to a team and channel you belong to, then use the following curl command to post with the bot:\ncurl -i -X POST -H 'Content-Type: application/json' -d '{\"channel_id\":\"\u003cchannel-id\u003e\", \"message\":\"This is a message from a bot\", \"props\":{\"attachments\": [{\"pretext\": \"Look some text\",\"text\": \"This is text\"}]}}' -H 'Authorization: Bearer \u003cbot-access-token\u003e' \u003cmattermost-url\u003e/api/v4/posts\nReplace the following parameters:\n\u003cchannel-id\u003e with the channel you added the bot to \u003cbot-access-token\u003e with the bot access token generated when you created the bot account \u003cmattermost-url\u003e with your Mattermost domain, e.g. https://example.mattermost.com Do bot access tokens expire? No, but you can automate your integration to cycle its token through the REST API:\nUse GetUserAccessTokensForUser to list the existing tokens for the bot user. Use CreateUserAccessToken to generate a new token. Update the services that leverage the token with the new token value from step 1. Use RevokeUserAccessToken to revoke the old token based on the token_id. For more information about access tokens, see the personal access tokens documentation.\nDo bot accounts make it easier to impersonate someone else such as the CEO or an HR coordinator? Possibly yes. Currently a System Admin can disable overriding the profile picture and the username from integrations to help prevent impersonation, but this is not the case for bot accounts.\nMitigations:\nBOT tag is used everywhere in the UI where bot accounts are referenced, including messages and user lists. For Direct Message channels, the channel header distinguishes the bot from a regular user account with a BOT tag. What happens when a user who owns bot accounts is disabled? By default, bot accounts managed by the deactivated user are disabled for enhanced security. Those with permissions to manage bot accounts can re-enable them via the Product menu \u003e Integrations \u003e Bot Accounts.\nWe strongly recommend creating new tokens for the bot, to ensure the user who was deactivated no longer has access to read or write data in the system via the bot access token.\nIf you prefer to have bot accounts remain enabled after user deactivation, set DisableBotsWhenOwnerIsDeactivated to false in your config.json file.\nCan bot accounts edit messages through the RESTful API? Yes. By default, bot accounts can update their own posts.\nIf you find yourself unable to edit posts as a bot, check the following:\nInstead of using a slash command to respond directly, use an an API call for the initial interaction with a user to enable message edits. If your system is using advanced permissions, then post edits could be disabled for users. If neither of the above help resolve your issue, you also have the option to choose what role the bot account has. If System Admin is chosen, then the bot can update any posts in the system. Note that giving the System Admin role to a bot account enables the bot with other System Admin privileges so this should be done with care.\nIf AD/LDAP or SAML synchronization is enabled, do bot accounts need to have an associated email address in AD/LDAP or SAML? When AD/LDAP or SAML synchronization is enabled, you can create bot accounts using the steps outlined above. These bot accounts won’t require an email address.\nIf you need to sync service accounts from AD/LDAP or SAML to Mattermost and use them as bot accounts, please reach out to us to discuss in detail. You may not need to sync service accounts and use them as bot accounts to meet your use case.\nHow are bot accounts identified in compliance exports? As of v5.14, a field named UserType is added to Compliance Exports, including Global Relay, Actiance, and CSV. The field identifies whether a message was posted by a user or by a bot account.\n","permalink":"https://developers.mattermost.com/integrate/reference/bot-accounts/","section":"integrate","subsection":null,"tags":null,"title":"Bot accounts"},{"categories":null,"contents":"From Mattermost v9.6, you can integrate Mattermost with custom integrations hosted within your internal OAuth infrastructure. More specifically, slash commands now support communicating to external systems using the Client Credentials OAuth 2.0 grant type. In a future iteration of this feature, we plan to support outgoing webhooks, interactive dialogs, and interactive messages.\nTo configure Mattermost to communicate with your OAuth system, you first need to create an Outgoing OAuth Connection in Mattermost. This involves providing the following values in the connection configuration form:\nClient Id \u0026 Client Secret Token URL - The URL that Mattermost will contact to retrieve a new token for each request to your integration. Audience URLs - The URLs that your custom integration hosts to process requests, e.g. slash command submission requests. Once this is set up, whenever Mattermost sends a request to your custom integration, Mattermost will first fetch an access token from the Token URL, and include the access token in the request to your integration. A new token will be requested for every request sent to your integration.\nConfigure the Outgoing OAuth Connection To configure a new outgoing OAuth connection, navigate to your Mattermost server’s integrations backstage console, as shown below, then go to the Outgoing OAuth 2.0 Connections tab, and select Add Outgoing OAuth Connection. If you don’t have the Integrations option, integrations may not be enabled on your Mattermost Server, or integrations may be disabled for non-admins. Enable integrations by going to System Console \u003e Integrations \u003e Integration Management or ask your Mattermost system admin to do so.\nNow we can configure our OAuth connection. With the values configured in the example below, Mattermost will perform the following operations when a user interacts with your integration, e.g. when the user submits a slash command associated with your integration.\nMattermost will request an access token from Token URL, by providing the Client Id and Client Secret via a URL-encoded form submission. Mattermost will send a request to your slash command submission handler, with the access token in the request’s Authorization HTTP header, in the format of Bearer (token). In the future, any further requests to your integration (such as interactive dialog submissions) will perform the above steps as well.\nNotice that the Audience URL configured below ends in a wildcard *. This means that an access token will be retrieved for any integration request that matches that URL, i.e. any URL that has the prefix of the string leading up to the * character. If you would like to restrict the OAuth connection to exact URLs, simply omit the *, and supply the exact URLs you would like to configure for your integration. You can come back to this form to update these values at any time. Note that the Client Secret will not be shown again when you return to this form.\nYou can validate that the connection is set up properly by selecting the Validate Connection button. This will cause Mattermost to request a token from the Token URL using the provided credentials. If a token is retrieved successfully, a Validated Connection message will be shown in the UI. If the token retrieval was unsuccessful for any reason, an error will be shown in the UI. Note that validating the connection here is optional, though a warning will be shown upon submitting the form if the connection was not validated before submission.\nConfigure a Slash Command to use the Outgoing OAuth Connection In order to configure your custom Slash Command to use the OAuth connection, we just need to provide a request URL that matches the Audience URL provided in the OAuth connection form. If you have at least one OAuth connection configured on your Mattermost instance, you will see a message in the slash command configuration form stating whether or not your slash command will use a configured OAuth connection.\n","permalink":"https://developers.mattermost.com/integrate/slash-commands/outgoing-oauth-connections/","section":"integrate","subsection":"slash commands","tags":null,"title":"Outgoing OAuth connections"},{"categories":null,"contents":"There are a couple of slash-commands available on GitHub which are implemented via Mattermod. They only work on PRs.\nThe commands are:\n/cherry-pick $BRANCH_NAME, e.g. /cherry-pick release-5.10: Opens a PR to cherry-pick a change into the branch $BRANCH_NAME. This command only works for the submitter of the PR and members of the Mattermost organization. /check-cla: Checks if the PR contributor has signed the CLA. /autoassign: Automatically assigns reviewers to a PR. /update-branch: Updates the pull request branch with the latest upstream changes by merging HEAD from the base branch into the pull request branch. This command only works for members of the Mattermost organization. ","permalink":"https://developers.mattermost.com/contribute/more-info/getting-started/slash-commands/","section":"contribute","subsection":"more info","tags":null,"title":"Slash commands"},{"categories":null,"contents":"A Mattermost plugin consists of several components:\nServer: subprocesses invoked by the Mattermost Server that communicate using RPC Webapp: extensions/modifications to the Mattermost web and desktop apps Mobile: mobile device support ","permalink":"https://developers.mattermost.com/integrate/plugins/components/","section":"integrate","subsection":"plugins","tags":null,"title":"Components"},{"categories":null,"contents":"Mattermost supports plugins that offer powerful features for extending and deeply integrating with both the Server and Web/Desktop Apps.\nShare constructive feedback on our forum post or join the Toolkit channel on our Mattermost community server.\nFeatures Customize user interfaces Write a Web App plugin to add to the channel header, sidebars, main menu, and more. Register your plugin against a post type to render custom posts or wire up a root component to build an entirely new experience. All this is possible without having to fork the source code and rebase on every Mattermost release.\nLaunch tightly-integrated services Launch and manage Server plugins as services from your Mattermost server over RPC. Handle events via real-time hooks and invoke Mattermost server methods directly using a dedicated plugin API.\nExtend the Mattermost REST API Extend the Mattermost REST API with custom endpoints for use by Web App plugins or third-party services. Custom endpoints have access to all the features of the standard Mattermost REST API, including personal access tokens and OAuth 2.0.\nTip: See the Mattermost Server SDK Reference and Mattermost Client UI SDK Reference documentation for details on available server API endpoints and client methods. Simple development and installation It’s simple to set up a plugin development environment with the mattermost-plugin-starter-template. Just select “Use this template” when cloning the repository. Please see the developer setup and developer workflow pages for more information.\nRead the plugins overview to learn more.\n","permalink":"https://developers.mattermost.com/integrate/plugins/","section":"integrate","subsection":"plugins","tags":null,"title":"Plugins"},{"categories":null,"contents":"For additional formatting options, and for compatibility with Slack non-markdown integrations, an attachments array can be sent by integrations and rendered by Mattermost.\nYou can also add interactive message buttons as part of attachments. They help make your integrations richer by completing common tasks inside Mattermost conversations, increasing user engagement and productivity. For more information, see documentation.\nAttachment options When sending an attachment, you can use any of the following to format how you want the posted message to look.\nfallback: A required plain-text summary of the attachment. This is used in notifications, and in clients that don’t support formatted text (e.g. IRC).\ncolor: A hex color code that will be used as the left border color for the attachment. If not specified, it will default to match the channel sidebar header background color.\npretext: An optional line of text that will be shown above the attachment. Supports @mentions.\ntext: The text to be included in the attachment. It can be formatted using Markdown. For long texts, the message is collapsed and a “Show More” link is added to expand the message. Supports @mentions.\nAuthor details author_name: An optional name used to identify the author. It will be included in a small section at the top of the attachment.\nauthor_link: An optional URL used to hyperlink the author_name. If no author_name is specified, this field does nothing.\nauthor_icon: An optional URL used to display a 16x16 pixel icon beside the author_name.\nTitles title: An optional title displayed below the author information in the attachment.\ntitle_link: An optional URL used to hyperlink the title. If no title is specified, this field does nothing.\nFields Fields can be included as an optional array within attachments, and are used to display information in a table format inside the attachment.\ntitle: A title shown in the table above the value. As of v5.14 a title will render emojis properly.\nvalue: The text value of the field. It can be formatted using Markdown. Supports @mentions.\nshort: Optionally set to true or false (boolean) to indicate whether the value is short enough to be displayed beside other values.\nImages image_url: An optional URL to an image file (GIF, JPEG, PNG, BMP, or SVG) that is displayed inside a message attachment.\nLarge images are resized to a maximum width of 400px or a maximum height of 300px, while still maintaining the original aspect ratio.\nthumb_url: An optional URL to an image file (GIF, JPEG, PNG, BMP, or SVG) that is displayed as a 75x75 pixel thumbnail on the right side of an attachment. We recommend using an image that is already 75x75 pixels, but larger images will be scaled down with the aspect ratio maintained.\nExample message attachment Here is an example message attachment:\n{ \"attachments\": [ { \"fallback\": \"test\", \"color\": \"#FF8000\", \"pretext\": \"This is optional pretext that shows above the attachment.\", \"text\": \"This is the text of the attachment. It should appear just above an image of the Mattermost logo. The left border of the attachment should be colored orange, and below the image it should include additional fields that are formatted in columns. At the top of the attachment, there should be an author name followed by a bolded title. Both the author name and the title should be hyperlinks.\", \"author_name\": \"Mattermost\", \"author_icon\": \"https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png\", \"author_link\": \"https://mattermost.org/\", \"title\": \"Example Attachment\", \"title_link\": \"https://developers.mattermost.com/integrate/reference/message-attachments/\", \"fields\": [ { \"short\":false, \"title\":\"Long Field\", \"value\":\"Testing with a very long piece of text that will take up the whole width of the table. And then some more text to make it extra long.\" }, { \"short\":true, \"title\":\"Column One\", \"value\":\"Testing\" }, { \"short\":true, \"title\":\"Column Two\", \"value\":\"Testing\" }, { \"short\":false, \"title\":\"Another Field\", \"value\":\"Testing\" } ], \"image_url\": \"https://mattermost.com/wp-content/uploads/2022/02/icon_WS.png\" } ] } And here is how it renders in Mattermost:\nFooter footer: An optional line of text that will be displayed at the bottom of the attachment. Footers with more than 300 characters will be truncated with an ellipsis (…).\nfooter_icon: An optional URL to an image file (GIF, JPEG, PNG, BMP, or SVG) that is displayed as a 16x16 pixel thumbnail before the footer text.\nKnown issues The footer timestamp field (ts) is not yet supported. Message Attachment contents do not show up in search. Frequently asked questions Can I send a message attachment via the API? Yes, you can use the create post RESTful API.\nYou need to add an attachments key to the post’s props JSON field. The value is an array of message attachments you want attached to the post. See below for an example curl command.\ncurl -i -X POST -H 'Content-Type: application/json' -d '{\"channel_id\":\"qmd5oqtwoibz8cuzxzg5ekshgr\", \"message\":\"Test message #testing\", \"props\":{\"attachments\": [{\"pretext\": \"This is the attachment pretext.\",\"text\": \"This is the attachment text.\"}]}}' https://{your-mattermost-site}/api/v4/posts\nBelow is an example HTTP request:\nPOST /api/v4/posts HTTP/1.1 Host: {your-mattermost-site} User-Agent: curl/7.63.0 Accept: */* Content-Type: application/json Content-Length: 192 {\"channel_id\":\"qmd5oqtwoibz8cuzxzg5ekshgr\", \"message\":\"Test message #testing\", \"props\":{\"attachments\": [{\"pretext\": \"This is the attachment pretext.\",\"text\": \"This is the attachment text.\"}]}} Can I post a message attachment using a webhook? Yes, you can also use an incoming webhook to post a message attachment.\nYou need to add an attachments key to the post’s JSON. The value is an array of message attachments you want attached to the post. See below for an example curl command.\ncurl -i -X POST -H 'Content-Type: application/json' -d '{\"text\":\"Test message #testing\", \"attachments\": [{\"pretext\": \"This is the attachment pretext.\",\"text\": \"This is the attachment text.\"}]}' https://{your-mattermost-site}/hooks/{your-webhook-id}\nBelow is an example HTTP request:\nPOST /hooks/{your-webhook-id} HTTP/1.1 Host: {your-mattermost-site} User-Agent: curl/7.63.0 Accept: */* Content-Type: application/json Content-Length: 192 {\"text\":\"Test message #testing\", \"attachments\": [{\"pretext\": \"This is the attachment pretext.\",\"text\": \"This is the attachment text.\"}]} ","permalink":"https://developers.mattermost.com/integrate/reference/message-attachments/","section":"integrate","subsection":null,"tags":null,"title":"Message attachments"},{"categories":null,"contents":"Mattermost makes it easy to migrate integrations written for Slack to Mattermost.\nTranslate Slack’s data format to Mattermost Mattermost automatically translates the data coming from Slack:\nJSON responses written for Slack, that contain the following, are translated to Mattermost markdown and rendered equivalently to Slack:\n\u003c\u003e to denote a URL link, such as {\"text\": \"\u003chttps://mattermost.com/\u003e\"}. | within a \u003c\u003e to define linked text, such as {\"text\": \"Click \u003chttps://mattermost.com/|here\u003e for a link.\"}. \u003cuserid\u003e to trigger a mention to a user, such as {\"text\": \"\u003c5fb5f7iw8tfrfcwssd1xmx3j7y\u003e this is a notification.\"}. \u003c!channel\u003e, \u003c!here\u003e, or \u003c!all\u003e to trigger a mention to a channel, such as {\"text\": \"\u003c!channel\u003e this is a notification.\"}. Both the HTTP POST and GET request bodies sent to a web service are formatted the same as Slack’s. This means your Slack integration’s receiving function does not need change to be compatible with Mattermost.\nKnown Slack compatibility issues Using icon_emoji to override the username is not supported. Referencing channels using \u003c#CHANNEL_ID\u003e does not link to the channel. \u003c!everyone\u003e and \u003c!group\u003e are not supported. Parameters mrkdwn, parse, and link_names are not supported (Mattermost always converts markdown and automatically links @mentions). Bold formatting supplied as *bold* is not supported (must be done as **bold**). Slack assumes default values for some fields if they are not specified by the integration, while Mattermost does not. ","permalink":"https://developers.mattermost.com/integrate/slash-commands/slack/","section":"integrate","subsection":"slash commands","tags":null,"title":"Slack compatibility"},{"categories":null,"contents":"Our Engineering Guilds are a forum for sharing knowledge, discussing and agreeing on plans, and disseminating information related to a particular subject or technical topic. Many (although not all) Engineering Guilds are affiliated to a particular “platform” that forms a part of Mattermost, and are therefore associated with a particular Platform Team. However, the participants in each Guild span many engineering teams at Mattermost in order to get people together to share knowledge and disseminate decisions across the Engineering organization.\nThe main activity of Guilds is a regular meeting. These meetings are public, and anyone within Mattermost and the wider community is welcome to attend. Details are provided below on each of the Guilds, including how to participate.\nWeb platform guild The Web Platform Guild covers all areas related to front-end web development and Electron desktop app development at Mattermost.\nGuild Lead: Harrison Healey Guild channel: ~Developers: WebApp Guild meeting time: (see header of Guild channel for meeting invite) Mobile guild The Mobile Guild covers all areas related to mobile application development and React Native.\nGuild Lead: Elias Nahum Guild channel: ~Developers: Mobile Guild meeting time: (see header of Guild channel for meeting invite) Server guild The Server Guild covers all areas related to our server codebase and general Go development at Mattermost.\nGuild Lead: Agniva De Sarker Guild channel: ~Developers: Server Guild meeting time: (see header of Guild channel for meeting invite) QA guild The QA Guild covers all areas related to Quality Assurance and automated testing infrastructure at Mattermost.\nGuild Lead: Saturnino Abril Guild channel: ~QA: Weekly Meetings Guild meeting time: (see header of Guild channel for meeting invite) ","permalink":"https://developers.mattermost.com/contribute/more-info/getting-started/guilds/","section":"contribute","subsection":"more info","tags":null,"title":"Engineering guilds"},{"categories":null,"contents":"If the API or plugins don’t meet your your needs, take a look at the other methods for integrating and extending Mattermost:\nWebhooks - Post to channels with incoming webhooks, or listen for new messages with outgoing webhooks. Slash Commands - Enable your users to trigger custom actions from within Mattermost. Embed - Learn how to use the Mattermost API to embed Mattermost into web browsers and web applications. Customize - Modify the source code for the server or web app to make basic changes and customization. Interactive Messages - Create messages that include interactive functionality. ","permalink":"https://developers.mattermost.com/integrate/customization/","section":"integrate","subsection":null,"tags":null,"title":"Customize and embed"},{"categories":null,"contents":"Mattermost allows for a variety of customization options and modifications, making it possible to create a more adequate experience depending on the needs of each deployment.\nThere are a few limitations regarding how the re-branding of Mattermost must be handled, such as the fact that changes to the Enterprise Edition’s source code isn’t supported. However these limitations can be overcome with the utilization of Plugins.\nCustomizable components Server (Team Edition only) The Mattermost server’s source code, written in Golang, may be customized to deliver additional functionalities or to meet specific security requirements.\nIt’s recommended that you attempt to meet such customizations by leveraging the Plugin framework in order to avoid creating any breaking changes, however details on how to build a custom server may be found here.\nServer files Some parts of server-side customizations don’t require changes to the source code. View more details on which server files may be customized in here.\nNote: Modifications to server files can be utilized in both Team Edition and Enterprise Editions. Web app Mattermost’s web application runs on React, and its codebase has been open-sourced (regardless of which edition your server uses). You can view details on how to customize the web app in here. Keep in mind, however, that some changes to the web app can also leverage the Plugin framework, which can help reduce the necessity of rebasing your custom client to each Mattermost release.\n","permalink":"https://developers.mattermost.com/integrate/customization/customization/","section":"integrate","subsection":null,"tags":null,"title":"Customize Mattermost"},{"categories":null,"contents":"Messages can be sent with a priority level to help users identify the importance of the message. For more information about message priority, see formatting text.\nPriority options When sending a message, you can use any of the following to format how you want the posted message to look.\npriority: A required plain-text summary of the message. This is used in notifications, and in clients that don’t support formatted text (e.g. IRC).\nrequested_ack: If set to true, the message will be marked as requiring an acknowledgment from the users by displaying a checkmark icon next to the message. Keep in mind that this requires the message priority to be set to Important or Urgent.\npersistent_notifications: Only for Urgent messages. If set to true recipients will receive a persistent notification every five minutes until they acknowledge the message.\nExample of post priority { \"priority\": { \"priority\": \"urgent\", } } And this is how it renders on Mattermost:\nExample of post priority with requested acknowledgment { \"priority\": { \"priority\": \"important\", \"requested_ack\": true } } And this is how it renders on Mattermost:\nRelated documentation Message priority CreatePost API ","permalink":"https://developers.mattermost.com/integrate/reference/message-priority/","section":"integrate","subsection":null,"tags":null,"title":"Message priority"},{"categories":null,"contents":"See our demo plugin that illustrates the potential for a Mattermost plugin. To start writing your own plugin, consult our starter template.\nConsider using a plugin in the following scenarios:\nWhen you want to customize the Mattermost user interface. When you want to extend Mattermost functionality to meet a specific, complex requirement. When you want to build integrations that are managed by your Mattermost server. Plugins are fully supported in both Team Edition and Enterprise Edition.\nMarketplace The Marketplace is a collection of plugins that can greatly increase the value and capabilities of your Mattermost deployment. End users can see increased productivity through quick access to systems such as Jira, Zoom, Webex, and GitHub. Mattermost System Admins can discover new plugins and quickly deploy them to their servers, including High Availability clusters.\nThe Marketplace is available from v5.16 and is accessed via Product menu \u003e Marketplace. More listings will be added as we add new features and plugins that our customers request.\nPlugin labels Plugins in the Marketplace are labeled to make it easier for administrators to choose plugins that fit their company’s security and risk policies if they do not allow for community plugins to be used.\nCommunity plugins\nPlugins identified as “Community” are produced by the open-source community or partners and the features/roadmap are not controlled directly by Mattermost. Prior to being listed on the Marketplace, they are reviewed by the Mattermost development team and code-signed to ensure the code Mattermost reviewed, is delivered. Mattermost does not directly support these plugins in production environments.\nBeta plugins\nPlugins may be labeled as “Beta” if they’re released to the Marketplace early for customer previews. We do not recommend running beta plugins on production servers.\nExperimental\nPlugins labeled as “Experimental” are still being tested. These should not be run on production servers.\nPartner\nPlugins identified as “Partner” are created and maintained by a Mattermost partner.\nInstall a plugin When a new plugin becomes available on the Marketplace, it’s listed with an option to Install. Select Install to download and install the latest plugin binary from its respective GitHub repository. If there’s a cluster present, the plugin will be distributed to each server automatically.\nConfigure and enable a plugin Once a plugin is installed (or pre-installed if shipped with Mattermost binary release):\nSelect Configure \u003e Settings. Enter the plugin settings as required. Set Enable Plugin setting to True. If this flag is not enabled, the plugin will not become active. Test out the plugin as needed. Upgrade plugins Upgrade a plugin on demand when a new version becomes available. New versions of plugins that you have already installed will display a link to easily install the upgraded plugins. Some plugin versions may have breaking changes; please check the release notes if you’re performing a major version change.\nUpgrade plugins (prior to v5.18) In v5.16 and v5.17 the Marketplace only supports the installation of new plugins. To upgrade a plugin, you need to manually update it by downloading the binary file from the GitHub repository and then upload it in System Console \u003e Plugin Management.\nMarketplace server There are two settings in System Console \u003e Plugin Management:\nEnable Marketplace: Turns the Marketplace user interface on or off for System Admins (end users cannot see the Marketplace). Marketplace URL: The location of the Marketplace server to query for new plugins. Mattermost hosts a Marketplace for the community and this is the default value for this field. You can also set up your own Marketplace server. When you first access the Marketplace, your Mattermost server will attempt to contact the Mattermost Marketplace server and return a list of available plugins that are appropriate based on the server version that is currently running. Only your server version and search query is passed over to the Mattermost Marketplace; we retain an anonymized record for product analytics whenever a new plugin is installed, unless you have opted out of Telemetry.\nThe Marketplace server code is available as an open source project and can be used to set up your own private Marketplace if desired.\nMattermost Marketplace The Mattermost Marketplace is a service run by Mattermost that contains listings of plugins that we have reviewed and, in many cases, built. In the future, we plan to include community-developed plugins that will be labeled separately to Mattermost-developed plugins. as well as settings that would restrict which types of plugins you can install. Comments in our forum are welcome as we develop this feature further.\nMattermost integration directory There are many ways to integrate Mattermost aside from plugins, and we have created a directory of integration “recipes”, some of which are scripts, plugins, or instructions on how to connect Mattermost with your Enterprise systems. Many are sourced from our community of customers. You can browse the directory at https://integrations.mattermost.com.\nAbout plugins Plugins may have one or both of the following parts:\nWeb App plugins: Customize the Mattermost user interface by adding buttons to the channel header, overriding the RHS, or even rendering a custom post type within the center channel. All this is possible without having to fork the source code and rebase on every Mattermost release. For a sample plugin, see our Zoom plugin. Server plugins: Run a Go process alongside the server, filtering messages, or integrating with third-party systems such as Jira, GitLab, or Jenkins. For a sample plugin, see our Jira plugin. Security Plugins are intentionally powerful and not artificially sandboxed in any way and effectively become part of the Mattermost server. Server plugins can execute arbitrary code alongside your server and webapp plugins can deploy arbitrary code in client browsers.\nWhile this power enables deep customization and integration, it can be abused in the wrong hands. Plugins have full access to your server configuration and thus also to your Mattermost database. Plugins can read any message in any channel, or perform any action on behalf of any user in the Web App.\nYou should only install custom plugins from sources you trust to avoid compromising the security of your installation.\nPlugin signing The Marketplace allows System Admins to download and install plugins from a central repository. Plugins installed via the Marketplace must be signed by a public key certificate trusted by the local Mattermost server.\nWhile the server ships with a default certificate used to verify plugins from the default Mattermost Marketplace, the server can be configured to trust different certificates and point at a different plugin marketplace. This document outlines the steps for generating a public key certificate and signing plugins for use with a custom plugin marketplace. It assumes access to the GNU Privacy Guard (GPG) tool.\nConfiguration Configuring plugin signatures allows finer control over the verification process:\nPluginSettings.RequirePluginSignature = true\nThis is used to enforce plugin signature verification. With flag on, only Marketplace plugins will be installed and verified. With flag off, customers will be able to install plugins manually without signature verification.\nNote that the Marketplace plugins will still be verified even if the flag is off.\nKey generation Public and private key pairs are needed to sign and verify plugins. The private key is used for signing and should be kept in a secure location. The public key is used for verification and can be distributed freely. To generate a key pair, run the following command:\ngpg --full-generate-key Please select what kind of key you want: (1) RSA and RSA (default) (2) DSA and Elgamal (3) DSA (sign only) (4) RSA (sign only) Your selection? 1 RSA keys may be between 1024 and 4096 bits long. What keysize do you want? (2048) 3072 Requested keysize is 3072 bits Please specify how long the key should be valid. 0 = key does not expire \u003cn\u003e = key expires in n days \u003cn\u003ew = key expires in n weeks \u003cn\u003em = key expires in n months \u003cn\u003ey = key expires in n years Key is valid for? (0) 0 Key expires at ... Is this correct? (y/N) y GnuPG needs to construct a user ID to identify your key. Real name: Mattermost Inc Email address: info@mattermost.com Comment: You selected this USER-ID: \"Mattermost Inc \u003cinfo@mattermost.com\u003e\" Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O Key size should be at least 3072 bits.\nExport the private key Find the ID of your private key first. The ID is a hexadecimal number.\ngpg --list-secret-keys\nThis is your private key and should be kept secret. Your hexadecimal key ID will, of course, be different.\ngpg --export-secret-keys F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F \u003e ./my-priv-key\nExport the public key Find the ID of your public key first. The ID is a hexadecimal number.\ngpg --list-keys\ngpg --export F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F \u003e ./my-pub-key\nImporting the key If you already have a public and private key pair, you can import them to the GPG.\ngpg --import ./my-priv-gpg-key gpg --import ./my-pub-gpg-key\nSign the plugin For plugin signing, you have to know the hexadecimal ID of the private key. Let’s assume you want to sign com.mattermost.demo-plugin-0.1.0.tar.gz file, run:\ngpg -u F3FACE45E0DE642C8BD6A8E64C7C6562C192CC1F --verbose --personal-digest-preferences SHA256 --detach-sign com.mattermost.demo-plugin-0.1.0.tar.gz\nThis command will generate com.mattermost.demo-plugin-0.1.0.tar.gz.sig, which is the signature of your plugin.\nPlugin verification Mattermost server will verify plugin signatures downloaded from the Marketplace. To add custom public keys, run the following command on the Mattermost server:\nmattermost plugin add key my-pub-key\nMultiple public keys can be added to the Mattermost server:\nmattermost plugin add key my-pk-file1 my-pk-file2\nTo list the names of all public keys installed on your Mattermost server, use:\nmattermost plugin keys\nTo delete public key(s) from your Mattermost server, use:\nmattermost plugin delete key my-pk-file1 my-pk-file2\nImplementation See the implementation document for more information.\nSet up guide To manage plugins, go to System Console \u003e Plugins \u003e Plugin Management. From here, you can:\nEnable or disable pre-packaged plugins. Install and manage custom plugins. Pre-packaged plugins Mattermost ships with a number of pre-packaged plugins written and maintained by Mattermost. Instead of building these features directly into the product, you can selectively enable the functionality your installation requires. Install pre-packaged plugins from the Marketplace, even if your system cannot directly connect to the internet.\nPrior to v5.20, pre-packaged plugins were installed by default and could not be uninstalled without manually modifying the prepackaged_plugins directory. Any pre-packaged plugins installed prior to v5.20 and left enabled on upgrade will remain installed, but can now be uninstalled.\nCustom plugins Installing a custom plugin introduces some risks. As a result, plugin uploads are disabled by default and cannot be enabled via the System Console or REST API.\nTo enable plugin uploads, manually set PluginSettings \u003e EnableUploads to true in your config.json file and restart your server.\nIf you have installed Mattermost Omnibus, to enable plugin uploads, manually set enable_plugin_uploads: true in the mmomni.yml file and restart your server.\nYou can disable plugin uploads at any time without affecting previously uploaded plugins.\nWith plugin uploads enabled, navigate to System Console \u003e Plugins \u003e Management and upload a plugin bundle. Plugin bundles are *.tar.gz files containing the server executables and web app resources for the plugin. You can also specify a URL to install a plugin bundle from a remote source.\nNote: When RequirePluginSignature is true, plugin uploads cannot be enabled, and may only be installed via the Marketplace (which verifies Plugin Code Signatures). EnableRemoteMarketplaceURL also remains disabled as long as EnableUploads is disabled. Custom plugins may also be installed via the command line interface.\nWhile no longer recommended, plugins may also be installed manually by unpacking the plugin bundle inside the plugins directory of a Mattermost installation.\nPlugin uploads in high availability mode Prior to Mattermost 5.14, Mattermost servers configured for High Availability mode required plugins to be installed manually. As of Mattermost 5.14, plugins uploaded via the System Console or the CLI are persisted to the configured file store and automatically installed on all servers that join the cluster.\nManually installed plugins remain supported, but must be individually installed on each server in the cluster.\nFrequently asked questions Where can I share feedback on plugins? Join our community server discussion in the Toolkit channel.\nTroubleshooting Please see common questions below. For further assistance, review the Troubleshooting forum for previously reported errors, or join the Mattermost user community for troubleshooting help.\nPlugin uploads fail even though uploads are enabled If plugin uploads fail and you see permission denied errors in System Console \u003e Logs such as:\n[2017/11/13 20:42:18 UTC] [EROR] failed to start up plugins: mkdir /home/ubuntu/mattermost/client/plugins: permission denied\nIt’s likely that the Mattermost server doesn’t have the necessary permissions for uploading plugins. Ensure the Mattermost server has write access to the mattermost/client directory.\nIt may also be that the working directory for the service running Mattermost is not correct. On Ubuntu you might see:\n[2018/01/03 08:34:47 EST] [EROR] failed to start up plugins: mkdir ./client/plugins: no such file or directory\nThis can be fixed on Ubuntu 16.04 and RHEL by opening the service configuration file and setting WorkingDirectory to the path to Mattermost (generally it’s /opt/mattermost).\nA similar problem can occur on Windows:\n[EROR] failed to start up plugins: mkdir ./client/plugins: The system cannot find the path specified.\nTo fix this, set the AppDirectory of your service using nssm set mattermost AppDirectory c:\\mattermost.\nx509: certificate signed by unknown authority If you’re seeing x509: certificate signed by unknown authority in your server logs, it usually means that the CA for a self-signed certificate for a server your plugin is trying to access has not been added to your local trust store of the machine the Mattermost server is running on.\nYou can add one in Linux following instructions in this StackExchange article, or set up a load balancer like NGINX per production install guide to resolve the issue.\n","permalink":"https://developers.mattermost.com/integrate/plugins/using-and-managing-plugins/","section":"integrate","subsection":"plugins","tags":null,"title":"Use and manage plugins"},{"categories":null,"contents":"The plugin package exposed by Mattermost 5.6 and later drops support for automatically unmarshalling a plugin’s configuration onto the struct embedding MattermostPlugin. As server plugins are inherently concurrent (hooks being called asynchronously) and the plugin configuration can change at any time, access to the configuration must be synchronized.\nPlugins compiled against 5.5 and earlier will continue to work without modification, automatically unmarshalling a plugin’s configuration but with the existing risk of a corrupted read or write. Once the plugin is recompiled against Mattermost 5.6, it will be necessary to manually unmarshal your plugin’s configuration. Client-only plugins and server plugins without public fields require no modifications.\nNote that you do not need to wait until Mattermost 5.6 to make these changes, as the hardened approach explained below will work with Mattermost 5.5 and earlier. Any implementation of OnConfigurationChange you define overrides the one automatically unmarshalling.\nServer changes Unmarshalling configuration Previously, any public fields defined on the struct embedding MattermostPlugin would be automatically unmarshalled from the plugin’s configuration:\ntype Plugin struct { plugin.MattermostPlugin Greeting string } func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, \"Hello %s!\", p.Greeting) } Writing to Greeting while the plugin may be concurrently reading from same could result in a corrupted read or write. The mattermost-plugin-starter-template and mattermost-plugin-demo have both been updated with more complete examples, but the general idea is to manually handle the OnConfigurationChange hook and synchronize access to these variables. One such way is with a sync.RWMutex:\ntype Plugin struct { plugin.MattermostPlugin greetingLock sync.Mutex greeting string } func (p *Plugin) OnConfigurationChange() error { type configuration struct { Greeting string } // Load the public configuration fields from the Mattermost server configuration. if err := p.API.LoadPluginConfiguration(configuration); err != nil { return errors.Wrap(err, \"failed to load plugin configuration\") } p.configurationLock.Lock() defer p.configurationLock.Unlock() p.greeting = configuration.Greeting return nil } func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { p.configurationLock.RLock() defer p.configurationLock.RUnlock() fmt.Fprintf(w, \"Hello %s!\", p.greeting) } Unfortunately, this adds a fair bit of extra complexity. You may wish to base your updated implementation off of mattermost-plugin-starter-template or mattermost-plugin-demo to simplify your code.\nMigrate plugins from Mattermost 5.1 and earlier Mattermost 5.2 introduces breaking changes to the plugins beta. This page documents the changes necessary to migrate your existing plugins to be compatible with Mattermost 5.2 and later.\nSee mattermost-plugin-zoom for an example migration involving both a server and web app component.\nServer changes Although the underlying changes are significant, the required migration for server plugins is minimal.\nEntry point The plugin entry point was previously:\nimport \"github.com/mattermost/server/public/plugin/rpcplugin\" func main() { rpcplugin.Main(\u0026HelloWorldPlugin{}) } Change the imported package and invoke ClientMain instead:\nimport \"github.com/mattermost/mattermost/server/public/plugin\" func main() { plugin.ClientMain(\u0026HelloWorldPlugin{}) } Hook parameters Most hook callbacks now contain a leading plugin.Context parameter. Consult the Hooks documentation for more details, but for example, the ServeHTTP hook was previously:\nfunc (p *MyPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) { // ... } Change it to:\nfunc (p *MyPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { // ... } API changes Most of the previous API calls remain available and unchanged, with the notable exception of removing the KeyValueStore(). Use KVSet, KVGet and KVDelete instead test:\nfunc (p *MyPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { key := r.URL.Query().Get(\"key\") switch r.Method { case http.MethodGet: value, _ := p.API.KVGet(key) fmt.Fprintf(w, string(value)) case http.MethodPut: value := r.URL.Query().Get(\"value\") p.API.KVSet(key, []byte(value)) case http.MethodDelete: p.API.KVDelete(key) } } Any standard error from your plugin will now be captured in the server logs, including output from the standard log package, but there are also explicit API methods for emitting structured logs:\nfunc (p *MyPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { p.API.LogDebug(\"received http request\", \"user_agent\", r.UserAgent()) if r.Referer() == \"\" { p.API.LogError(\"missing referer\") } } This would generate something like the following in your server logs:\n{\"level\":\"debug\",\"ts\":1531494669.83655,\"caller\":\"app/plugin_api.go:254\",\"msg\":\"received http request\",\"plugin_id\":\"my-plugin\",\"user_agent\":\"HTTPie/0.9.9\"} {\"level\":\"error\",\"ts\":1531494669.8368616,\"caller\":\"app/plugin_api.go:260\",\"msg\":\"missing referer\",\"plugin_id\":\"my-plugin\"} Web app changes The changes to web app plugins are more significant than server plugins.\nEntry point The plugin entry point was previously registered by directly manipulating a global variable:\nwindow.plugins['my-plugin'] = new MyPlugin(); Instead, use the globally exported registerPlugin method:\nwindow.registerPlugin('my-plugin', new MyPlugin()); Externalize dependencies The plugins beta suggested relying on the global export of common libraries from the web app:\nconst React = window.react; While this remains supported, it is more natural to leverage Webpack Externals. Configure this in your .webpack.config.js:\nmodule.exports = { // ... externals: { react: 'react', }, // ... }; and then import your modules naturally:\nimport React from 'react'; Note however that the exported variables have changed to the following:\nPrior to Mattermost 5.2 Mattermost 5.2 window.react window.React window[‘react-dom’] window.ReactDom window.redux window.Redux window[‘react-redux’] window.ReactRedux window[‘react-bootstrap’] window.ReactBootstrap window[‘post-utils’] window.PostUtils N/A window.PropTypes Initialization The initialize callback used to receive a registerComponents callback to configure components, post types and main menu overrides:\nimport ChannelHeaderButton from './components/channel_header_button'; import MobileChannelHeaderButton from './components/mobile_channel_header_button'; import PostTypeZoom from './components/post_type_zoom'; import {configureZoom} from './actions/zoom'; class MyPlugin { initialize(registerComponents) { registerComponents( {ChannelHeaderButton, MobileChannelHeaderButton}, {custom_zoom: PostTypeZoom}, { id: 'zoom-configuration', text: 'Zoom Configuration', action: configureZoom, }, ); } } The initialize callback now receives an instance of the plugin registry. In some cases, the registry’s API now requires a more discrete breakdown of the registered component to allow the web app to handle various rendering scenarios:\nimport ChannelHeaderButtonIcon from './components/channel_header_button/icon'; import MobileChannelHeaderButton from './components/mobile_channel_header_button'; import PostTypeZoom from './components/post_type_zoom'; import {startZoomMeeting, configureZoom} from './actions/zoom'; class MyPlugin { initialize(registry) { registry.registerChannelHeaderButtonAction( ChannelHeaderButtonIcon, startZoomMeeting, 'Start Zoom Meeting', ); registry.registerPostTypeComponent('custom_zoom', PostTypeZoom); registry.registerMainMenuAction( 'Zoom Configuration', configureZoom, MobileChannelHeaderButton, ); } } Restructuring your plugin to use the new registry API will likely prove to be the hardest part of migrating.\n","permalink":"https://developers.mattermost.com/integrate/plugins/migration/","section":"integrate","subsection":"plugins","tags":null,"title":"Migrate plugins"},{"categories":null,"contents":"Mattermost Enterprise Edition servers with an E20 license have the ability to run in High Availability (HA) mode, meaning a cluster of Mattermost app servers running together as a single Mattermost deployment.\nIt is important that all plugins consider HA environments when being built.\nPlugins are started as subprocesses of the main Mattermost process on each app server. This means a Mattermost deployment that has three app servers will have three separate copies of the same plugin running. Each running copy of the plugin will be isolated from one another on different servers. Therefore, to run properly in HA the plugin’s server-side code must be stateless.\nTo be stateless, the plugin must not retain any information or status in memory that may be needed across multiple events (e.g. HTTP requests or in other hooks). This data should instead be stored in a place that all running copies of the plugin have access to. For example, the key-value store the plugin API provides.\nTo better explain the problem with having a plugin store data in-memory, consider this case:\nLet’s say we have two Mattermost app servers running together in HA and an installed plugin with a feature that alerts a user any time a trigger word is posted in a channel.\nThe user uses the web app side of the plugin to set the word hello as the trigger word An HTTP request is made to the server-side of the plugin to set hello The plugin process running on app server 1 handles the request and stores hello in a variable In this scenario, the trigger word is now only set for the plugin process running on app server 1. The plugin process running on app server 2 is unaware that the trigger word is hello. This means when someone posts a message containing hello and the request is load balanced to app server 2, the plugin is not going to alert our user when it should.\nThe proper way to deal with this case would be for the plugin to store the trigger word in a global store, such as the KV store. Then any time a user posts the plugin can pull the trigger word from the store and properly alert the user, regardless of which app server handles the request.\nRun a scheduled job in high availability mode Using the mattermost/public/pluginapi/cluster package, we can schedule jobs to perform background activity at regular intervals, without having to explicitly coordinate with other instances of the same plugin. Here’s an example from the Demo Plugin:\njob, cronErr := cluster.Schedule( p.API, \"BackgroundJob\", cluster.MakeWaitForRoundedInterval(15*time.Minute), p.BackgroundJobFunc, ) if cronErr != nil { return errors.Wrap(cronErr, \"failed to schedule background job\") } p.backgroundJob = job ","permalink":"https://developers.mattermost.com/integrate/plugins/components/server/ha/","section":"integrate","subsection":"plugins","tags":null,"title":"High availability"},{"categories":null,"contents":"See a full list of all Mattermost integrations here.\n**Hubot Adapter** Hubot is a “chat bot” created by GitHub that listens for commands and executes actions based on your requests.\nhubot-matteruser is a Hubot adapter for Mattermost written in coffee script that uses the Mattermost Web Services API and WebSockets to deliver Hubot functionality.\n**Bitbucket Webhooks** This integration, written in Python, converts Bitbucket outgoing webhook events to be posted into Mattermost using an incoming webhook.\n**Interactive Poll** Backend code for powering a Mattermost slash command that creates an interactive poll. Contains Firebase Cloud Functions that connect Mattermost Interactive Buttons with the Firebase Realtime Database.\n**Mattermost Chat Bridge** Simple bridge between Mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat(via xmpp), Matrix, Steam and ssh-chat.\n","permalink":"https://developers.mattermost.com/integrate/examples/","section":"integrate","subsection":null,"tags":null,"title":"Examples"},{"categories":null,"contents":"Integrations open dialogs by sending an HTTP POST, containing some data in the request body, to an endpoint on the Mattermost server. Integrations can use this endpoint to open dialogs when users click message buttons or select an option from a menu, or use a custom slash command.\nMoreover, plugins can trigger a dialog based on user actions. For instance, if a plugin adds a button in the channel header, clicking that button may open a dialog.\nHere is an example of what a dialog looks like for creating a Jira issue within the Mattermost user interface:\nOpen a dialog To open a dialog, your integration must first receive an HTTP request from the Mattermost server. This request will be triggered by a slash command or an interactive message. It will include a trigger ID.\nOnce you have the trigger ID you can use it to open the interactive dialog by sending an HTTP POST request to https://\u003cyour-mattermost-url\u003e/api/v4/actions/dialogs/open. See the below section for what to include in the body of that HTTP request.\nParameters Interactive dialogs support the following parameters:\nParameter Type Description title String Title of the dialog. Maximum 24 characters. introduction_text String Markdown-formatted introduction text which is displayed above the dialog elements. elements Array See below for more details on elements. If none are supplied the dialog box acts as a simple confirmation. url String The URL to send the submitted dialog payload to. icon_url String (Optional) The URL of the icon used for your dialog. If none specified, no icon is displayed. submit_label String (Optional) Label of the button to complete the dialog. Default is Submit. notify_on_cancel Boolean (Optional) When true, sends an event back to the integration whenever there’s a user-induced dialog cancellation. No other data is sent back with the event. Default is false. state String (Optional) String provided by the integration that will be echoed back with dialog submission. Default is the empty string. source_url String (Optional) URL for field refresh requests. When a select element with refresh: true changes value, Mattermost sends a refresh request to this URL. Also used as the endpoint for multi-step form responses. Sample JSON is given below. Form submissions are sent back to the URL defined by the integration. You must also include the trigger ID you received from the slash command or interactive message.\n{ \"trigger_id\": \"\u003cunique ID generated by the server\u003e\", \"url\": \"\u003cURL to send the submitted request to\u003e\", \"dialog\": { \"callback_id\": \"\u003cID specified by the integration to identify the request\u003e\", \"title\": \"\u003ctitle of the dialog\u003e\", \"introduction_text\": \"\u003cText describing the dialog box content\u003e\", \"elements\": [\"\u003cArray of UI elements to display in the dialog\u003e\"], \"submit_label\": \"\u003clabel of the button to complete the dialog\u003e\", \"notify_on_cancel\": false, \"state\": \"\u003cstring provided by the integration that will be echoed back with dialog submission\u003e\", \"source_url\": \"\u003coptional URL for field refresh and multi-step form handling\u003e\" } } Elements Each dialog supports elements for users to enter information.\ntext: Single-line plain text field. Use this for inputs such as names, email addresses, or phone numbers. textarea: Multi-line plain text field. Use this field when the answer is expected to be longer than 150 characters. select: Message menu. Use this for pre-selected choices. Can either be static menus or dynamic menus generated from users and Public channels of the system. For more information on message menus, see the documentation. bool: Checkbox option. Use this for binary selection. radio: Radio button option. Use this to quickly select an option from pre-selected choices. date: Date picker field. Use this for selecting dates without time information. datetime: Date and time picker field. Use this for selecting both date and time with timezone support. Each element is required by default, otherwise the client will return an error as shown below. Note that the error message will appear below the help text, if one is specified. To make an element optional, set the field \"optional\": \"true\".\nText elements Text elements are single-line plain text fields. Below is an example of a text element that asks for an email address.\n{ \"display_name\": \"Email\", \"name\": \"email\", \"type\": \"text\", \"subtype\": \"email\", \"placeholder\": \"placeholder@example.com\" } There is an optional \"subtype\": \"email\" field in the above example, which specifies the keyboard layout used on mobile. For this example, the email keypad is shown to the user given the subtype is set to email.\nThe full list of supported fields is included below:\nField Type Description display_name String Display name of the field shown to the user in the dialog. Maximum 24 characters. name String Name of the field element used by the integration. Maximum 300 characters. You should use unique name fields in the same dialog. type String Set this value to text for a text element. subtype String (Optional) One of text, email, number, password (as of v5.14), tel, or url. Default is text. Use this to set which keypad is presented to users on mobile when entering the field. min_length Integer (Optional) Minimum input length allowed for an element. Default is 0. max_length Integer (Optional) Maximum input length allowed for an element. Default is 150. If you expect the input to be greater 150 characters, consider using a textarea type element instead. optional Boolean (Optional) Set to true if this form element is not required. Default is false. help_text String (Optional) Set help text for this form element. Maximum 150 characters. default String (Optional) Set a default value for this form element. Maximum 150 characters. placeholder String (Optional) A string displayed to help guide users in completing the element. Maximum 150 characters. Textarea elements Textarea elements are multi-line plain text fields. A sample JSON is provided below:\n{ \"display_name\": \"Ticket Description\", \"name\": \"ticket_description\", \"type\": \"textarea\", \"help_text\": \"Provide description for your ticket.\" } The maximum length for a textarea is 3,000 characters.\nThe list of supported fields is the same as for the textarea type element.\nField Type Description display_name String Display name of the field shown to the user in the dialog. Maximum 24 characters. name String Name of the field element used by the integration. Maximum 300 characters. You should use unique name fields in the same dialog. type String Set this value to texareat for a textarea element. subtype String (Optional) One of text, email, number, password (as of v5.14), tel, or url. Default is text. Use this to set which keypad is presented to users on mobile when entering the field. min_length Integer (Optional) Minimum input length allowed for an element. Default is 0. max_length Integer (Optional) Maximum input length allowed for an element. Default is 3000. optional Boolean (Optional) Set to true if this form element is not required. Default is false. help_text String (Optional) Set help text for this form element. Maximum 150 characters. default String (Optional) Set a default value for this form element. Maximum 3000 characters. placeholder String (Optional) A string displayed to help guide users in completing the element. Maximum 3000 characters. Select elements Select elements are message menus that allow users to select one or multiple predefined options from a list. Below is an example of a select element that asks for one of three different options.\n{ \"display_name\": \"Option Selector\", \"name\": \"options\", \"type\": \"select\", \"options\": [ { \"text\": \"Option1\", \"value\": \"opt1\" }, { \"text\": \"Option2\", \"value\": \"opt2\" }, { \"text\": \"Option3\", \"value\": \"opt3\" } ] } Multiselect support Minimum Server Version: 11.0 Select elements can be configured to allow multiple selections by setting the multiselect parameter to true. When multiselect is enabled, users can choose multiple options from the list.\n{ \"display_name\": \"Multiple Options\", \"name\": \"multi_options\", \"type\": \"select\", \"multiselect\": true, \"default\": \"opt1,opt3\", \"options\": [ { \"text\": \"Option1\", \"value\": \"opt1\" }, { \"text\": \"Option2\", \"value\": \"opt2\" }, { \"text\": \"Option3\", \"value\": \"opt3\" } ] } For multiselect elements, default values can be specified as a comma-separated string (e.g., \"opt1,opt3\" to pre-select Option1 and Option3).\nDynamic select support Minimum Server Version: 11.0 Select elements can be configured to load options dynamically from an external API endpoint. This enables real-time filtering, searching, and loading of options based on user input.\nTo use dynamic select, set data_source: \"dynamic\" and provide a data_source_url that points to your lookup endpoint:\n{ \"display_name\": \"Dynamic Options\", \"name\": \"dynamic_field\", \"type\": \"select\", \"data_source\": \"dynamic\", \"data_source_url\": \"https://your-mattermost-server.com/plugins/your-plugin-id/api/lookup\", \"placeholder\": \"Search for options...\" } When users interact with a dynamic select field, Mattermost sends an HTTP POST request to the /api/v4/actions/dialogs/lookup endpoint, which proxies the request to your data_source_url. The payload uses the same SubmitDialogRequest structure:\n{ \"type\": \"dialog_lookup\", \"url\": \"\u003cyour data_source_url\u003e\", \"callback_id\": \"\u003ccallback ID\u003e\", \"state\": \"\u003cstate\u003e\", \"user_id\": \"\u003cuser ID\u003e\", \"channel_id\": \"\u003cchannel ID\u003e\", \"team_id\": \"\u003cteam ID\u003e\", \"submission\": { \"query\": \"user_search_input\", \"selected_field\": \"dynamic_field\", \"other_field_name\": \"current_value\" } } The submission map contains query (the user’s search input), selected_field (the field being searched), and current values of all other form fields.\nYour endpoint should respond with an array of options:\n{ \"items\": [ { \"text\": \"Option 1\", \"value\": \"option1\" }, { \"text\": \"Option 2\", \"value\": \"option2\" } ] } Security Requirements:\nExternal URLs must use HTTPS Plugin paths (starting with /plugins/) are proxied locally and do not require HTTPS The lookup endpoint should validate user permissions Dynamic select with multiselect:\n{ \"display_name\": \"Multiple Dynamic Options\", \"name\": \"multi_dynamic_field\", \"type\": \"select\", \"multiselect\": true, \"data_source\": \"dynamic\", \"data_source_url\": \"https://your-mattermost-server.com/plugins/your-plugin-id/api/lookup\", \"placeholder\": \"Search and select multiple options...\" } The select element can also be generated dynamically from users and channels of the system.\nFor users, use:\n{ \"display_name\": \"Assignee\", \"name\": \"assignee\", \"type\": \"select\", \"data_source\": \"users\" } For multiple user selection:\n{ \"display_name\": \"Multiple Assignees\", \"name\": \"assignees\", \"type\": \"select\", \"multiselect\": true, \"data_source\": \"users\" } For Public channels, use:\n{ \"display_name\": \"Post this message to\", \"name\": \"channel\", \"type\": \"select\", \"data_source\": \"channels\" } For multiple channel selection:\n{ \"display_name\": \"Post to multiple channels\", \"name\": \"channels\", \"type\": \"select\", \"multiselect\": true, \"data_source\": \"channels\" } The list of supported fields for the select type element is included below:\nField Type Description display_name String Display name of the field shown to the user in the dialog. Maximum 24 characters. name String Name of the field element used by the integration. Maximum 300 characters. You should use unique name fields in the same dialog. type String Set this value to select for a select element. multiselect Boolean (Optional) Set to true to allow multiple selections from the list. Default is false. data_source String (Optional) One of users, channels, or dynamic. If none specified, assumes a manual list of options is provided by the integration. data_source_url String (Optional) URL for dynamic option loading when data_source is dynamic. Must be HTTPS or a /plugins/ path. refresh Boolean (Optional) When true, triggers a field refresh request to the dialog’s source_url when this field’s value changes. Use this for dependent dropdowns or conditional form fields. Default is false. optional Boolean (Optional) Set to true if this form element is not required. Default is false. options Array (Optional) An array of options for the select element. Not applicable for users, channels, or dynamic data sources. help_text String (Optional) Set help text for this form element. Maximum 150 characters. default String (Optional) Set a default value for this form element. For multiselect, use comma-separated values. Maximum 3,000 characters. placeholder String (Optional) A string displayed to help guide users in completing the element. Maximum 3,000 characters. Checkbox element From Mattermost v5.16 you can use checkbox elements. It looks like a plain text field with a checkbox to be selected. Below is an example of a checkbox element that asks for meeting feedback.\n{ \"display_name\": \"Can you please select below\", \"placeholder\": \"The meeting was helpful.\", \"name\": \"meeting_input\", \"type\": \"bool\" } The full list of supported fields is included below:\nField Type Description display_name String Display name of the field shown to the user in the dialog. Maximum 24 characters. name String Name of the field element used by the integration. Maximum 300 characters. You should use unique name fields in the same dialog. type String Set this value to bool for a checkbox element. optional Boolean (Optional) Set to true if this form element is not required. Default is false. help_text String (Optional) Set help text for this form element. Maximum 150 characters. default String (Optional) Set a default value for this form element. true or false. placeholder String (Optional) A string displayed to include a label besides the checkbox. Maximum 150 characters. Radio element From Mattermost v5.16 you can use radio elements. It looks like a plain text field with a radio button to be selected. Below is an example of a radio element that asks for a department.\n{ \"display_name\": \"Which department do you work in?\", \"name\": \"department\", \"type\": \"radio\", \"options\": [ { \"text\": \"Engineering\", \"value\": \"engineering\" }, { \"text\": \"Sales\", \"value\": \"sales\" }, { \"text\": \"Administration\", \"value\": \"administration\" } ], \"help_text\": \"Please indicate your department as of January 1.\", \"default\": \"engineering\" } The full list of supported fields are included below:\nField Type Description display_name String Display name of the field shown to the user in the dialog. Maximum 24 characters. name String Name of the field element used by the integration. Maximum 300 characters. You should use unique name fields in the same dialog. type String Set this value to radio for a radio element. options Array (Optional) An array of options for the radio element. help_text String (Optional) Set help text for this form element. Maximum 150 characters. default String (Optional) Set a default value for this form element. Date elements Minimum Server Version: 11.1 Date elements provide native date picker functionality for selecting dates without time information. Below is an example of a date element for event scheduling.\n{ \"display_name\": \"Event Date\", \"name\": \"event_date\", \"type\": \"date\", \"default\": \"2024-03-15\", \"placeholder\": \"Select a date\", \"help_text\": \"Choose the date for your event\", \"optional\": false, \"datetime_config\": { \"min_date\": \"today\", \"max_date\": \"+30d\" } } The full list of supported fields for date elements is included below:\nField Type Description display_name String Display name of the field shown to the user in the dialog. Maximum 24 characters. name String Name of the field element used by the integration. Maximum 300 characters. You should use unique name fields in the same dialog. type String Set this value to date for a date element. default String (Optional) Default value in ISO date format (YYYY-MM-DD) or relative format (today, tomorrow, +1d, +1w, +1M, +1y). Full ISO datetime strings are accepted, but only the date part is parsed; timezone information is ignored. placeholder String (Optional) Placeholder text shown in the input field. Maximum 150 characters. help_text String (Optional) Help text displayed below the field. Maximum 150 characters. optional Boolean (Optional) Set to true if this form element is not required. Default is false. datetime_config Object (Optional) Nested date configuration object. See datetime_config object for supported properties. min_date String (Deprecated — use datetime_config.min_date.) Earliest selectable date. Supports ISO date format (YYYY-MM-DD) or relative formats (today, tomorrow, +1d, -7d, etc.). Full ISO datetime strings are accepted, but only the date part is parsed; timezone information is ignored. max_date String (Deprecated — use datetime_config.max_date.) Latest selectable date. Supports ISO date format (YYYY-MM-DD) or relative formats (today, +30d, +1y, etc.). Full ISO datetime strings are accepted, but only the date part is parsed; timezone information is ignored. Date field usage examples Basic date selection:\n{ \"display_name\": \"Project Deadline\", \"name\": \"deadline\", \"type\": \"date\", \"help_text\": \"When is this project due?\", \"datetime_config\": { \"min_date\": \"today\" }, \"optional\": false } Date range with relative defaults:\n{ \"display_name\": \"Flexible Date\", \"name\": \"any_date\", \"type\": \"date\", \"help_text\": \"Any date within the next year\", \"datetime_config\": { \"min_date\": \"today\", \"max_date\": \"+1y\" }, \"default\": \"+1w\", \"optional\": true } DateTime elements Minimum Server Version: 11.1 DateTime elements provide combined date and time picker functionality with timezone support. Below is an example of a datetime element for meeting scheduling.\n{ \"display_name\": \"Meeting Time\", \"name\": \"meeting_time\", \"type\": \"datetime\", \"default\": \"2024-03-15T14:30:00Z\", \"placeholder\": \"Select date and time\", \"help_text\": \"Choose when the meeting should start\", \"optional\": false, \"datetime_config\": { \"min_date\": \"today\", \"max_date\": \"+7d\", \"time_interval\": 30 } } The full list of supported fields for datetime elements is included below:\nField Type Description display_name String Display name of the field shown to the user in the dialog. Maximum 24 characters. name String Name of the field element used by the integration. Maximum 300 characters. You should use unique name fields in the same dialog. type String Set this value to datetime for a datetime element. default String (Optional) Default value in ISO datetime format (RFC3339) or relative format. For relative dates, time defaults to noon. When specifying a time, it must align with datetime_config.time_interval (be a multiple of the interval). placeholder String (Optional) Placeholder text shown in the input field. Maximum 150 characters. help_text String (Optional) Help text displayed below the field. Maximum 150 characters. optional Boolean (Optional) Set to true if this form element is not required. Default is false. datetime_config Object (Optional) Nested datetime configuration object. See datetime_config object for supported properties. min_date String (Deprecated — use datetime_config.min_date.) Earliest selectable date. Supports ISO format or relative formats (today, tomorrow, +1d, -7d, etc.). max_date String (Deprecated — use datetime_config.max_date.) Latest selectable date. Supports ISO format or relative formats (today, +30d, +1y, etc.). time_interval Integer (Deprecated — use datetime_config.time_interval.) Time selection interval in minutes. Must be between 1 and 1440, and must be a divisor of 1440 to create evenly spaced intervals throughout the day. Common values: 15, 30, 60, 90, 120. Default is 60. DateTime field usage examples Meeting scheduler with business hours:\n{ \"display_name\": \"Meeting Time\", \"name\": \"meeting_time\", \"type\": \"datetime\", \"help_text\": \"Select a time during business hours\", \"datetime_config\": { \"min_date\": \"+1d\", \"max_date\": \"+14d\", \"time_interval\": 30 }, \"optional\": false } Event with custom time intervals:\n{ \"display_name\": \"Event Start Time\", \"name\": \"event_start\", \"type\": \"datetime\", \"help_text\": \"When does your event begin?\", \"datetime_config\": { \"min_date\": \"today\", \"max_date\": \"+90d\", \"time_interval\": 15 }, \"default\": \"today\" } Fixed location timezone with manual entry:\n{ \"display_name\": \"Conference Start\", \"name\": \"conference_start\", \"type\": \"datetime\", \"help_text\": \"All attendees see this time in the conference timezone\", \"datetime_config\": { \"location_timezone\": \"America/Denver\", \"manual_time_entry\": true, \"time_interval\": 30 } } datetime_config object Minimum Server Version: 11.6 The datetime_config object groups date/datetime configuration into a single nested structure. It is supported on both date and datetime elements.\nField Type Applies to Since Description min_date String date, datetime 11.8 (Optional) Earliest selectable date. Supports ISO format or relative formats (today, +1d, etc.). max_date String date, datetime 11.8 (Optional) Latest selectable date. Supports ISO format or relative formats (+30d, +1y, etc.). time_interval Integer datetime 11.6 (Optional) Time selection interval in minutes. Must be between 1 and 1440, and must be a divisor of 1440. Default is 60. location_timezone String datetime 11.6 (Optional) IANA timezone used to display and submit the time (e.g. America/Denver, Asia/Tokyo). When set, all users see the same wall-clock time regardless of their own timezone. Defaults to the viewing user’s timezone. manual_time_entry Boolean datetime 11.8 (Optional) When true, users can type the time directly in addition to using the dropdown. Default is false. allow_manual_time_entry Boolean datetime 11.6 (deprecated in 11.8) (Deprecated — use manual_time_entry.) When both are set, either enabling turns the feature on. Backward compatibility (new in 11.8): The top-level min_date, max_date, and time_interval fields on date and datetime elements are still accepted for existing integrations, but are deprecated in favor of datetime_config. When both are provided on the same element, values inside datetime_config take precedence over the legacy top-level values.\nDate and DateTime field specifications Relative Date Formats:\ntoday: Current date tomorrow: Next day yesterday: Previous day +Nd: N days from today (e.g., +1d, +7d) +Nw: N weeks from today (e.g., +1w, +2w) +NM: N months from today (e.g., +1M, +6M) +Ny: N years from today (e.g., +1y) -Nd: N days ago (e.g., -1d, -7d) ISO Format Examples:\nDate: 2024-03-15 (YYYY-MM-DD) DateTime: 2024-03-15T14:30:00Z (RFC3339 format) DateTime with timezone: 2024-03-15T14:30:00-05:00 Form Submission Format:\nDate fields submit in ISO date format: 2024-03-15 DateTime fields submit in RFC3339 format with user’s timezone: 2024-03-15T14:30:00-05:00 Localization:\nDate fields automatically format according to user’s locale (e.g., “Mar 15, 2024” for en-US) DateTime fields respect user’s 12/24 hour preference and locale-specific formatting Time picker displays times according to user’s timezone Validation:\nClient validates date formats and range constraints Server validates relative date resolution and field configuration For datetime fields, datetime_config.time_interval must be a divisor of 1440 (24 hours in minutes) Default times must align with the effective time_interval (be a multiple of the interval) Invalid dates, times, or out-of-range selections show appropriate error messages Time Interval Examples:\n\"time_interval\": 15 creates options: 00:00, 00:15, 00:30, 00:45, 01:00, etc. \"time_interval\": 30 creates options: 00:00, 00:30, 01:00, 01:30, etc. \"time_interval\": 60 creates options: 00:00, 01:00, 02:00, 03:00, etc. Invalid: \"time_interval\": 7 (7 is not a divisor of 1440) Dialog submission When a user submits a dialog, Mattermost will perform client-side input validation to make sure:\nAll required fields are filled. All formats are correct (e.g. email, telephone number, etc.). The submission payload sent to the integration is:\n{ \"type\": \"dialog_submission\", \"callback_id\": \"\u003ccallback ID provided by the integration\u003e\", \"state\": \"\u003cstate provided by the integration\u003e\", \"user_id\": \"\u003cuser ID of the user who submitted the dialog\u003e\", \"channel_id\": \"\u003cchannel ID the user was in when submitting the dialog\u003e\", \"team_id\": \"\u003cteam ID the user was on when submitting the dialog\u003e\", \"submission\": { \"some_element_name\": \"\u003cvalue of that element\u003e\", \"some_other_element\": \"\u003cvalue of some other element\u003e\" }, \"cancelled\": false } Optionally, the dialog can send an event back to the integration if notify_on_cancel parameter is set to true. If this happens, cancelled will be set to true on the above payload, and submission will be empty.\nMoreover, Mattermost also allows the integration itself to perform input validation. This can be done by responding to the dialog submission request with a JSON body containing an errors field. The errors field can contain a JSON object, mapping input field names to string error messages you would like to display to the user. For example, if you have a field named num_between_0_and_10, you can enforce the user to enter a number between 0 and 10 by returning the following response body if the condition isn’t satisfied:\n{\"errors\": {\"num_between_0_and_10\": \"Enter a number between 0 and 10.\"}} The integration may also return a generic error message to the user that is not attached to a specific field. This can be done by responding to the dialog submission request with a JSON body containing an error field. The error field should contain a string with the error message to display to the user. For example, if a server-side error occurs, you can return a message explaining it:\n{\"error\": \"Failed to fetch additional data. Please try again.\"} Support for generic error messages was added in Mattermost v5.18.\nFinally, once the request is submitted, we recommend that the integration responds with a system message or an ephemeral message confirming the submission. This should be a separate request back to Mattermost once the service has received and responded to a submission request from a dialog. This can be done either via the REST API, or via the Plugin API if you’re developing a plugin.\nMulti-step dialogs Minimum Server Version: 11.1 Multi-step dialogs enable wizard-like form experiences where users progress through multiple steps to complete a complex workflow. Each step can have different fields, and form values are accumulated across steps.\nMulti-step behavior is achieved by having your submit handler return a new form instead of closing the dialog. No special dialog property is required.\nMulti-step workflow Initial dialog: Open a normal dialog with the first step’s elements. Use the state field to track which step you’re on (e.g., \"step_1\"). Step submission: When the user clicks Submit, your integration receives a standard dialog_submission request with type: \"dialog_submission\". Return next step: Instead of returning an empty response (which closes the dialog), return a response with type: \"form\" and a form object containing the next step’s dialog definition. Value accumulation: The client automatically accumulates submission values across steps. Each step’s submission includes all values from previous steps plus the current step. Final submission: On the last step, return an empty response (HTTP 200) or {\"type\": \"ok\"} to close the dialog. Multi-step submission handling Each step submission uses the standard dialog submission format:\n{ \"type\": \"dialog_submission\", \"callback_id\": \"\u003ccallback ID\u003e\", \"state\": \"step_1\", \"user_id\": \"\u003cuser ID\u003e\", \"channel_id\": \"\u003cchannel ID\u003e\", \"team_id\": \"\u003cteam ID\u003e\", \"submission\": { \"step1_field\": \"value_from_step_1\" }, \"cancelled\": false } To advance to the next step, your integration responds with a new form:\n{ \"type\": \"form\", \"form\": { \"callback_id\": \"multistep_wizard\", \"title\": \"Setup Wizard - Step 2 of 3\", \"elements\": [ { \"display_name\": \"Step 2 Field\", \"name\": \"step2_field\", \"type\": \"text\" } ], \"submit_label\": \"Next\", \"state\": \"step_2\" } } On the final step, the submission includes accumulated values from all steps:\n{ \"type\": \"dialog_submission\", \"callback_id\": \"\u003ccallback ID\u003e\", \"state\": \"step_3\", \"user_id\": \"\u003cuser ID\u003e\", \"channel_id\": \"\u003cchannel ID\u003e\", \"team_id\": \"\u003cteam ID\u003e\", \"submission\": { \"step1_field\": \"value_from_step_1\", \"step2_field\": \"value_from_step_2\", \"step3_field\": \"value_from_step_3\" }, \"cancelled\": false } Return an empty response (HTTP 200 with empty body) or {\"type\": \"ok\"} to close the dialog.\nMulti-step example { \"trigger_id\": \"trigger_id_here\", \"url\": \"https://your-integration.com/dialog\", \"dialog\": { \"callback_id\": \"multistep_wizard\", \"title\": \"Setup Wizard - Step 1 of 3\", \"introduction_text\": \"Welcome to the setup wizard\", \"elements\": [ { \"display_name\": \"Project Name\", \"name\": \"project_name\", \"type\": \"text\", \"placeholder\": \"Enter project name\" } ], \"submit_label\": \"Next\", \"state\": \"step_1\" } } Dynamic field refresh Minimum Server Version: 11.1 Interactive dialogs support dynamic field refresh functionality, allowing fields to be updated based on user input. This is useful for dependent dropdowns or conditional form fields.\nTo enable field refresh, two things are required:\nSet refresh: true on the element that should trigger refreshes when its value changes. Set source_url on the dialog to specify where refresh requests are sent. When a user changes a select element that has refresh: true, Mattermost sends a request to the dialog’s source_url via the /api/v4/actions/dialogs/submit endpoint.\nField refresh webhook The field refresh request uses the standard SubmitDialogRequest structure with type set to \"refresh\":\n{ \"type\": \"refresh\", \"url\": \"\u003csource_url\u003e\", \"callback_id\": \"\u003ccallback ID\u003e\", \"state\": \"\u003ccurrent state\u003e\", \"user_id\": \"\u003cuser ID\u003e\", \"channel_id\": \"\u003cchannel ID\u003e\", \"team_id\": \"\u003cteam ID\u003e\", \"submission\": { \"category\": \"software\", \"subcategory\": \"\", \"selected_field\": \"category\" } } The submission map contains the current values of all form fields, plus a selected_field key indicating which field triggered the refresh. Cleared fields are sent as empty strings.\nYour integration should respond with a complete updated dialog wrapped in a form response:\n{ \"type\": \"form\", \"form\": { \"callback_id\": \"dynamic_form\", \"title\": \"Dynamic Form\", \"source_url\": \"/plugins/your-plugin-id/refresh\", \"elements\": [ { \"display_name\": \"Category\", \"name\": \"category\", \"type\": \"select\", \"refresh\": true, \"options\": [ {\"text\": \"Software\", \"value\": \"software\"}, {\"text\": \"Hardware\", \"value\": \"hardware\"} ] }, { \"display_name\": \"Subcategory\", \"name\": \"subcategory\", \"type\": \"select\", \"options\": [ {\"text\": \"Frontend\", \"value\": \"frontend\"}, {\"text\": \"Backend\", \"value\": \"backend\"} ] } ], \"submit_label\": \"Submit\", \"state\": \"step_1\" } } The response replaces the entire dialog with the new form definition. The client preserves the user’s current field values where field names match.\nField refresh example { \"trigger_id\": \"trigger_id_here\", \"url\": \"https://your-integration.com/dialog/submit\", \"dialog\": { \"callback_id\": \"dynamic_form\", \"title\": \"Dynamic Form\", \"source_url\": \"/plugins/your-plugin-id/refresh\", \"elements\": [ { \"display_name\": \"Category\", \"name\": \"category\", \"type\": \"select\", \"refresh\": true, \"options\": [ {\"text\": \"Software\", \"value\": \"software\"}, {\"text\": \"Hardware\", \"value\": \"hardware\"} ] }, { \"display_name\": \"Subcategory\", \"name\": \"subcategory\", \"type\": \"select\", \"options\": [] } ], \"submit_label\": \"Submit\", \"state\": \"step_1\" } } Note: If the dialog is closed by clicking Cancel or X, no data will be submitted. If a user clicks away from the dialog, the dialog won’t close. This is to prevent accidentally losing any answers they’ve made to an unsubmitted dialog. Example Below is a full example of a JSON payload that creates an interactive dialog in Mattermost:\n{ \"trigger_id\":\"nbt1dxzqwpn6by14sfs66ganhc\", \"url\":\"http://localhost:5000/dialog_submit\", \"dialog\":{ \"callback_id\":\"somecallbackid\", \"title\":\"Test Title\", \"icon_url\":\"https://mattermost.com/wp-content/uploads/2022/02/icon.png\", \"elements\":[ { \"display_name\":\"Display Name\", \"name\":\"realname\", \"type\":\"text\", \"subtype\":\"\", \"default\":\"default text\", \"placeholder\":\"placeholder\", \"help_text\":\"This a regular input in an interactive dialog triggered by a test integration.\", \"optional\":false, \"min_length\":0, \"max_length\":0, \"data_source\":\"\", \"options\":null }, { \"display_name\":\"Email\", \"name\":\"someemail\", \"type\":\"text\", \"subtype\":\"email\", \"default\":\"\", \"placeholder\":\"placeholder@bladekick.com\", \"help_text\":\"This a regular email input in an interactive dialog triggered by a test integration.\", \"optional\":false, \"min_length\":0, \"max_length\":0, \"data_source\":\"\", \"options\":null }, { \"display_name\":\"Number\", \"name\":\"somenumber\", \"type\":\"text\", \"subtype\":\"number\", \"default\":\"\", \"placeholder\":\"\", \"help_text\":\"\", \"optional\":false, \"min_length\":0, \"max_length\":0, \"data_source\":\"\", \"options\":null }, { \"display_name\":\"Display Name Long Text Area\", \"name\":\"realnametextarea\", \"type\":\"textarea\", \"subtype\":\"\", \"default\":\"\", \"placeholder\":\"placeholder\", \"help_text\":\"\", \"optional\":true, \"min_length\":5, \"max_length\":100, \"data_source\":\"\", \"options\":null }, { \"display_name\":\"User Selector\", \"name\":\"someuserselector\", \"type\":\"select\", \"subtype\":\"\", \"default\":\"\", \"placeholder\":\"Select a user...\", \"help_text\":\"\", \"optional\":false, \"min_length\":0, \"max_length\":0, \"data_source\":\"users\", \"options\":null }, { \"display_name\":\"Channel Selector\", \"name\":\"somechannelselector\", \"type\":\"select\", \"subtype\":\"\", \"default\":\"\", \"placeholder\":\"Select a channel...\", \"help_text\":\"Choose a channel from the list.\", \"optional\":true, \"min_length\":0, \"max_length\":0, \"data_source\":\"channels\", \"options\":null }, { \"display_name\":\"Option Selector\", \"name\":\"someoptionselector\", \"type\":\"select\", \"subtype\":\"\", \"default\":\"\", \"placeholder\":\"Select an option...\", \"help_text\":\"\", \"optional\":false, \"min_length\":0, \"max_length\":0, \"data_source\":\"\", \"options\":[ { \"text\":\"Option1\", \"value\":\"opt1\" }, { \"text\":\"Option2\", \"value\":\"opt2\" }, { \"text\":\"Option3\", \"value\":\"opt3\" } ] }, { \"display_name\":\"Multiple Option Selector\", \"name\":\"somemultioptionselector\", \"type\":\"select\", \"multiselect\":true, \"default\":\"opt1,opt3\", \"placeholder\":\"Select multiple options...\", \"help_text\":\"You can select multiple options from this list.\", \"optional\":true, \"data_source\":\"\", \"options\":[ { \"text\":\"Option1\", \"value\":\"opt1\" }, { \"text\":\"Option2\", \"value\":\"opt2\" }, { \"text\":\"Option3\", \"value\":\"opt3\" }, { \"text\":\"Option4\", \"value\":\"opt4\" } ] }, { \"display_name\":\"Dynamic Lookup\", \"name\":\"somedynamicfield\", \"type\":\"select\", \"data_source\":\"dynamic\", \"data_source_url\":\"https://your-mattermost-server.com/plugins/your-plugin-id/api/lookup\", \"placeholder\":\"Search for dynamic options...\", \"help_text\":\"Type to search for options from external API\", \"optional\":true }, { \"display_name\":\"Event Date\", \"name\":\"eventdate\", \"type\":\"date\", \"default\":\"today\", \"placeholder\":\"Select event date\", \"help_text\":\"Choose when your event takes place\", \"datetime_config\":{ \"min_date\":\"today\", \"max_date\":\"+30d\" }, \"optional\":false }, { \"display_name\":\"Meeting Time\", \"name\":\"meetingtime\", \"type\":\"datetime\", \"default\":\"\", \"placeholder\":\"Select meeting date and time\", \"help_text\":\"Schedule your meeting with date and time\", \"datetime_config\":{ \"min_date\":\"today\", \"max_date\":\"+14d\", \"time_interval\":30 }, \"optional\":true } ], \"submit_label\":\"Submit\", \"notify_on_cancel\":true, \"state\":\"somestate\" } } Share your integration If you’ve built an integration for Mattermost, please consider sharing your work in our app directory.\nThe app directory lists open source integrations developed by the Mattermost community and are available for download, customization and deployment to your private cloud or self-hosted infrastructure.\nSlack compatibility Like Slack, dialogs are triggered by an interactive message menu or button, or by a custom slash command. Additionally, Mattermost can trigger dialogs via plugins.\nThe schema for these objects is the same as Slack’s, except for the following differences:\nurl field must be specified for Mattermost dialogs, which specifies where the request is sent to. In Slack, this is handled by specifying the URL within the Slack app that uses the dialog. icon_url is an optional field to set the icon for Mattermost dialogs. In Slack, the dialogs use the icon set for the app that uses the dialog. label in Slack dialogs is display_name in Mattermost dialogs for a more consistent naming convention with other integration types. hint in Slack dialogs is help_text in Mattermost dialogs for a more consistent naming convention with other integration types. value in Slack dialogs is default in Mattermost dialogs for a more consistent naming convention with other integration types. Moreover, the JSON payload for select type elements matches interactive message menus.\n","permalink":"https://developers.mattermost.com/integrate/plugins/interactive-dialogs/","section":"integrate","subsection":"plugins","tags":null,"title":"Interactive dialogs"},{"categories":null,"contents":"Launch Mattermost from a button selection The most common way of integrating Mattermost into another application is via a link or a button that brings up Mattermost in a new browser window or tab, with a link to a specific Mattermost channel to begin discussion.\nOptionally, single-sign-on can be added to make the experience seamless.\nMattermost launch button example in HTML Save the below HTML code in a file called mattermost-button-example.html then open the file in a browser as an example.\n\u003cscript\u003e var myWindow = null; function openMMWindow() { myWindow = window.open(\"https://community.mattermost.com/core/channels/developer\", \"Mattermost\", \"top=0,left=0,width=400,height=600,status=no,toolbar=no,location=no,menubar=no,titlebar=no\"); } function closeMMWindow() { if (myWindow) { myWindow.close(); } } \u003c/script\u003e \u003chtml\u003e \u003cbr/\u003e \u003cbr/\u003e \u003cbutton onclick=\"openMMWindow()\"\u003eOpen Developer Channel\u003c/button\u003e \u003cbr/\u003e \u003cbr/\u003e \u003cbutton onclick=\"closeMMWindow()\"\u003eClose Developer Channel\u003c/button\u003e \u003cbr/\u003e \u003cbr/\u003e \u003c/html\u003e Embed Mattermost in web apps using an \u003ciframe\u003e Any web application embedded into another using an \u003ciframe\u003e is at risk of security exploits, since the outer application intercepts all user input into the embedded application, an exploit known as Click-Jacking. By default, Mattermost disables embedding. If you choose to embed Mattermost we highly recommend it is done only on a private network that you control.\nSee this recipe for details.\nEmbed Mattermost in mobile apps The open source mobile applications can serve as a guide or starter code to embed Mattermost in mobile applications. The Mattermost Javascript Driver is used to connect with the Mattermost server and product the interactivity for these applications.\nThe mobile applications also provide full source code for push notifications.\nMobile apps offering Mattermost as a web view https://github.com/mattermost/ios Mobile apps offering Mattermost with React Native components https://github.com/mattermost/mattermost-mobile ","permalink":"https://developers.mattermost.com/integrate/customization/embedding/","section":"integrate","subsection":null,"tags":null,"title":"Embed Mattermost"},{"categories":null,"contents":"What’s the difference between incoming and outgoing webhooks? A webhook is a way for one app to send real-time data to another app.\nIn Mattermost, incoming webhooks receive data from external applications and make a post in a specified channel. They’re great for setting up notifications when something happens in an external application.\nOutgoing webhooks take data from Mattermost, and send it to an external application. Then the outgoing webhook can post a response back in Mattermost. They’re great for listening in on channels, and then notifying external applications when a trigger word is used.\nWhat is a slash command? A slash command is similar to an outgoing webhook, but instead of listening to a channel it is used as a command tool. This means if you type in a slash command it will not be posted to a channel, whereas an outgoing webhook is only triggered by posted messages.\nWhat does Slack-compatible mean? Slack compatible means that Mattermost accepts integrations that have a payload in the same format as Slack.\nIf you have a Slack integration, you should be able to set it up in Mattermost without changing the format.\nWhat if I have a webhook from somewhere other than Slack? If you have an integration that outputs a payload in a different format you need to write an intermediate application to act as a translation layer to change it to the format Mattermost uses. Since there’s currently no general standard for webhook formatting, this is unavoidable and just a part of how webhooks work.\nIf there’s no translation layer, Mattermost won’t understand the data you’re sending.\nWhat are attachments? When “attachments” are mentioned in the integrations documentation, it refers to Slack’s Message Attachments. These “attachments” can be optionally added as an array in the data sent by an integration, and are used to customize the formatting of the message.\nWe currently don’t support the ability to attach files to a post made by an integration.\nWhere can I find existing integrations? Visit our app directory for dozens of open source integrations to common tools like Jira, Jenkins, GitLab, Trac, Redmine, and Bitbucket, along with interactive bot applications (Hubot, mattermost-bot), and other communication tools (Email, IRC, XMPP, Threema) that are freely available for use and customization.\nWhere should I install my integrations? For self-hosted deployments in small setups you might host integrations on the same server on which Mattermost is installed. For larger deployments you can setup a separate server for integrations, or add them to the server on which the external application is hosted - for example, if you’re self-hosting a Jira server you could deploy a Jira integration on the Jira server itself.\nWhen self-hosting restrictions are less strict, AWS, Heroku, and other public cloud options could also be used.\nHow do I create a bot account with personal access tokens? See bot accounts documentation to learn more about how to create and manage bot accounts in Mattermost.\nHow do I create a bot account without personal access tokens or webhooks? Deployments that cannot create bot accounts via webhooks due to security reasons and do not want to use personal access tokens with no expiry time, can use the following approach:\nCreate a bot account using a secure email and strong password.\nManually add the account to all teams and channels it needs access to. If your deployment has a lot of teams or channels, you may create a CLI script to automate the process.\nIn a testing environment, you may also make the bot account a System Admin, giving the bot permissions to post to any channel. Not recommended in production due to potential security vulnerabilities. Provide the email and password to your integration, and store it in a secure location with restricted access.\nHave your integration use the email and password with an `/api/v4/login` endpoint to retrieve a session token. The session token is used to authenticate to the Mattermost system.\nSet up your bot to make an HTTP POST to your-mattermost-url.com/api/v4/users/login with a JSON body, including the bot account’s email and password.\nPOST /api/v4/users/login HTTP/1.1 Host: your-mattermost-url.com Content-Length: 66 Content-Type: application/json {\"login_id\":\"someone@nowhere.com\",\"password\":\"thisisabadpassword\"} where we assume there is a Mattermost instance running at http://localhost:8065.\nIf successful, the response will contain a Token header and a user object in the body:\nHTTP/1.1 200 OK Set-Cookie: MMSID=hyr5dmb1mbb49c44qmx4whniso; Path=/; Max-Age=2592000; HttpOnly Token: hyr5dmb1mbb49c44qmx4whniso X-Ratelimit-Limit: 10 X-Ratelimit-Remaining: 9 X-Ratelimit-Reset: 1 X-Request-Id: smda55ckcfy89b6tia58shk5fh X-Version-Id: developer Date: Fri, 11 Sep 2015 13:21:14 GMT Content-Length: 657 Content-Type: application/json; charset=utf-8 {{user object as json}} The bot should retrieve the session token from the Token header and store it in memory for use with future requests.\nNote: Each session token has an expiry time, set depending on the server’s configuration. If the session token your bot is using expires, it will receive a 401 Unauthorized response from requests using that token. When your bot receives this response, it should reapply the login logic (using the above steps) to get another session token. Then resend the request that received the 401 status code. Include the Token as part of the Authorization header on API requests from your integration.\nTo confirm the token works, you can have your bot make a simple GET request to /api/v4/users/me with the Authorization: bearer \u003cyourtokenhere\u003e in the header. If it returns a 200 with the bot’s user object in the response, the API request was made successfully.\nGET /api/v4/users/me HTTP/1.1 Authorization: bearer \u003cyourtokenhere\u003e Host: your-mattermost-url.com Note: The Mattermost development team is also working on an API developer token, which allows you to authenticate the bot account via the API token rather than retrieving a session token from a user account. How should I automate the install and upgrade of Mattermost when included in another application? Automate Mattermost installation within another application:\nReview the Mattermost installation documentation to understand configuration steps of the production deployment. Install Mattermost files to a dedicated /opt/mattermost directory by decompressing the tar.gz file of the latest release for your target platform (for example linux-amd64). Review Configuration Settings in config.json and set your automation to customize your Mattermost deployment based on your requirements. For directory locations defined in config.json, such as the location of the local file storage directory (./data/) or logs directory (./logs), you can redefine those locations in your config.json settings and move the directories. All other directories should remain as they are in /mattermost. Test that your Mattermost server is running with your new configuration. Also, from the command line run ./bin/mattermost -version to test that the command line interface is functioning properly. Automate Mattermost upgrade within another application:\nReview the upgrade guide for an overview of the upgrade procedure. Create automation to upgrade to the next Mattermost versions: Back up the config.json file to preserve any settings a user may have made. Back up the ./data directory if local storage is used for files. Replace the contents of /mattermost directory with the decompressed contents of the latest release. Restore config.json and ./data to their previous locations (which may have been overwritten). If you need to overwrite any config.json parameters use a `sed` command or similar tool to update config.json Starting the Mattermost server to upgrade the database, config.json file, and ./data as necessary. Optionally the upgrade procedure can be chained so users can upgrade across an arbitrary number of Mattermost versions rather than to just the latest release. Come join our Contributors community channel on our daily build server, where you can discuss questions with community members and the Mattermost core team. Join our Developers channel for technical discussions and our Integrations channel for all integrations and plugins discussions.\n","permalink":"https://developers.mattermost.com/integrate/faq/","section":"integrate","subsection":null,"tags":null,"title":"Frequently asked questions (FAQ)"},{"categories":null,"contents":"\nFor information on interactive dialogs, see here.\nUse interactive messages to simplify complex workflows by allowing users to take quick actions directly through your integration post. For example, they enable your integration to:\nMark a task complete in your project management tracker. Conduct a customer survey or a poll. Initiate a command to merge a branch into a release. To try it out, you can use this Matterpoll plugin to add polling to Mattermost channels via a /poll slash command.\nMessage buttons Add message buttons as actions in your integration message attachments.\nThe following payload gives an example that uses message buttons.\n{ \"attachments\": [ { \"pretext\": \"This is the attachment pretext.\", \"text\": \"This is the attachment text.\", \"actions\": [ { \"id\": \"message\", \"name\": \"Ephemeral Message\", \"tooltip\": \"Send a message only visible to you\", \"integration\": { \"url\": \"http://127.0.0.1:7357\", \"context\": { \"action\": \"do_something_ephemeral\" } } }, { \"id\": \"update\", \"name\": \"Update\", \"tooltip\": \"Update this message\", \"integration\": { \"url\": \"http://127.0.0.1:7357\", \"context\": { \"action\": \"do_something_update\" } } } ] } ] } In the HTTP response for this request, the integration can choose to update the original post, and/or respond with an ephemeral message:\n{ \"update\": { \"message\": \"Updated!\", \"props\": {} }, \"ephemeral_text\": \"You updated the post!\" } To return a custom error message to the user, your integration can respond with an error object:\n{ \"error\": { \"message\": \"Unable to complete action. Please check your permissions.\" } } The error message will be displayed to the user below the message attachment. If no custom error message is provided, a default “Action failed to execute” message is shown. This feature is available in Mattermost v10.5 and later.\nButton actions support a style parameter to change the color of the button. The possible values for style are: good, warning, danger, default, primary, and success. It’s also possible to pass a theme variable or a hex color, but we discourage this approach because it won’t be resilient against theme changes.\nThe actions used in the previous example include the following:\n[ { \"id\": \"vote0\", \"type\": \"button\", \"name\": \"Yes\", \"style\": \"default\" }, { \"id\": \"vote1\", \"type\": \"button\", \"name\": \"No\", \"style\": \"primary\" }, { \"id\": \"addOption\", \"type\": \"button\", \"name\": \"Add Option\", \"style\": \"warning\" }, { \"id\": \"deletePoll\", \"type\": \"button\", \"name\": \"Delete Poll\", \"style\": \"success\" }, { \"id\": \"endPoll\", \"type\": \"button\", \"name\": \"End Poll\", \"style\": \"danger\" } ] Tooltips on buttons You can add tooltips to provide helpful information when users hover over action buttons:\n{ \"attachments\": [ { \"pretext\": \"Review this pull request\", \"text\": \"Pull request #1234: Add new feature\", \"actions\": [ { \"id\": \"approve\", \"type\": \"button\", \"name\": \"Approve\", \"tooltip\": \"Click to approve this pull request\", \"style\": \"primary\", \"integration\": { \"url\": \"http://127.0.0.1:7357\", \"context\": { \"action\": \"approve\", \"pr_id\": 1234 } } }, { \"id\": \"reject\", \"type\": \"button\", \"name\": \"Reject\", \"tooltip\": \"Click to reject this pull request\", \"style\": \"danger\", \"integration\": { \"url\": \"http://127.0.0.1:7357\", \"context\": { \"action\": \"reject\", \"pr_id\": 1234 } } } ] } ] } Message menus Similar to buttons, add message menus as actions in your integration message attachments.\nThe following payload gives an example that uses message menus (where id in the actions array may only consist of letters and numbers, no other characters are allowed):\n{ \"attachments\": [ { \"pretext\": \"This is the attachment pretext.\", \"text\": \"This is the attachment text.\", \"actions\": [ { \"id\": \"actionoptions\", \"name\": \"Select an option...\", \"integration\": { \"url\": \"http://127.0.0.1:7357/actionoptions\", \"context\": { \"action\": \"do_something\" } }, \"type\": \"select\", \"options\": [ { \"text\": \"Option1\", \"value\": \"opt1\" }, { \"text\": \"Option2\", \"value\": \"opt2\" }, { \"text\": \"Option3\", \"value\": \"opt3\" } ] } ] } ] } The integration can respond with an update to the original post, or with an ephemeral message:\n{ \"update\": { \"message\": \"Updated!\", \"props\": {} }, \"ephemeral_text\": \"You updated the post!\" } Message menus for channels You can provide a list of channels for message menus for users to select from. Users can only select from public channels in their teams.\nSpecify channels as your action’s data_source as follows:\n{ \"attachments\": [ { \"pretext\": \"This is the attachment pretext.\", \"text\": \"This is the attachment text.\", \"actions\": [ { \"id\": \"actionoptions\", \"name\": \"Select an option...\", \"integration\": { \"url\": \"http://127.0.0.1:7357/actionoptions\", \"context\": { \"action\": \"do_something\" } }, \"type\": \"select\", \"data_source\": \"channels\" } ] } ] } Message menus for users Similar to channels, you can also provide a list of users for message menus. The user can choose the user who is part of the Mattermost system.\nSpecify users as your action’s data_source as follows:\n{ \"attachments\": [ { \"id\": \"actionoptions\", \"pretext\": \"This is the attachment pretext.\", \"text\": \"This is the attachment text.\", \"actions\": [ { \"name\": \"Select an option...\", \"integration\": { \"url\": \"http://127.0.0.1:7357/actionoptions\", \"context\": { \"action\": \"do_something\" } }, \"type\": \"select\", \"data_source\": \"users\" } ] } ] } Parameters Below is a brief description of each parameter to help you customize the interactive message button and menu in Mattermost. For more information on message attachments, see our documentation.\nID A per post unique identifier.\nName Give your action a descriptive name.\nTooltip (optional) Display helpful text when users hover over the button. Available in Mattermost v10.5 and later.\nURL The actions are backed by an integration that handles HTTP POST requests when users select the message button. The URL parameter determines where this action is sent. The request contains an application/json JSON string. As of 5.14, relative URLs are accepted, simplifying the workflow when a plugin handles the action.\nContext The requests sent to the specified URL contain the user ID, post ID, channel ID, team ID, and any context that was provided in the action definition. If the post was of type Message Menus, then context also contains the selected_option field with the user-selected option value. The post ID can be used to, for example, delete or edit the post after selecting a message button.\nA simple example of a request is given below:\n{ \"user_id\": \"rd49ehbqyjytddasoownkuqrxe\", \"post_id\": \"gqrnh3675jfxzftnjyjfe4udeh\", \"channel_id\": \"j6j53p28k6urx15fpcgsr20psq\", \"team_id\": \"5xxzt146eax4tul69409opqjlf\", \"context\": { \"action\": \"do_something\" } } In most cases, your integration will do one or both of these things:\nIdentifying which action was triggered. For example, a GitHub integration might store something like this in the context:\n{ \"user_id\": \"rd49ehbqyjytddasoownkuqrxe\", \"post_id\": \"gqrnh3675jfxzftnjyjfe4udeh\", \"channel_id\": \"j6j53p28k6urx15fpcgsr20psq\", \"team_id\": \"5xxzt146eax4tul69409opqjlf\", \"context\": { \"repo\": \"mattermost/mattermost\", \"pr\": 1234, \"action\": \"merge\" } } In the example above, when the message button is selected, your integration sends a request to the specified URL with the intention to merge the pull request identified by the context.\nAuthenticating the server. An important property of the context parameter is that it’s kept confidential. If your integration is not behind a firewall, you could add a token to your context without users ever being able to see it:\n{ \"user_id\": \"rd49ehbqyjytddasoownkuqrxe\", \"post_id\": \"gqrnh3675jfxzftnjyjfe4udeh\", \"channel_id\": \"j6j53p28k6urx15fpcgsr20psq\", \"team_id\": \"5xxzt146eax4tul69409opqjlf\", \"context\": { \"repo\": \"mattermost/mattermost\", \"pr\": 1234, \"action\": \"merge\", \"token\": \"somerandomlygeneratedsecret\" } } Then, when your integration receives the request, it can verify that the token matches one that you previously generated and know that the request is legitimately coming from the Mattermost server and is not forged.\nDepending on the application, integrations can also perform authentication statelessly with cryptographic signatures such as:\n{ \"user_id\": \"rd49ehbqyjytddasoownkuqrxe\", \"post_id\": \"gqrnh3675jfxzftnjyjfe4udeh\", \"channel_id\": \"j6j53p28k6urx15fpcgsr20psq\", \"team_id\": \"5xxzt146eax4tul69409opqjlf\", \"context\": { \"repo\": \"mattermost/mattermost\", \"pr\": 1234, \"action\": \"merge\", \"signature\": \"mycryptographicsignature\" } } It’s also possible for integrations to do both of these things with a single token and use something like this as context:\n{ \"user_id\": \"rd49ehbqyjytddasoownkuqrxe\", \"post_id\": \"gqrnh3675jfxzftnjyjfe4udeh\", \"channel_id\": \"j6j53p28k6urx15fpcgsr20psq\", \"team_id\": \"5xxzt146eax4tul69409opqjlf\", \"context\": { \"action_id\": \"someunguessableactionid\" } } Then, when the integration receives the request, it can act based on the action ID.\nError handling When an action button integration fails, Mattermost automatically displays an error message to the user below the message attachment. This provides immediate feedback when button actions don’t work as expected.\nKey behaviors:\nIntegrations can return custom error messages using the error response format (see above) If no custom message is provided, a default “Action failed to execute” message is shown Previous errors are automatically cleared when a new action is triggered Error display works for both button and menu actions This feature is available in Mattermost v10.5 and later.\nAutomatic error display scenarios:\nWhen your integration returns an error response with a custom message, that message is displayed to the user. For system-level errors where no custom message can be returned, a default error message is shown:\nNetwork connection fails when contacting the integration URL Integration URL is invalid or unreachable Integration returns a non-200 HTTP status code Integration returns invalid JSON For troubleshooting integration errors from the server side, see the FAQ section “Why does an interactive button or menu return a 400 error?” below.\nTips and best practices The external application may be written in any programming language. It needs to provide a URL which receives the request sent by your Mattermost server and responds within the required JSON format. To get started, you can use this sample plugin to add polling to Mattermost channels via a /poll slash command. Share your integration If you’ve built an integration for Mattermost, please consider sharing your work in our app directory.\nThe app directory lists open source integrations developed by the Mattermost community and are available for download, customization, and deployment to your private cloud or self-hosted infrastructure.\nSlack compatibility Like Slack, actions are specified in an Actions list within the message attachment. Moreover, your integrations can react with ephemeral messages or message updates similar to Slack.\nHowever, the schema for these objects is slightly different given Slack requires a Slack App and action URL to be pre-configured beforehand. Mattermost instead allows an integration to create an interactive message without pre-configuration.\nIf your ephemeral_text gets incorrectly handled by the Slack-compatibility logic, send \"skip_slack_parsing\":true along your ephemeral_text to bypass it.\n{ \"update\": { \"message\": \"Updated!\" }, \"ephemeral_text\": \"You updated the post!\", \"skip_slack_parsing\": true } Frequently asked questions Are message buttons and menus supported in ephemeral messages? Yes, message buttons and menus are supported in ephemeral messages in Mattermost 5.10 and later. This applies to integrations using plugins, the RESTful API and webhooks, across the browser and Desktop App.\nAs an advanced feature, you can also use plugins to update the contents of an ephemeral message with message buttons or menus with the UpdateEphemeralMessage plugin API.\nWhy does an interactive button or menu return a 400 error? It is likely for one of three reasons:\nMattermost wasn’t able to connect to the integration. If the integration is on your internal infrastructure, it’ll need to be whitelisted (see AllowedUntrustedInternalConnections config.json setting). The log will include the text err=address forbidden in the error message. The integration didn’t return HTTP status 200. The log will include the text status=XXX in the error message. The integration didn’t return a valid JSON response. The log will include the text err=some json error message in the error message. How do I manage properties of an interactive message? Use update.Props in the following ways to manage properties (Props) of an interactive message after a user performs an action via an interactive button or menu:\nupdate.Props == nil - Do not update Props field. update.Props == {} - Clear all properties, except the username and icon of the original message, as well as whether the message was pinned to channel or contained emoji reactions. update.Props == some_props - Post will be updated to some_props. Username and icon of the original message, and whether the message was pinned to channel or contained emoji reactions will not be updated. ","permalink":"https://developers.mattermost.com/integrate/plugins/interactive-messages/","section":"integrate","subsection":"plugins","tags":null,"title":"Interactive messages"},{"categories":null,"contents":"The authorization allows:\nUsers with an account on a Mattermost server to sign in to third-party applications. You can find a sample OAuth2 Client Application for Mattermost here to test the functionality. A Mattermost server to authenticate requests to a third-party API. One popular application is Zapier integration which allows you to integrate more than 700 applications with Mattermost through OAuth 2.0. See our Zapier documentation to learn more. Understanding client types Mattermost supports two types of OAuth 2.0 clients as defined in RFC 6749:\nConfidential clients Authentication: Have a client_secret that must be kept secure Use cases: Server-side applications, trusted backends Token endpoint auth method: client_secret_post Created: Via UI (default) or Dynamic Client Registration (DCR) Security: Can securely store credentials on the server side Public clients Authentication: No client_secret (uses auth method none) Use cases: Single-Page Applications (SPAs), mobile apps, browser-based applications that cannot securely store secrets Security: MUST use PKCE for authorization code flow Token endpoint auth method: none Created: Via API with is_public: true parameter, or via DCR with token_endpoint_auth_method: \"none\" Limitations: Do not receive refresh tokens (access tokens only) Register your application in Mattermost You must register your application in Mattermost to generate OAuth 2.0 credentials (client ID and secret for confidential clients, or just client ID for public clients), which your application can use to authenticate API calls to Mattermost, and which Mattermost uses to authorize API requests from the application.\nIf you’d like to set up a Zapier integration with OAuth 2.0, see our Zapier documentation to learn more.\nEnable OAuth 2.0 applications OAuth 2.0 applications are off by default and can be enabled by the System Admin as follows:\nLog in to your Mattermost server as the System Administrator. Go to System Console \u003e Integrations \u003e Integration Management. Set Enable OAuth 2.0 Service Provider to True. (Optional) If you’d like to allow external applications to post with customizable usernames and profile pictures, then set Enable integrations to override usernames and Enable integrations to override profile picture icons to true. (Optional) If you’d like to allow users on your system who are not System Admins to create OAuth 2.0 applications, then set Restrict managing integrations to Admins to False. (Optional) To enable Dynamic Client Registration, set Enable Dynamic Client Registration to True in System Console \u003e Integrations \u003e Integration Management. See the Dynamic Client Registration section for important security considerations. Register an OAuth 2.0 application Go to Product menu \u003e Integrations.\nSelect OAuth 2.0 Applications, then choose Add OAuth 2.0 Application.\nSet Is Trusted: When set to Yes, your application is considered trusted by Mattermost. This means Mattermost doesn’t require users to accept authorization when signing to third-party applications. When set to No, users will be provided with the following page to accept or deny authorization when authenticating for the first time.\nOnly System Admins can set OAuth 2.0 applications as trusted.\nSet Is Public Client: When set to Yes, your application is registered as a public client (for SPAs, mobile apps, etc.). Public clients do not receive a client secret and must use PKCE for the authorization code flow. When set to No (default), your application is registered as a confidential client with a client secret.\nSpecify the Display Name: Enter a name for your application made of up to 64 characters. This is the name users will see when granting access to the application, when viewing a list of authorized applications in Settings \u003e Security \u003e OAuth 2.0 Applications and when viewing a list of OAuth 2.0 applications in the Integrations menu.\nAdd Description: This is a short description of your application that users will see when viewing a list of authorized applications in Settings \u003e Security \u003e OAuth 2.0 Applications.\nSpecify the Homepage: This is the homepage of the OAuth 2.0 application and lets users visit the app page to learn more what it does. The URL must be a valid URL and start with http:// or https:// depending on your server configuration.\n(Optional) Add Icon URL: The image users will see when viewing a list of authorized applications in Settings \u003e Security \u003e OAuth 2.0 Applications and when viewing a list of OAuth 2.0 applications in the Integrations menu. Must be a valid URL and start with http:// or https://.\nAdd Callback URLs: These are the URL(s) to which Mattermost will redirect users after accepting or denying authorization of your application, and which will be the only URL(s) that handle authorization codes or access tokens. If more than one URL is specified, users will be redirected to the URL used for the initial authorization of the app. Each URL must be on a separate line and start with http:// or https://.\nSelect Save to create the application.\nYou’ll be provided with a Client ID, Client Secret, and the authorized redirect URLs. Save these values and use them in your application to connect it to Mattermost.\nNote: Client Secret can be regenerated by the application creator or System Admin. Tokens created with the old secret will remain valid and authorization of existing users will continue to work; however, new authorizations and requests for new tokens will fail until the client secret has been updated in your app configuration. Grant permissions to your application Once you have created an OAuth 2.0 application, all users on the Mattermost server are automatically given access to it.\nThe application does not automatically have permissions based on the user who registers the app - the permissions are based on which users go through the OAuth flow, which could be the user who registers the app or anyone else on the system.\nIf the application was registered as a trusted OAuth 2.0 app on the Mattermost server, authorization from users is not required. Otherwise, the following page will be provided to accept or deny authorization when authenticating the app for the first time.\nOnce authorized, the application receives an access token to perform requests on behalf of that user. The application can then perform any action for which the user has permission.\nUsers can view a list of authorized apps from Settings \u003e Security \u003e OAuth 2.0 Applications, and revoke authorization from this setting.\nSupported OAuth flows Mattermost supports the authorization code and implicit grant flows for OAuth 2.0 applications.\nFlow support by client type Client Type Authorization Code Flow Implicit Flow PKCE Required Public Client Supported Supported YES (code flow only) Confidential Client Supported Supported (Not recommended) Optional Key notes:\nPublic clients cannot use the refresh token grant type - they only receive access tokens, not refresh tokens Implicit flow does not require PKCE (no code exchange happens) Confidential clients can optionally use PKCE; if initiated with code_challenge, it will be enforced throughout the flow PKCE (Proof Key for Code Exchange) PKCE (RFC 7636) is a security extension that protects against authorization code interception attacks. It’s especially important for public clients like single-page applications and mobile apps that cannot securely store client secrets.\nWhen PKCE is required Public clients: MUST use PKCE when using the authorization code flow. The Mattermost server will reject authorization requests from public clients that don’t include PKCE parameters. Confidential clients: PKCE is optional. However, if you include PKCE parameters in the authorization request, you must complete the PKCE flow in the token request. How PKCE works PKCE adds two parameters to the OAuth flow:\nAuthorization request: Include a code_challenge (a hashed value) and code_challenge_method (Mattermost supports S256 only) Token request: Include the code_verifier (the original unhashed value) The server verifies that the code_verifier matches the original code_challenge, ensuring the same client that started the authorization is completing it.\nDynamic Client Registration Dynamic Client Registration (DCR) allows applications to programmatically register OAuth clients without requiring manual configuration by a System Admin. This follows RFC 7591.\nEnabling DCR DCR is disabled by default and can be enabled by a System Admin:\nGo to System Console \u003e Integrations \u003e Integration Management Set Enable Dynamic Client Registration to True Security Warning: When DCR is enabled, the registration endpoint is publicly accessible without authentication. This means anyone can register OAuth clients on your Mattermost server. Only enable DCR if you understand and accept this security model, or have additional network-level access controls in place. Using DCR When enabled, applications can register themselves as either public or confidential clients by making a request to the registration endpoint (/api/v4/oauth/apps/register). The application specifies its redirect URIs and authentication method:\ntoken_endpoint_auth_method: \"none\" creates a public client (no client secret) token_endpoint_auth_method: \"client_secret_post\" creates a confidential client (with client secret) Discovery endpoint Mattermost provides an OAuth 2.0 Authorization Server Metadata endpoint at /.well-known/oauth-authorization-server that advertises server capabilities, including whether DCR is enabled and what authentication methods are supported.\nRefresh tokens Confidential clients receive refresh tokens and can use them to obtain new access tokens. Public clients do not receive refresh tokens for security reasons.\nOAuth endpoints Authorize URI /oauth/authorize Token URI /oauth/access_token User info URI /api/v4/users/me Delete your application Deleting the application will revoke access from all users. Only the user who created the application or a System Admin can delete the app.\n","permalink":"https://developers.mattermost.com/integrate/apps/authentication/oauth2/","section":"integrate","subsection":null,"tags":null,"title":"Use OAuth2.0 with Mattermost"},{"categories":null,"contents":"Server “Hello, world!” To get started extending server-side functionality with plugins, take a look at our server “Hello, world!” tutorial.\nWeb app “Hello, world!” To get started extending browser-side functionality with plugins, take a look at our web app “Hello, world!” tutorial.\nDemo plugin To see a demonstration of all server-side hooks and webapp components, take a look at our demo plugin.\nSample plugin To see a stripped down version of the demo plugin with just the build scripts and templates to get started, take a look at our plugin starter template.\nZoom The Zoom plugin for Mattermost adds UI elements that allow users to easily create and join Zoom meetings:\nTopics demonstrated:\nUses a custom HTTP handler to integrate with external systems. Defines a settings schema, allowing system administrators to configure the plugin via system console UI. Implements tests using the plugin/plugintest package. Creates rich posts using custom post types. Extends existing webapp components to add elements to the UI. JIRA The JIRA plugin for Mattermost creates a webhook that your JIRA server can use to post messages to Mattermost when issues are created:\nTopics demonstrated:\nUses a custom HTTP handler to integrate with external systems. Defines a settings schema, allowing system administrators to configure the plugin via system console UI. Implements tests using the plugin/plugintest package. Compiles and publishes releases for multiple platforms using Travis-CI. Profanity filter The profanity filter plugin for Mattermost automatically detects restricted words in posts and censors them prior to writing to the database. For more use cases, see this forum post.\nTopics demonstrated:\nInterception and modification of posts prior to writing them into the database. Rejection of posts prior to writing them into the database. Memes The Memes plugin for Mattermost creates a slash command that can be used to create dank memes:\nTopics demonstrated:\nRegisters a custom slash command. Uses a custom HTTP handler to generate and serve content. Compiles and publishes releases for multiple platforms using Travis-CI. ","permalink":"https://developers.mattermost.com/integrate/plugins/example-plugins/","section":"integrate","subsection":"plugins","tags":null,"title":"Example plugins"},{"categories":null,"contents":"This reference section contains information on integrating plugins with Mattermost, including Server SDK, Web App SDK, and REST API references.\n","permalink":"https://developers.mattermost.com/integrate/reference/","section":"integrate","subsection":null,"tags":null,"title":"Quick reference"},{"categories":null,"contents":"See here for server-specific best practices for plugins. Webapp-specific best practices are incoming.\nHow can a plugin enable its configuration through the System Console? Once a plugin is installed, Administrators have access to the plugin’s configuration page in the System Console \u003e Plugins section. The configurable settings must first be defined in the plugin’s manifest setting schema. The web app supports several basic pre-defined settings type, e.g. bool and dropdown, for which the corresponding UI components are provided in order to complete configuration in the System Console.\nThese settings are stored within the server configuration under [Plugins] indexed by plugin ids. The plugin’s server code can access their current configuration calling the getConfig API call and can also make changes as needed with saveConfig.\nHow can a plugin define its own setting type? A plugin could define its own type of setting with a corresponding custom user interface that will be displayed in the System Console following the process below.\nDefine a type: custom setting in the plugins manifest settings_schema\n\"settings_schema\": { \"settings\": [{ \"key\": \"NormalSetting\", \"type\": \"text\", + }, { + \"key\": \"CustomSetting\", + \"type\": \"custom\" }] } In the plugin’s web app code, define a custom component to manage the plugin’s custom setting and register it in the web app with registerAdminConsoleCustomSetting. This component will be instantiated in the System Console with the following props passed in:\nid: The setting key as defined in the plugin manifest within settings_schema.settings. label: The text for the component label based on the setting’s displayName defined in the manifest. helpText: The help text based on the setting’s helpText defined in the manifest. value: The setting’s current json value in the config at the time the component is loaded. disabled: Boolean indicating if the setting is disabled by a parent component within the System Console. config: The server configuration loaded by the web app. license: The license information for the related Mattermost server. setByEnv: Boolean that indicates if the setting is based on a server environment variable. onChange: Function that receives the setting id and current json value of the setting when it has been changed within the custom component. setSaveNeeded: Function that will prompt the System Console to enable the Save button in the plugin settings screen. registerSaveAction: Registers the given function to be executed when the setting is saved. This is registered when the custom component is mounted. unRegisterSaveAction: On unmount of the custom component, unRegisterSaveAction will remove the registered function executed on save of the custom component. On initialization of the custom component, the current value of the custom setting is passed in the props.value in a json format as read from the config. This value can be processed as necessary to display in your custom UI and ready to be modified by the end user. In the example below, it processes the initial props.value and sets it in a local state for the component to use as needed:\nconstructor(props) { super(props); this.state = { attributes: this.initAttributes(props.value), } } When a user makes a change in the UI, the OnChange handler sends back the current value of the setting as a json. Additionally, setSaveNeeded should be called to enable the Save button in order for the changes to be saved.\nhandleChange = () =\u003e { // ... this.props.onChange(this.props.id, Array.from(this.state.attributes.values())); this.props.setSaveNeeded() }; Once the user saves the changes, any handler that was registered with registerSaveAction will be executed to perform any additional custom actions the plugin may require, such as calling an additional endpoint within the plugin.\nFor examples of custom settings see: Demo Plugin `CustomSetting` and Custom Attributes Plugin implementation.\nHow can I review the entire code base of a plugin? Sometimes, you have been working on a personal repository for a new plugin, most probably based on the mattermost-plugin-starter-template repo. As it was a personal project, you may have pushed all of your commits directly to master. And now that it’s functional, you need a reviewer to take a look at the whole thing.\nFor this, it is useful to create a PR with only the commits you added. Follow these steps to do so:\nFirst of all, you need to obtain the identifier of the oldest commit that should be reviewed. You can review your history with git log --oneline, where you need to look for the very first commit that you added. Imagine that the output is something like the following:\nf7d89b8 (HEAD -\u003e master, origin/master) Lint code fa99500 Fix bug 0b3b5bd Add feature 8f6aef3 My first commit to the plugin ... ... rest of commits from mattermost-plugin-starter-template ... In this case, the identifier that we need to copy is 8f6aef3.\nCreate a new branch without the commits that you added. Using the SHA that you copied, create the branch base and push it:\ngit branch base 8f6aef3~1 git push origin base Note that 8f6aef3~1 means the parent commit of 8f6aef3, effectively selecting all the commits in the branch except the ones that you added.\nCreate a branch with all the commits, included the ones that you added, and push it. This branch, compare, will be an exact copy of master:\ngit branch compare master git push origin compare Now you have two new branches in the repository: base and compare. In Github, create a new PR in your repository, setting the base branch to base and the compare branch to compare.\nRequest a code review on the resulting PR.\nFor future changes, you can always repeat this process, making sure to identify the first commit you want to be reviewed. You can also consider the more common scenario of creating a feature branch (using something like git checkout -b my.feature.branch) and opening a PR whenever you want to merge the changes into master. It’s up to you!\nWhen to write a new API method or hook? Don’t be afraid to extend the API or hooks to support brand new functionality. Consider accepting an options struct instead of a list of parameters to simplify extending the API in the future:\n// GetUsers a list of users based on search options. // // Minimum server version: 5.10 GetUsers(options *model.UserGetOptions) ([]*model.User, *model.AppError) Old servers won’t do anything with new, unrecognized fields, but also won’t break if they are present.\nHow to expose performance metrics for a plugin? From Mattermost v9.4, a ServeMetrics hook can be used to expose performance metrics in the open metrics format under the common HTTP listener controlled by the MetricsSettings.ListenAddress config setting.\nData returned by the hook’s implementation through the given http.ResponseWriter object will be served through the http://SITE_URL:8067/plugins/PLUGIN_ID/metrics URL.\nHere’s a sample implementation using the Prometheus HTTP client library:\nimport ( \"net/http\" \"github.com/mattermost/mattermost/server/public/plugin\" \"github.com/prometheus/client_golang/prometheus\" \"github.com/prometheus/client_golang/prometheus/promhttp\" ) func (p *Plugin) initMetrics() { p.registry = prometheus.NewRegistry() // ... Registrations } func (p *Plugin) ServeMetrics(_ *plugin.Context, w http.ResponseWriter, r *http.Request) { promhttp.HandlerFor(p.registry, promhttp.HandlerOpts{}).ServeHTTP(w, r) } ","permalink":"https://developers.mattermost.com/integrate/plugins/best-practices/","section":"integrate","subsection":"plugins","tags":null,"title":"Best practices"},{"categories":null,"contents":"The Mattermost web Marketplace supports discovery, installation, and updates of extensions of the Mattermost platform. If you have built a plugin, app, playbook, integration, or tool that helps other users get more from Mattermost, the web Marketplace is a great way to get feedback on your contribution and help make it more popular. Once your submission is accepted to the web Marketplace, Mattermost will also send you swag! All contributions are eligible following a review from our team.\nTypes of integrations to submit to the web Marketplace Plugins Webhook configuration Playbook templates Utility tools, such as importers, command line interfaces, and scripts Submit your contribution for the web Marketplace Please fill out the information in this form to be reviewed by a member of the Mattermost team.\nEvery contribution goes through the following checklist before being added to the web Marketplace at mattermost.com/marketplace:\nThere is a link to the contribution so we can test it out. There is documentation to support people installing, configuring, and using the contribution. We also recommend including screenshots to give users a better understanding of the workflow and user experience. There is a public issue or bug tracker for users to report bugs or issues they encounter. You’ve included a link to an image for your contribution: Dimensions: 512 x 512 recommended File size: Maximum 5 MB File types: PNG, JPG, or SVG We will also reach out to you to learn more about the integration, your experience developing with Mattermost, and to coordinate postings for social media.\nSecurity issues Any security issues found in contributions in the web Marketplace should be reported by email to responsibledisclosure@mattermost.com, or sent directly to a member of the Security team on the Community Server.\nTakedown policy If an integration available through the Marketplace contains a medium or greater security issue or bug that blocks or prevents usage for users isn’t fixed within 14 days of being acknowledged, that integration will be removed from the Marketplace, and may be resubmitted to the Marketplace once the issue is resolved. Mattermost reserves the right to take down contributions at any time if a fix for a security issue is not forthcoming, or the issue is critical enough to justify an immediate takedown.\n","permalink":"https://developers.mattermost.com/integrate/marketplace-submissions/","section":"integrate","subsection":null,"tags":null,"title":"Contribute to the Marketplace"},{"categories":null,"contents":"You can create “zaps” that contain a trigger and an action for a task that you want to perform repeatedly. Zapier regularly checks your trigger for new data and automatically performs the action for you.\nUsing Zapier you can integrate over 700 apps into Mattermost, including Email, GitHub, Jira, Wufoo, Salesforce, Gmail, and many more.\nZapier setup guide Zapier is authorized using OAuth2.0. The setup guide requires that a System Admin register the Zapier app on their Mattermost server and can then optionally allow any users with a Zapier account to create integrations.\nEnable Zapier The first time you set up Zapier on your Mattermost instance you’ll be required to enable an OAuth 2.0 application which can be used by everyone on your server. Your System Admin must execute these steps.\nTo learn more about OAuth 2.0 applications, including what permissions they have access to, see the OAuth 2.0 documentation.\nEnable OAuth 2.0 Open Product menu \u003e System Console. Under Integrations \u003e Integration Management: Set Enable OAuth 2.0 Service Provider to True. If you’d like to allow Zapier integrations to post with customizable usernames and profile pictures, then set Enable integrations to override usernames and Enable integrations to override profile picture icons to True. Register Zapier as an OAuth 2.0 application Go to Product menu \u003e Integrations. Select OAuth 2.0 Applications \u003e Add OAuth 2.0 Application and enter the following fields: Is Trusted: No Display Name: Zapier Description: Application for Zapier integrations Homepage: https://zapier.com/ Icon URL: https://cdn.zapier.com/zapier/images/logos/zapier-logomark.png Callback URLs: https://zapier.com/dashboard/auth/oauth/return/MattermostDevAPI/ Select Save to create the application. You’ll be provided with a Client ID and Client Secret. Save these values, or share them with your team to connect Zapier in the steps below.\nCreate a Zap Sign up for a free Zapier account or log in if you already have one.\nOn your Zapier dashboard select Make a Zap!.\nTrigger App: Events in this app will trigger new messages in Mattermost.\nSelect a Trigger App: This will trigger new messages in Mattermost. If the app you’re looking to connect isn’t supported on Zapier, consider firing in-app events to a Gmail account and then connecting Gmail to Mattermost using Zapier. Select the Trigger Event: New messages in Mattermost will fire depending on these selected events in conjunction with any filters you apply. Connect the Trigger Account: Connect the account from which you’d like to trigger events and Test it to ensure Zapier can connect successfully. Filtering: (Optional) Exclude certain events from triggering new messages. Learn more about using Zapier custom filtering.\nAdd a filter by selecting the small + icon before the Action step. Zapier supports AND and OR filters. Use the dropdown selectors to choose what events will allow the trigger to send a Mattermost message. Mattermost Action: Connect your Mattermost Account and then specify posting details.\nSelect the Action App: Search for “Mattermost”. Select the Action Event: Select Post a Message. The Mattermost team plans to expand the actions available here. Connect the Action Account: Select Connect a New Account and enter the following fields: Mattermost URL: This is the URL you use to access your Mattermost site. Don’t include a slash at the end of the URL and don’t append a team to the end of the server URL. For example, https://community.mattermost.com/core is the entire URL to the Contributors team on our community server. The Mattermost URL entered here would be https://community.mattermost.com.\nClient ID/Secret: If Zapier has been enabled as an OAuth application as per the steps above, then these values can be found by navigating to one of your Mattermost teams, then Product menu \u003e Integrations \u003e OAuth 2.0 Applications. Select Show Secret next to the Zapier app, then obtain the Client ID and Client Secret.\nLog in to Mattermost: After completing the above fields you will be prompted to log in to your Mattermost account if you’re not logged in already. If you’re having trouble connecting then please read our troubleshooting guide.\nYou’ll then be prompted to allow Zapier to access your Mattermost account. Select Allow.\nMessage Post Details: Specify the formatting of the messages and the team/channel where messages will be posted.\nTeam: Choose the team where new messages will post. The dropdown should contain all teams you have access to on Mattermost.\nChannel: Choose the channel where new messages will post. The dropdown contains all channels that you belong to. Zapier cannot post into Direct Message channels.\nMessage Text: Enter the message text that will post to Mattermost. This text can be formatted using Markdown and include the dynamic fields offered by your selected trigger app. Read our message formatting tips below.\nUsername: This is the username that Zapier will post as. Zapier integrations will always appear with a BOT tag next to the username. In order for bots to override the username of the authorized user, your System Admin must set Enable integrations to override usernames to True.\nIcon URL: This is the profile picture of the bot that Zapier will post as. In order for bots to override the profile picture of the authorized user, your System Admin must set Enable integrations to override profile picture icons to True.\nTest the Zap: You may want to test your zap formatting in a Private Channel before posting in a channel that is visible to your entire team.\nMessage formatting tips Here are some useful tips we recommend to get the most out of Zapier integration:\nMarkdown: Mattermost supports the use of Markdown in Zapier integrations. For example, use heading markdown for Jira issue titles. Custom Icons: Use different icons for different services and Zapier integrations. Hashtags: Use hashtags to make your Zapier posts searchable. Use different hashtags for different services and Zapier integrations. For example, use the dynamic fields available in Zapier to include ticket a Jira ticket number in hashtags. This makes all conversation on a specific ticket instantly searchable by selecting the hashtag. Quick Links: Link back to the service that fired the zap through the use of Markdown embedded links. For example, in our zaps we embed a link back to the service within the timestamp so it’s easy to take action on any zap. Examples The Mattermost team has over 50 zaps integrated on our Community Contributors tem used for internal communication and interacting with contributors. The Community Heartbeat channel integrates all our community services in one accessible location. These zaps are formatted in two ways depending on the service:\nGitHub Issues and Comments, UserVoice Suggestions and Comments, GitLab MM Issues, GitLab Omnibus MM Issues\n#### [Title of issue] #[searchable-hashtag] in [external service](link to service) by [author](link to author profile) on [time-stamp](link to specific issue or comment) [Body of issue or comment] Forum Posts, Jira Comments, Hacker News Mentions, Tweets\n\u003e [forum post, media mention, or tweet] #[searchable-hashtag] in [external service](link to service) by [author](link to author profile) on [time-stamp](link to specific forum post, media mention or tweet) Troubleshooting guide Possible solutions to common issues encountered during setup.\nCannot connect a Mattermost account \"Token named access_token was not found in oauth response!\" a. Possible Solution: Try removing any trailing /’s on the end of your Mattermost URL.\nCorrect: https://community.mattermost.com Incorrect: https://community.mattermost.com/ \"[Server URL] returned (404)\" a. Possible Solution: The Mattermost URL cannot have a team appended to the end of the server URL.\nCorrect: https://community.mattermost.com Incorrect: https://community.mattermost.com/core \"[Server URL] returned (500) Internal Server Error\" a. Possible Solution: The Client Secret might be incorrect. Verify this value by selecting Integrations \u003e OAuth 2.0 Applications from the Product menu, or check with your System Admin.\n\"Error Invalid client id\" a. Possible Solution: The Client ID and/or Client Secret might have trailing spaces in them when copied and pasted into the form. Verify there are no trailing spaces in the Client ID and Client Secret fields then try again.\n\"Mattermost needs your help: We couldn't find the requested app\" a. Possible Solution: The Client ID might be incorrect. Verify this value by selecting Integrations \u003e OAuth 2.0 Applications from the Product menu, or check with your System Admin.\nDeauthorize the Zapier app If you’d like to deauthorize Zapier so it can no longer post through your connected account, select your avatar, then select Profile \u003e Security \u003e OAuth 2.0 Applications, then select Deauthorize on the Zapier app.\n","permalink":"https://developers.mattermost.com/integrate/zapier-integration/","section":"integrate","subsection":null,"tags":null,"title":"Integrate with Zapier"},{"categories":null,"contents":"In Mattermost v6.0, Plugin Helpers have been removed in favor of the Mattermost plugin API.\n","permalink":"https://developers.mattermost.com/integrate/plugins/helpers/","section":"integrate","subsection":"plugins","tags":null,"title":"Plugin helpers"},{"categories":null,"contents":"Once your plugin has reached a certain level of quality, you might consider submitting it to the web Marketplace. The Mattermost web Marketplace supports discovery, installation, and updates of extensions of the Mattermost platform.\nIf you have built a plugin, app, playbook, integration, or tool that helps other users get more from Mattermost, the web Marketplace is a great way to get feedback on your contribution and help make it more popular.\nRead more about the process on the Marketplace submissions page.\n","permalink":"https://developers.mattermost.com/integrate/plugins/community-plugin-marketplace/","section":"integrate","subsection":"plugins","tags":null,"title":"Community plugins in the Marketplace"},{"categories":null,"contents":"Getting your plugin onto our Community server https://community.mattermost.com is a valuable source of feedback. Whether you’re a core committer or anyone from the community, we want you to get feedback to improve your plugin.\nHowever we must ensure that our Community server remains stable for everyone. This document outlines the process of getting your plugin onto the Community server and some of these steps are required to get your plugin into the Marketplace.\nWhen you’re ready to begin this process for your plugin, ask in the Toolkit channel on the Community server. The PM, or someone else from the Integrations team, will help you start the process.\nChecklist Deploy to ci-extensions Basic Code Review passed CI system setup to build master Has a compatible licence. Deploy to ci-extensions-release Complete code review by two core committers. One focused on security. QA pass PM/UX review Release created and CI system setup to build releases Deploy to community.mattermost.com QA pass on ci-extensions-release of the release to deploy. Step definitions Basic code review Basic code review of an experimental plugin involves a quick review by a core committer to verify that the plugin does what it says it does and to provide any guidance and feedback. To make it easier to provide feedback, a PR can be made that contains all the code of the plugin that isn’t the boilerplate from mattermost-plugin-starter-template.\nWhen you are ready for your plugin to start this process, post an introduction in the Integrations and Apps channel on the Community server. Here is an example. CI system setup Setting up the CI system for your plugin will allow continuous testing of your master branch and releases on our testing servers. Master branch testing is done on https://ci-extensions.azure.k8s.mattermost.com/ and release testing is done on https://ci-extensions-release.azure.k8s.mattermost.com/.\nIn order to set this up, the plugin creator needs to provide a URL that hosts latest master build, which we can pull on a nightly basis. Once that exists you can make a request in the Integrations and Apps channel.\nCompatible licence Recommended licences:\nApache Licence 2.0 MIT BSD 2-clause BSD 3-clause More info Complete code review A more thorough code review is performed before allowing a plugin on ci-extensions-release. This review works the same as the basic code review, but the developers performing the review will be more thorough. If the developer that performed the first review is available, they should be one of the reviewers. Another of the reviewers should focus their review on any security implications of the plugin.\nQA pass QA pass involves getting a member of our QA team to take a look and verify the functionality advertised by your plugin.\nSteps involved:\nEnsure all setup documentation needed is clear and can be successfully followed. Dedicated instance or test account to access and test the third-party service, if applicable. Functional testing has been done to ensure the integration works as expected. For plugins owned by Mattermost, release testing is added to cover the main functionality of the plugin. PM/UX review A PM/UX pass involves getting PM support in ironing out any user experience or UI issues with the plugin.\nSteps involved:\nCreate a one paragraph summary of the integration. Document the main use cases in bullet form. Review the primary use cases and run through them to ensure they are complete, clear, and functional. Ensure there is documentation to support the plugin. This may include having sufficient helper text in the plugin. Consider if communication to other teams or users is required as part of the rollout to our Community server. ","permalink":"https://developers.mattermost.com/integrate/plugins/community_process/","section":"integrate","subsection":"plugins","tags":null,"title":"Process to include plugin on community"},{"categories":null,"contents":" The plugin manifest defines the metadata required to load and present your plugin. The manifest file should be named plugin.json or plugin.yaml and placed in the top of your plugin bundle. Example plugin.json: { \"id\": \"com.mycompany.myplugin\", \"name\": \"My Plugin\", \"description\": \"This is my plugin\", \"homepage_url\": \"https://example.com\", \"support_url\": \"https://example.com/support\", \"release_notes_url\": \"https://example.com/releases/v0.0.1\", \"icon_path\": \"assets/logo.svg\", \"version\": \"0.1.0\", \"min_server_version\": \"5.6.0\", \"server\": { \"executables\": { \"linux-amd64\": \"server/dist/plugin-linux-amd64\", \"darwin-amd64\": \"server/dist/plugin-darwin-amd64\", \"windows-amd64\": \"server/dist/plugin-windows-amd64.exe\" } }, \"webapp\": { \"bundle_path\": \"webapp/dist/main.js\" }, \"settings_schema\": { \"header\": \"Some header text\", \"footer\": \"Some footer text\", \"settings\": [{ \"key\": \"someKey\", \"display_name\": \"Enable Extra Feature\", \"type\": \"bool\", \"help_text\": \"When true, an extra feature will be enabled!\", \"default\": \"false\" }] }, \"props\": { \"someKey\": \"someData\" } } Table of contents id - String name - String description - String homepage_url - String support_url - String release_notes_url - String icon_path - String version - String min_server_version - String server - Object executables - Dict executable - String webapp - Object bundle_path - String settings_schema - Object header - String footer - String settings - Array key - String display_name - String type - String help_text - String regenerate_help_text - String placeholder - String default options - Array display_name - String value - String hosting - String secret - Bool sections - Array key - String title - String subtitle - String settings - Array key - String display_name - String type - String help_text - String regenerate_help_text - String placeholder - String default options - Array display_name - String value - String hosting - String secret - Bool header - String footer - String custom - Bool fallback - Bool props - Dict Documentation id - String\nThe id is a globally unique identifier that represents your plugin. Ids must be at least 3 characters, at most 190 characters and must match ^[a-zA-Z0-9-_\\.]+$. Reverse-DNS notation using a name you control is a good option, e.g. \"com.mycompany.myplugin\". name - String\nThe name to be displayed for the plugin. description - String\nA description of what your plugin is and does. homepage_url - String\nHomepageURL is an optional link to learn more about the plugin. support_url - String\nSupportURL is an optional URL where plugin issues can be reported. release_notes_url - String\nReleaseNotesURL is an optional URL where a changelog for the release can be found. icon_path - String\nA relative file path in the bundle that points to the plugins svg icon for use with the Plugin Marketplace. This should be relative to the root of your bundle and the location of the manifest file. Bitmap image formats are not supported. version - String\nA version number for your plugin. Semantic versioning is recommended: http://semver.org min_server_version - String\nThe minimum Mattermost server version required for your plugin. Minimum server version: 5.6 server - Object\nServer defines the server-side portion of your plugin. executables - Dict\nExecutables are the paths to your executable binaries, specifying multiple entry points for different platforms when bundled together in a single plugin. executable - String\nExecutable is the path to your executable binary. This should be relative to the root of your bundle and the location of the manifest file. On Windows, this file must have a \".exe\" extension. If your plugin is compiled for multiple platforms, consider bundling them together and using the Executables field instead. webapp - Object\nIf your plugin extends the web app, you'll need to define webapp. bundle_path - String\nThe path to your webapp bundle. This should be relative to the root of your bundle and the location of the manifest file. settings_schema - Object\nTo allow administrators to configure your plugin via the Mattermost system console, you can provide your settings schema. header - String\nOptional text to display above the settings. Supports Markdown formatting. footer - String\nOptional text to display below the settings. Supports Markdown formatting. settings - Array\nA list of setting definitions. key - String\nThe key that the setting will be assigned to in the configuration file. display_name - String\nThe display name for the setting. type - String\nThe type of the setting. \"bool\" will result in a boolean true or false setting. \"dropdown\" will result in a string setting that allows the user to select from a list of pre-defined options. \"generated\" will result in a string setting that is set to a random, cryptographically secure string. \"radio\" will result in a string setting that allows the user to select from a short selection of pre-defined options. \"text\" will result in a string setting that can be typed in manually. \"longtext\" will result in a multi line string that can be typed in manually. \"number\" will result in integer setting that can be typed in manually. \"username\" will result in a text setting that will autocomplete to a username. \"custom\" will result in a custom defined setting and will load the custom component registered for the Web App System Console. help_text - String\nThe help text to display to the user. Supports Markdown formatting. regenerate_help_text - String\nThe help text to display alongside the \"Regenerate\" button for settings of the \"generated\" type. placeholder - String\nThe placeholder to display for \"generated\", \"text\", \"longtext\", \"number\" and \"username\" types when blank. default\nThe default value of the setting. options - Array\nFor \"radio\" or \"dropdown\" settings, this is the list of pre-defined options that the user can choose from. display_name - String\nThe display name for the option. value - String\nThe string value for the option. hosting - String\nThe intended hosting environment for this plugin setting. Can be \"cloud\" or \"on-prem\". When this field is set, and the opposite environment is running the plugin, the setting will be hidden in the admin console UI. Note that this functionality is entirely client-side, so the plugin needs to handle the case of invalid submissions. secret - Bool\nIf true, the setting is sanitized before showing it in the System Console or returning it via the API. This is useful for settings that contain sensitive information. sections - Array\nA list of settings section definitions. key - String\nA unique identifier for this section. title - String\nOptional text to display as section title. subtitle - String\nOptional text to display as section subtitle. settings - Array\nA list of setting definitions to display inside the section. key - String\nThe key that the setting will be assigned to in the configuration file. display_name - String\nThe display name for the setting. type - String\nThe type of the setting. \"bool\" will result in a boolean true or false setting. \"dropdown\" will result in a string setting that allows the user to select from a list of pre-defined options. \"generated\" will result in a string setting that is set to a random, cryptographically secure string. \"radio\" will result in a string setting that allows the user to select from a short selection of pre-defined options. \"text\" will result in a string setting that can be typed in manually. \"longtext\" will result in a multi line string that can be typed in manually. \"number\" will result in integer setting that can be typed in manually. \"username\" will result in a text setting that will autocomplete to a username. \"custom\" will result in a custom defined setting and will load the custom component registered for the Web App System Console. help_text - String\nThe help text to display to the user. Supports Markdown formatting. regenerate_help_text - String\nThe help text to display alongside the \"Regenerate\" button for settings of the \"generated\" type. placeholder - String\nThe placeholder to display for \"generated\", \"text\", \"longtext\", \"number\" and \"username\" types when blank. default\nThe default value of the setting. options - Array\nFor \"radio\" or \"dropdown\" settings, this is the list of pre-defined options that the user can choose from. display_name - String\nThe display name for the option. value - String\nThe string value for the option. hosting - String\nThe intended hosting environment for this plugin setting. Can be \"cloud\" or \"on-prem\". When this field is set, and the opposite environment is running the plugin, the setting will be hidden in the admin console UI. Note that this functionality is entirely client-side, so the plugin needs to handle the case of invalid submissions. secret - Bool\nIf true, the setting is sanitized before showing it in the System Console or returning it via the API. This is useful for settings that contain sensitive information. header - String\nOptional text to display above the settings. Supports Markdown formatting. footer - String\nOptional text to display below the settings. Supports Markdown formatting. custom - Bool\nIf true, the section will load the custom component registered using `registry.registerAdminConsoleCustomSection` fallback - Bool\nIf true and Custom = true, the settings defined under this section will still render as fallback (unless the individual setting is type 'custom') when the plugin is disabled. props - Dict\nPlugins can store any kind of data in Props to allow other plugins to use it. ","permalink":"https://developers.mattermost.com/integrate/plugins/manifest-reference/","section":"integrate","subsection":"plugins","tags":null,"title":"Manifest reference"},{"categories":null,"contents":"Some plugins authored by Mattermost are licensed under the Mattermost Source Available License. This document outlines how to apply the license in various situations.\nHow do I apply the license to an enterprise-only plugin? An Enterprise-only plugin is a plugin that requires a valid Mattermost Enterprise E20 license. It is not designed to be used with Team Edition or any other Enterprise license.\nAdd the LICENSE file to the root of your plugin repository.\nAdd the following section to your README.md directly below the opening paragraph:\n## License This repository is licensed under the [Mattermost Source Available License](LICENSE) and requires a valid Enterprise E20 license. See Mattermost Source Available License to learn more. How do I apply the license to a mixed-license plugin? A mixed-license plugin includes components that require a valid Mattermost Enterprise E20 license. Not all features are available when used with Team Edition or any other enterprise license.\nOrganize the Enterprise-only, server-side parts of your plugin in a dedicated folder, typically server/enterprise.\nAdd the LICENSE file to the server/enterprise folder.\nSymlink that license in the root of your repository as LICENSE.enterprise.\nAdd the following section to your README.md directly below the opening paragraph:\n## License This repository is licensed under the Apache 2.0 License, except for the [server/enterprise](server/enterprise) directory which is licensed under the [Mattermost Source Available License](LICENSE.enterprise). See Mattermost Source Available License to learn more. ","permalink":"https://developers.mattermost.com/integrate/plugins/source-available-license/","section":"integrate","subsection":"plugins","tags":null,"title":"Source available license"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/","section":"","subsection":null,"tags":null,"title":"Mattermost Developers"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/","section":"categories","subsection":null,"tags":null,"title":"Categories"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/cloud/","section":"categories","subsection":null,"tags":null,"title":"cloud"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/performance/","section":"categories","subsection":null,"tags":null,"title":"performance"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/testing/","section":"categories","subsection":null,"tags":null,"title":"testing"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/i18n/","section":"categories","subsection":null,"tags":null,"title":"i18n"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/go/","section":"categories","subsection":null,"tags":null,"title":"Go"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/devops/","section":"categories","subsection":null,"tags":null,"title":"devops"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/opensource/","section":"categories","subsection":null,"tags":null,"title":"opensource"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/security/","section":"categories","subsection":null,"tags":null,"title":"security"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/ui-automation/","section":"categories","subsection":null,"tags":null,"title":"ui automation"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/integrate/","section":"integrate","subsection":null,"tags":null,"title":"Integrates"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/git/","section":"categories","subsection":null,"tags":null,"title":"git"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/markdown/","section":"categories","subsection":null,"tags":null,"title":"markdown"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/linting/","section":"categories","subsection":null,"tags":null,"title":"“linting”"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/announcements/","section":"categories","subsection":null,"tags":null,"title":"announcements"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/kubernetes/","section":"categories","subsection":null,"tags":null,"title":"kubernetes"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/plugins/","section":"categories","subsection":null,"tags":null,"title":"plugins"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/contributing/","section":"categories","subsection":null,"tags":null,"title":"contributing"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/categories/announcement/","section":"categories","subsection":null,"tags":null,"title":"announcement"},{"categories":null,"contents":"NOTE: We don’t officially support Arch Linux for use with the Mattermost Desktop App. The provided guide is unofficial.\nOpen a terminal\nInstall nvm via\nnvm-sh: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash OR AUR (possibly using a helper): yay -S nvm Install NodeJS via\nnvm install --lts Install other dependencies:\nLinux requires the X11 development libraries and libpng to build native Node modules. Arch requires libffi since it’s not installed by default.\nsudo pacman -S npm git python3 gcc make libx11 libxtst libpng libffi Notes To build RPMs, you need rpmbuild\nsudo pacman -S rpm ","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/developer-setup/arch/","section":"contribute","subsection":"more info","tags":null,"title":""},{"categories":null,"contents":" Install Homebrew: http://brew.sh\nOpen Terminal\nInstall dependencies\nbrew install git python3 Install NVM by following these instructions.\nAfter installing, follow the post-install steps shown by the installer to add the necessary lines to your shell profile (for example ~/.zshrc or ~/.bash_profile). Then open a new terminal and run:\nnvm install --lts ","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/developer-setup/macos/","section":"contribute","subsection":"more info","tags":null,"title":""},{"categories":null,"contents":"NOTE: We don’t officially support Fedora/Red Hat/CentOS Linux for use with the Mattermost Desktop App. The provided guide is unofficial.\nOpen Terminal\nInstall NodeJS via nvm:\ncurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash nvm install --lts Install other dependencies:\nLinux requires the X11 developement libraries and libpng to build native Node modules.\nsudo yum install git python3 g++ libX11-devel libXtst-devel libpng-devel Notes To build RPMs, you need rpmbuild:\nsudo dnf install rpm-build ","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/developer-setup/redhat/","section":"contribute","subsection":"more info","tags":null,"title":""},{"categories":null,"contents":" Open Terminal\nInstall NodeJS from one of the following sources:\nnvm:\ncurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash nvm install --lts NodeSource:\nsudo apt-get update sudo apt-get install -y ca-certificates curl gnupg sudo mkdir -p /etc/apt/keyrings curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main\" | sudo tee /etc/apt/sources.list.d/nodesource.list sudo apt-get update sudo apt-get install -y nodejs You might need to install curl as well:\nsudo apt install curl Install other dependencies:\nLinux requires the X11 developement libraries and libpng to build native Node modules.\nsudo apt install git python3 make g++ libx11-dev libxtst-dev libpng-dev Notes To build RPMs, you need rpmbuild:\nsudo apt install rpm ","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/developer-setup/ubuntu/","section":"contribute","subsection":"more info","tags":null,"title":""},{"categories":null,"contents":" Install Chocolatey: https://chocolatey.org/install\nInstall Visual Studio Community: https://visualstudio.microsoft.com/vs/community/\nInclude Desktop development with C++ when installing If you are on Windows 11, you may need to install wmic via the system settings \u003e Optional Features.\nOpen PowerShell\nInstall dependencies\nchoco install nvm git python3 Restart PowerShell (to refresh the environment variables)\nRun nvm install lts and nvm use lts to install and use the latest NodeJS LTS version.\n","permalink":"https://developers.mattermost.com/contribute/more-info/desktop/developer-setup/windows/","section":"contribute","subsection":"more info","tags":null,"title":""},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/search/searchindex/index.json","section":"search","subsection":null,"tags":null,"title":""},{"categories":null,"contents":" ","permalink":"https://developers.mattermost.com/api-documentation/","section":"api-documentation","subsection":null,"tags":null,"title":"API Documentation"},{"categories":null,"contents":"How should plugins serve publicly available static files? Add all static files under a file directory named public within the plugin directory, and include the files in the plugin bundle using the Makefile.\nHow do plugins make sure http requests are authentic? Plugins can implement the ServeHTTP to listen to http requests. This can be used to receive post action requests when Interactive Messages Buttons and Menus are triggered by users.\nWhen plugins act as an HTTP server, they serve requests from Mattermost clients (which are authenticated in a Mattermost sense), but may also serve HTTP requests from external services like webhooks. These requests from external services might use the Authorization header to authorize themselves against the plugin.\nSince these requests are just HTTP requests, anyone can send them to the plugin. Hence, the plugin must make sure the requests are authentic. The Mattermost Server sets the HTTP header Mattermost-User-Id when the request is made by an authenticated client. If the plugin is expecting the request to come from an authenticated Mattermost user, and the Mattermost-User-Id is blank, it’s the plugin’s responsibility to reject the request.\nFrom Mattermost v9.4, external systems can use the Authorization header to authenticate with the plugin. HTTP requests to server-side plugins can use an Authorization header in the request for the plugin to use, and the header value will be present in the request received by the plugin, as long as the token provided in the header is not a user token issued by the Mattermost server. This is useful for connecting external systems that require authentication through an Authorization header with their own token.\n","permalink":"https://developers.mattermost.com/integrate/plugins/components/server/best-practices/","section":"integrate","subsection":"plugins","tags":null,"title":"Best practices"},{"categories":null,"contents":"Design best practices Actions that apply to specific Channels Recommendation: Have your plugin register the actions to the channel header. This makes it quickly accessible for users and the actions apply on the channel they’re viewing.\nExample: Zoom meeting posts to a channel\nYou can additionally register a slash command on the server-side to take channel-specific actions.\nExample: Jira project actions\nActions that apply to specific messages Recommendation: Have your plugin register a post dropdown menu component with some text, icon and an action function. This adds your action to the “More Actions” post menu dropdown for easy discovery.\nExamples: Create or attach to Jira issue from a message; copy a message to another channel; report an inappropriate message\nSample code: mattermost/mattermost-plugin-todo/webapp/src/index.js\nActions related to files or images Recommendation: Have your plugin register a file upload method with some text, icon and an action function. This adds your new action to the file upload menu.\nExamples: File sharing from OneDrive or GDrive; Draw plugin for sketches\nActions that apply to specific teams Recommendation: Have your plugin register left sidebar header component with some text, icon and an action function. This adds your action above your team’s channels in the sidebar.\nExamples: Trello kanban board plugin, GitHub Plugin\nQuick links or status summaries of workflows Recommendation: Have your plugin register a bottom team sidebar component. This adds icons to the lower left corner of the UI.\nExamples: GitHub sidebar links with summary of outstanding reviews or unread messages; ServiceNow incident status summary\nGlobal actions that can be taken anywhere in the server and not directly related to teams, channels or users Recommendation: Have your plugin register main menu action with some text, icon for mobile, and an action function. This adds your action to the Main Menu. You can additionally register a slash command on the server-side.\nExamples: Share feedback plugin in Main Menu; /jira slash commands for quick actions\nActions that apply to specific users Recommendation: Have your plugin register a popover user actions component. This adds your action button to the user profile popover.\nExamples: Report User plugin; Display extra information about the user from an LDAP server\nExtra information on a user profile Recommendation: Have your plugin register a popover user attribute component. This adds your custom attributes to the user profile popover.\nExamples: Custom User Attributes plugin\nActions related to emoji and GIFs Recommendation: Have your plugin add a component to the emoji picker. This is not yet supported, but some work had previously started with the issue currently opened as Help Wanted. Examples: Bitmoji plugin; GIFs via Giphy or Gfycat Set up the plugin to properly communicate with the Mattermost Server Use the Mattermost Server’s SiteURL in your web app plugin In order to make sure your plugin has full compatibility with your Mattermost server, you should use the server’s configured SiteURL in each API call you send to the server from the webapp. Here’s an example of how to compute the SiteURL:\nexport const getPluginServerRoute = (state) =\u003e { const config = getConfig(state); let basePath = ''; if (config \u0026\u0026 config.SiteURL) { basePath = new URL(config.SiteURL).pathname; if (basePath \u0026\u0026 basePath[basePath.length - 1] === '/') { basePath = basePath.substr(0, basePath.length - 1); } } return basePath + '/plugins/' + PluginId; }; Include the server’s CSRF token in your web app plugin’s requests The Mattermost server can be configured to require a CSRF token to be present in HTTP requests sent from the webapp. In order to include the token in each request, you can use the mattermost-redux library’s Client4.getOptions function to add the token to your fetch request. Here’s an example of how to include the CSRF token.\nconst response = await fetch(url, Client4.getOptions(options)); ","permalink":"https://developers.mattermost.com/integrate/plugins/components/webapp/best-practices/","section":"integrate","subsection":"plugins","tags":null,"title":"Best practices"},{"categories":null,"contents":"Mattermost uses the Docker Registry to publish the official images for the Mattermost Server and also for other supporting images that are used for internal/public development and testing.\nThis page lists all the Docker repositories currently in use.\nMattermost official docker images mattermost/mattermost-enterprise-edition - Official Mattermost Server image for the Enterprise Edition version. To find the Dockerfile please refer to the GitHub repo.\nmattermost/mattermost-team-edition - Official Mattermost Server image for the Team Edition version. To find the Dockerfile please refer to the GitHub repo.\nmattermost/mattermost-push-proxy - Mattermost Push Proxy. Documentation. GitHub repo.\nmattermost/mattermost-loadtest - Image for the Load Test application. Tools for profiling Mattermost under heavy load. GitHub repo.\nmattermost/mattermost-operator - Official image for Mattermost Operator for Kubernetes. For more information please refer to the GitHub repo.\nmattermost/mattermost-cloud - Mattermost Private Cloud is a SaaS offering meant to smooth and accelerate the customer journey from trial to full adoption. For more information please refer to the GitHub repo.\nmattermost/mattermost-preview - This is a Docker image to install Mattermost in Preview Mode for exploring product functionality on a single machine using Docker. Documentation. GitHub repo.\nmattermost/platform - Mirror of mattermost/mattermost-preview. This is a Docker image to install Mattermost in Preview Mode for exploring product functionality on a single machine using Docker. Preview image (mirror). Documentation. GitHub repo.\nCommunity-maintained Docker images mattermost/mattermost-prod-app - Community driven image for Mattermost Server. This Docker repository will be deprecated in Mattermost 6.0. For more information and to check the Dockerfile please refer to the GitHub repo.\nmattermost/mattermost-prod-db - Community driven image for Database to run together with mattermost/mattermost-prod-app. This Docker repository will be deprecated in Mattermost 6.0. For more information and to check the Dockerfile please refer to the GitHub repo.\nmattermost/mattermost-prod-web - Community driven image for WebServer to run together with mattermost/mattermost-prod-app. This Docker repository will be deprecated in Mattermost 6.0. For more information and to check the Dockerfile please refer to the GitHub repo.\nMattermost internal Docker images mattermost/mattermost-test-enterprise - Repository where all testing images are published and available for any type of testing. These images are built from the CircleCI Pipelines from the mattermost-server and mattermost-webapp.\nmattermost/mattermost-test-team - Repository where all testing images are published and available for any type of testing. These images are built from the CircleCI Pipelines from the mattermost-server and mattermost-webapp.\nmattermost/mattermost-elasticsearch-docker - Used in in CI and for local development. Please refer to the GitHub repo for more information.\nmattermost/mattermost-build-server - Image used to build Mattermost used in CI. To check the Docker file refer to the GitHub repo.\nmattermost/mattermost-wait-for-dep - Image used to wait for the other containers to start. Used in in CI and for local development. Please refer to the GitHub repo for more information.\nmattermost/sync-helpwanted-tickets - For internal use. This image runs the sync with Jira tickets and GitHub Issues. To check the code please refer to the GitHub repo.\nmattermost/podman - For internal use. Contains Podman to build/tag/push container images.\nmattermost/chewbacca - For internal use. A GitHub Bot for administrative tasks. Please refer to the GitHub repo for more information.\nmattermost/matterwick - For internal use. A GitHub Bot to spin test servers for pull requests. Please refer to the GitHub repo for more information.\nmattermost/webrtc - DEPRECATED. Preview docker image of Mattermost WebRTC.\n","permalink":"https://developers.mattermost.com/contribute/more-info/containers/","section":"contribute","subsection":"more info","tags":null,"title":"Containers"},{"categories":null,"contents":"Plugins communicate with the main Mattermost Server by RPC. In order to debug them with Delve, a few steps are necessary.\nmacOS After starting the main Mattermost application, run ps aux | grep name.of.your.plugin. This will print a list of running processes that match that name, as such: username 78836 0.0 0.1 4397696 12492 s006 S 7:07AM 0:00.03 plugins/name.of.your.plugin/server/dist/plugin-darwin-amd64. Grab the pid, which is the second number after your username in the output above. Run dlv attach pid, where pid is that number. let the plugin continue executing soon after connecting; Otherwise, the server will detect it as stopped and attempt to restart it. You’re done. You should have access to your plugin’s code through Delve and be able to set breakpoints, etc. ","permalink":"https://developers.mattermost.com/integrate/plugins/components/server/debugging/","section":"integrate","subsection":"plugins","tags":null,"title":"Debug Server plugins"},{"categories":null,"contents":"Hundreds of developers around the world contribute to Mattermost open source projects. Our Hall of Fame honors the best of the best.\nThe title of “Most Valued Professional” is awarded to an outstanding contributor for a key Mattermost release.\nVersion Release date MVP 11.2 December 16, 2025 Angel Mendez 11.1 November 14, 2025 Vicktor 11.0 October 16, 2025 Kaya Zeren 10.12 September 16, 2025 Lucas Reis 10.11 August 15, 2025 Alan Lew 10.10 July 16, 2025 Harsh Aulakh 10.9 June 16, 2025 Pineoak 10.8 May 16, 2025 Lucas Reis 10.7 April 16, 2025 Clément Collin 10.6 March 14, 2025 Vicktor 10.5 February 19, 2025 TheInvincible 10.4 January 16, 2025 Rohan Sharma 10.3 December 16, 2024 Nicolas Le Cam 10.2 November 15, 2024 Tanmay Thole and Rutam Prita Mishra 10.1 October 16, 2024 Ivy Gesare 10.0 September 16, 2024 Rita Anene 9.11 August 16, 2024 Arya Khochare 9.10 July 16, 2024 Frank Paul Silye 9.9 June 14, 2024 Anna Os and Ezekiel 9.8 May 16, 2024 Varghese Jose 9.7 April 16, 2024 Tanmay Thole 9.6 March 15, 2024 Syed Ali Abbas Zaidi 9.5 February 16, 2024 Tom De Moor 9.4 January 16, 2024 Paul Stern 9.3 December 15, 2023 Rutam Prita Mishra 9.2 November 16, 2023 Yusuke Nemoto 9.1 October 16, 2023 KyeongSoo Kim 9.0 September 15, 2023 Kaya Zeren 8.1 August 24, 2023 Ben Bodenmiller 8.0 July 14, 2023 Roy Orbitson 7.10 April 14, 2023 Matthew Dorner and Alexander Griesser 7.9 March 16, 2023 Matthew Dalton 7.8 February 16, 2023 Sinan Sonmez 7.7 January 16, 2023 Julien Fabre 7.5 November 16, 2022 Ayroti Dey Sarkar 7.4 October 16, 2022 Sridhar 7.3 September 16, 2022 Vishakha Poonia 7.2 August 16, 2022 Alexander Griesser 7.1 July 16, 2022 kamre 7.0 June 16, 2022 Tom De Moor 6.7 May 16, 2022 Vishakha Poonia 6.6 April 16, 2022 Sayanta Banerjee 6.5 March 16, 2022 Sinan Sonmez 6.4 February 16, 2022 Suneet Srivastava 6.3 January 16, 2022 Ujjwal Sharma 6.2 December 16, 2021 Julien Fabre 6.1 November 16, 2021 Penthaa Patel 6.0 October 13, 2021 Johannes Marbach 5.39 September 16, 2021 Rutam Prita Mishra 5.38 August 16, 2021 kamre 5.37 July 16, 2021 Sera Geyran 5.36 June 16, 2021 Ben Bodenmiller 5.35 May 16, 2021 Alexander Brenchev 5.34 April 16, 2021 darkLord19 5.33 March 16, 2021 Mahmudul Haque 5.32 February 16, 2021 Yusuke Nemoto 5.31 January 16, 2021 Haardik Dharma 5.30 December 16, 2020 XxLilBoPeepsxX 5.29 November 16, 2020 Tom De Moor 5.28 October 16, 2020 Soo Hwan Kim 5.27 September 16, 2020 Mohan Prasath 5.26 August 16, 2020 Abdu Assabri 5.25 July 16, 2020 Rodrigo Villablanca 5.24 June 16, 2020 Rakesh Peela 5.23 May 16, 2020 Vladimir Lebedev 5.22 April 16, 2020 Tim Estermann 5.21 March 16, 2020 Allan Guwatudde 5.20 February 16, 2020 Md Zubair Ahmed 5.19 January 16, 2020 Allen Lai 5.18 December 16, 2019 larkox 5.17 November 16, 2019 Andre Vasconcelos 5.16 October 16, 2019 Paulo Bittencourt 5.15 September 16, 2019 Siyuan Liu 5.14 August 16, 2019 Rodrigo Villablanca Vásquez 5.13 July 16, 2019 Adrian Mönnich 5.12 June 16, 2019 Scott Davis 5.11 May 16, 2019 Maneschi Romain 5.10 April 16, 2019 Grzegorz Kosik 5.9 March 16, 2019 JtheBAB 5.8 February 16, 2019 Kyâne Pichou 5.7 January 16, 2019 sadb 5.6 December 16, 2018 Pradeep Murugesan 5.5 November 16, 2018 Mukul Rawat 5.4 October 16, 2018 Hanzei 5.3 September 16, 2018 SmartHoneybee 5.2 August 16, 2018 Daniel Schalla 5.1 July 16, 2018 Hyeseong Kim 5.0 June 16, 2018 William Gathoye 4.10 May 16, 2018 Siyuan Liu 4.9 April 16, 2018 Christian Hoff 4.8 March 16, 2018 Pierre de La Morinerie of Codeurs en Liberté 4.7 February 16, 2018 Kher Yee Ting 4.6 January 16, 2018 Yusuke Nemoto 4.5 December 16, 2017 Ryan Wang 4.4 November 16, 2017 Sudheer Timmaraju 4.3 October 16, 2017 Jesús Espino 4.2 September 16, 2017 Uber Development Team 4.1 August 16, 2017 Nazar Laba 4.0 July 16, 2017 prixone and sousapro 3.10 June 16, 2017 Galois, Inc. 3.9 May 16, 2017 Carlos Tadeu Panato Junior 3.8 April 16, 2017 VeraLyu 3.7 March 16, 2017 Saturnino Abril 3.6 January 16, 2017 Carlos Tadeu Panato Junior 3.5 November 16, 2016 Harshavardhana 3.4 September 16, 2016 Wim van Wemmel 3.3 August 16, 2016 Ivan Naydonov 3.2 July 16, 2016 Christian Arnold 3.1 June 16, 2016 Thomas Balthazar ","permalink":"https://developers.mattermost.com/contribute/more-info/mvp/","section":"contribute","subsection":"more info","tags":null,"title":"MVP"},{"categories":null,"contents":"","permalink":"https://developers.mattermost.com/search/","section":"search","subsection":null,"tags":null,"title":"Search results"}]