ControlAltDieliet loves Mattermost

Plugins: The serverside

In the previous article we gave a global overview. Now we dive deeper in the serverside of the app. This part is written in Golang. At the end of the article you will have a "hello world" plugin that has a slash command /hello that prints world and when someone logs at your server, it says hello world to that person. You'll learn how to store and read configuration variables. This is not a full tutorial on what Serverside plugins can do, but a starting guide.
The serverside let your plugin react on events happening on the server or outside your server.
You can let your plugin interact with the Plugin API that is very similar to the REST API. We gave an introduction on the REST API in a previous article.

You have hooks and a API on the server side.

I made a git-repository from MatterMost Starter Template Plugin because it is minimalistic and is a good start. My repository contains a branch for each further step. You can find it at The Mattermost demo plugin gives a very nice overview but can be overwhelming for a first start but is a good reference. I also recomment to take a look at the Welcome bot or the Todo bot as starting point. Hooks are a first way your Plugin interacts with the server.They listen to events and let you react on events. The second way is the API let you do things on the Mattermost Server (add a user to a channel, post a message, create a channel, find a post).


1 Go for developers in less than 10 lines

Did you never written anything in Go but you have experience with other languages? Here is a very fast forward things you need to know Go based on my experiences. A good quickstart you can find at https://tour.golang.org

  1. Variables are declared with var variable name or in the short way: variable_name:=value
  2. Go code is structured in packages. All the files in a single directory form a package. When you have a folder myfirstpackage with two files a.go and b.go. In the file a.go you wrote a function with the name whattheheck. You can call this function in your file b.go without any import.
  3. Speaking about functions: defining a function goes like this: func mycoolfunctionname (an_integer int, and_a_strings str) str {}
    • The type of a variable is specified after the variable name.
    • A function can return multiple variables.
  4. Go doesn't know classes. Methods are just functions but have a special receiver argument between the func and the method name.
    You can only declare a method with a receiver whose type is defined in the same package as the method.
    func (a_variable a_type) mycoolfunctionname (an_integer int, and_a_strings str) {}
  5. The above example use a_variable as a value but the original a_variable will not be changed. If an * is added to the type, a_variable will be used a
    You can only declare a method with a receiver whose type is defined in the same package as the method.
    func (a_variable a_type) mycoolfunctionname (an_integer int, and_a_strings str) {}
This must be the shortest introduction to Go ever written.


2 Clone my code into your environment.

Go to your mmplugins folder and give the following command: git clone
You have now a folder mattermost-plugin-starter with my code in.

cd mmplugins
git clone https://github.com/ctlaltdieliet/mattermost-plugin-starter.git
clone code


3 Modify your configuration file.

First we modify our plugin.json file as described in the previous article.
The id has to be unique as it will be used as identifier in Mattermost for your plugin. Your reversed FQDN combined with your username and a plugin name will be quite unique.
In the repository this is branch server_step1. Use 'git checkout origin/server_step1' to see the code

       
        {
          "id": "be.controlaltdieliet.demoortom.first_mattermost_plugin",
          "name": "My First Plugin",
          "description": "This plugin is a first test.",
          "version": "0.1.0",
          "min_server_version": "5.2.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"
              }
          }
          "settings_schema": {
            "header": "Configure your demo plugin settings below.",
            "footer": "Footer: The code for this demo plugin can be found [here](https://github.com/mattermost/mattermost-plugin-demo).",
            "settings": [
                {
                    "key": "defaultgreeting",
                    "display_name": "The default answer:",
                    "type": "text",
                    "help_text": "The default greeting if no name is available",
                    "placeholder": "Hi there",
                    "default": "Hi there"
                }
            ]
        }
      }
      


4 Adding a slash command

Let's add a slash command to Mattermost through your plugin.
For this example we will add all our code to the plugin.go file. Later we will move some code to a new file commands.go.
Adding a slash command goes in two steps. First you have to register your command. But registering isn't enough. You also have to add functionality to your slash command. You do that with the ExcecuteCommand.


The function OnActivate() is excecuted when a plugin gets enabled.
It's a good place to register your slash command.
If the function OnActivate() throws an error, your plugin will not be enabled.
When you register the command, you can add AutoComplete functionality that gives a good user experience. The function ExcecuteCommand that we use to add functionality to our slash command is actually a hook that is called when a slash command is excecuted.
You find the slash command in the args.Command parameter of the function.
Your plugin will respond with Word. We use the CommandResponse function for that
.
In the repository this is branch server_step2. Use git checkout server_step2 to see the code.

       
package main

import (
	"fmt"
	"strings"
	"sync"

	"github.com/mattermost/mattermost-server/v5/model"
	"github.com/mattermost/mattermost-server/v5/plugin"
	"github.com/pkg/errors"
)

// Plugin implements the interface expected by the Mattermost server to communicate between the server and plugin processes.
type Plugin struct {
	plugin.MattermostPlugin

	// configurationLock synchronizes access to the configuration.
	configurationLock sync.RWMutex

	// configuration is the active plugin configuration. Consult getConfiguration and
	// setConfiguration for usage.
	configuration *configuration
}

// registering a slash command /hello
func (p *Plugin) OnActivate() error {

	if err := p.API.RegisterCommand(&model.Command{
		Trigger:          "hello",
		AutoComplete:     true,
		AutoCompleteDesc: "responds world",
	}); err != nil {
		return errors.Wrapf(err, "failed to register  command")
	}
	return nil

}

// adding functionality to to slash command
func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {

	// reading the slash command that the user entered
	trigger := strings.TrimPrefix(strings.Fields(args.Command)[0], "/")
	switch trigger {
	case "hello":
		return &model.CommandResponse{
			ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL,
			Text:         fmt.Sprintf("World"),
		}, nil

	default:
		return &model.CommandResponse{
			ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL,
			Text:         fmt.Sprintf("Unknown__ command: " + args.Command),
		}, nil
	}
}


          

confirmation built and enabled

5 Build, upload and enable your plugin to your mattermost instance

Now let's upload your plugin and also enable it through mmctl. I stay in the root folder of the plugin and execute following commands. I use these code alot for testing so my first lines actually deletes the previous version.
You use your plugin id for deleting it.
make dist builds your plugin
The third line adds the plugin (it uses the file that is build in the folder dist)
the fourth line enables the plugin

       
  ../mattermost-server/bin/mmctl plugin delete be.controlaltdieliet.demoortom.first_mattermost_plugin
make dist
../mattermost-server/bin/mmctl plugin add ./dist/be.controlaltdieliet.demoortom.first_mattermost_plugin-0.1.0.tar.gz 
../mattermost-server/bin/mmctl plugin enable be.controlaltdieliet.demoortom.first_mattermost_plugin 
confirmation built and enabled

6 Bringing structure in your code

Putting everything in the plugin.go file is not a good practice. It's better to move to your functions to a file named commands.go


7 Let's get personal

Instead of responding with just a text, we'll make it some more personal. The commands sends the userid from the user who gave the command. We use the Plugin API to retrieve the username and respond with it. You can see this in the branch server_step3. Use git checkout origin/server_step3 to see the code

       
func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
	trigger := strings.TrimPrefix(strings.Fields(args.Command)[0], "/")
	username := ""
	switch trigger {
	case "hello":
		UserId := args.UserId

		user, appErr := p.API.GetUser(UserId)
		if appErr != nil {
			username = "there"
		} else {
			username = user.GetFullName()
		}

		//fmt.Sprintf(Apperr.Message)
		return &model.CommandResponse{
			ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL,
			Text:         fmt.Sprintf("Hi " + username),
		}, nil

	default:
		return &model.CommandResponse{
			ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL,
			Text:         fmt.Sprintf("Unknown__ command: " + args.Command),
		}, nil
	}
}                      
responds with name

8 Let's put and retrieve a variable in the configuration

Let's add some variables in the configuration file configuration.go. You can see this in the branch server_step4. Use git checkout origin/server_step4 to see the code.
In the variable configrarion you add the variables.
You define two default variables beneath it. These will be used when there is no value set.

       

      type configuration struct {
        // The name for the command that we will create.
        Commandname string
      
        // The username that will respond.
        Username string
      }
      
      const DefaultCommandName = "hello"
      const DefaultUsername = "there"
      

      func (c *configuration) getCommandName() string {
        if len(c.Commandname) > 0 {
          return c.Commandname
        }
        return DefaultCommandName
      }
      
      func (c *configuration) getUsername() string {
        if len(c.Commandname) > 0 {
          return c.Username
        }
        return DefaultUsername
      }
    
Now you just have to read the variables from the configuration. You can check your code by changing the defaultCommandName to "ciao". You will have a /ciao command now.
If the user didn't set his firstname and lastname, you can define what to answer.
       
        func (p *Plugin) registerCommands() error {
          commandName := p.getConfiguration().getCommandName()
        
          if err := p.API.RegisterCommand(&model.Command{
            Trigger:          commandName,
            AutoComplete:     true,
            AutoCompleteDesc: "responds world",
          }); err != nil {
            return errors.Wrapf(err, "failed to register  command")
          }
        
          return nil
        }
        
        func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
          trigger := strings.TrimPrefix(strings.Fields(args.Command)[0], "/")
          commandName := p.getConfiguration().getCommandName()
          username := ""
          UserId := args.UserId
          user, appErr := p.API.GetUser(UserId)
          if appErr == nil {
            username = user.GetFullName()
          }
          if username == "" {
            username = p.getConfiguration().getUsername()
        
          }
        
          switch trigger {
          case commandName:
            return &model.CommandResponse{
              ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL,
              Text:         fmt.Sprintf("Hello " + username),
            }, nil
        
          default:
            return &model.CommandResponse{
              ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL,
              Text:         fmt.Sprintf("Unknown command: " + args.Command),
            }, nil
          }
        }
        
      
ciao

9 Modify your configuration variable through the slash command

If you want to change a configuration, you can write a function to change your variable.
If you want to change the word that your plugin responds, you can add a variable to your slash command.
This variable will overwrite your the variable in your configuration and your variable will be lost when the plugin is restarted.
In the repository this is branch server_step5. Use 'git checkout origin/server_step5' to see the code.


func (c *configuration) setUsername(newUsername string) {
	c.Username = newUsername
}


newname := ""
if len(strings.Fields(args.Command)) > 1 && newname == "" {
  newname = strings.Fields(args.Command)[1]
  p.getConfiguration().setUsername(newname)
  }
confirmation built

10 Modify your configuration variable through the plugins settings in the system console

Another way to do this is using the settings that you created in the settings_schema section of your plugin.json file. You can find the settings page in the System Console. The variable will be stored and available even after restarting the plugin because it was defined in the plugin.json in the main folder of the plugin.
In the repository this is branch server_step6. Use 'git checkout origin/server_step6' to see the code.

First add the configuration.co the same variable name as you defined in plugin.conf in the settings_schema part.

type configuration struct {
	// The name for the command that we will create.
	Commandname string

	// The username that will respond.
	Username        string
	DefaultGreeting string
}	c.Usernametype configuration struct {
	// The name for the command that we will create.
	Commandname string

	// The username that will respond.
	Username        string
	DefaultGreeting string
}

In the commands.go file you can easily get the variable through p.getConfiguration().DefaultGreeting.

if appErr == nil {
username = p.getConfiguration().DefaultGreeting
}

In the commands.go file you can easily get the variable through p.getConfiguration().DefaultGreeting.
I recomment that you try to change the variable in the System Console and try the /hello command again.

systemconsole hello nerd

11 Success!

You successfully finished this part! You should be able now to start building your own serverside plugins.
You can find much more information at https://developers.mattermost.com/extend/plugins/server/.
Specially the reference gives you a lot of information.
Our next article will handle Webapp-plugins.