Mattermost Platform

Building an Integration? Here’s When to Use Apps instead of Plugins

In the last few months, we have been working on a new Apps framework for developers. The idea behind this framework is to make it easier for more developers to integrate external applications or their own applications into Mattermost. But wait a moment, don’t we have Plugins for that? Yes, but Apps provide some advantages to developers over Plugins.

Plugins and Apps: What’s the Difference?

Plugins act as code that runs on the same hardware as the Mattermost server and are “all-powerful.” They can edit the configuration file, access the database, post as any user in any channel— which sometimes can be too much for solving a simple task. Of course, a Plugin should never do more than what it was programmed for, but when you are a system admin and someone asks you to install a Plugin, you may think twice about how secure that Plugin is based on the inherent level of access available to them.

On the other hand, an App is just an HTTP service. Everything an App can do is delimited by a set of permissions instantiated upon App installation. A system administrator can review, at least in a broad view, what access the App has before installing it.

Another significant consideration in Plugin development is the “webapp portion” of the Plugins, written in React. The first thing to notice is that it is the “webapp portion” and not the “mobileapp portion.” This is a piece of code written in javascript that is loaded directly into the webapp but has no presence on the mobile app. That leads to the common slightly mistaken thought that plugins don’t work on mobile. They do (slash commands, interactive dialogs, interactive messages), but anything controlled at the “webapp portion” (post action menus, channel header buttons, custom post types…) does not show up on the mobile client.

The Data-Driven Approach of the Apps Framework

Apps take a data-driven approach. Instead of directly including code to be executed on the client, they send configuration data (called Bindings) to the client with information on what elements it should show and where they should be (which channel, or other UI location, etc…). Apps are not as customizable as Plugins, and they do not support custom interfaces. But they provide an easier way to cover the most common use cases and ensure that your App will always be coherent with the rest of the product (in terms of themes, style, etc).

But that’s not the only great thing about Apps! As I mentioned before, Plugins run alongside the server code. And that means Go. I enjoy programming in Go, so I have no problem with that. But many people would prefer other languages. Standard Plugins can only be written in Go. On the other hand, from the Mattermost instance perspective, Apps are just an HTTP service. So any language that can create an HTTP server could be used to create an App.

A Practical Mattermost App Example

Finally, I want to give you a practical example of why you would want to create Apps instead of Plugins. Many years ago, I learned Python. But life happened, and I have not played with it for a long time. So I decided to dust off my knowledge of Python and create a simple App for this article. Here is the full code of a simple “Ping” App on Mattermost written in Python. The objective of this app is to answer “Pong” whenever we do any action.

Initialize the App and Connect to Mattermost

For this example, I’m using a simple but powerful web framework called bottle. With this framework, we create an HTTP server that will serve all the necessary information of the App to the Mattermost instance. The server runs beautifully and simply with the last line of code:

run(host='localhost', port=3030, debug=True)

Any App needs to provide a manifest to the Mattermost server to be installed and available to end-users. The manifest is a JSON file with information such as the name, description, or permissions requested. The code below shows that my manifest route on my HTTP server will be “/manifest,” which will directly serve up the static “manifest.json” file stored in my project.

@route('/manifest')
def manifest():
   return static_file("manifest.json", "./")

Add UI Bindings

At this point, the App can be installed, but we still need to add Bindings for the App’s UI elements to show up in the user’s client. Bindings are pieces of data that bind a Location to an action. For example, I want to bind my “ping” action to a slash command. So we create the ping Binding:

@post('/bindings')
def bindings():   
   pingCommand = Binding()
   pingCommand.location = "ping"
   pingCommand.label = "ping"
   pingCommand.call = Call()
   pingCommand.call.path = "/command/ping"
   pingCommand.form = Form()

I decide the location value, which is an internal value. The important values are `label` (what will be shown to the user) and the `call path` (the endpoint reached when the command is executed). I want the Mattermost server to call my “/command/ping” endpoint whenever a user executes the `ping` slash command. We also add an empty Form to let the framework know that there is no need to fetch a Form for this command. We will talk about Forms in a future article.

Now, I want the “ping” command to be a subcommand of the base command (/blogapp). To do this, we first create the base command Binding, and then we add the ping command as a Binding on the base command.

   baseCommandBinding = Binding()
   baseCommandBinding.location = "blogapp"
   baseCommandBinding.label = "blogapp"
   baseCommandBinding.description = "Commands for Blog App"
   baseCommandBinding.bindings = []
   baseCommandBinding.bindings.append(pingCommand)

This code is practically the same as the previous piece of code. I set a custom location, I set the label, and now I want to add a description so it shows as a description of the command. Finally, I add the pingCommand I just created as a Binding.

With this method of adding sub-bindings, we can create any tree of commands and subcommands we want, and we will get command parsing and autocomplete (suggestions for the user to choose from) “for free.” We know that whenever “/command/ping” is called, it is because someone has executed the “/blogapp ping” slash command in Mattermost. If we were working with Plugins, we would have to create the whole Autocomplete object to get autocomplete. We would also have to parse the command in our code since we would only receive the raw command string submitted by the user with the ExecuteSlashCommand hook.

Finally, we have to tell the framework these are commands. We create a special Binding with location “/command.” These special bindings serve as the root for all Bindings under the same location. Every command we create will be under the “/command” Binding; every channel header button will be under the “/channel_header” Binding, and so on.

  commandBinding = Binding()
   commandBinding.location = "/command"
   commandBinding.bindings = []
   commandBinding.bindings.append(baseCommandBinding)

Now we just need to create the Call Response and send it back to the server.

  appBindings = []
   appBindings.append(commandBinding)
 
   callResponse = CallResponse()
   callResponse.type = "ok"
   callResponse.data = appBindings
   return Encoder().encode(callResponse)

With this done, the Mattermost server will relay this information to the client, which will then show the command we have defined.

That covers the most tedious part about Apps—creating the Bindings. Creating a command as a baseline is easier in Plugins since you simply have to register the base command. But then you have to parse the command and add the autocomplete. By following this data-driven approach (which you would have to do similarly in Plugins to get autocomplete), you get autocomplete and parsing for free in your App.

app vs plugin command
app vs plugin ping

Bringing It All Together

Finally, let’s look at the actual action.

@post('/command/ping/<type>')
def ping(type):
   callResponse = CallResponse()
   callResponse.type = "ok"
   callResponse.markdown = "PONG!"
   return Encoder().encode(callResponse)

Here, we are saying that the response is of type `OK` and that we want to write a markdown message in response to the command execution that says “PONG!”

So now we have a working App, in 50 lines of code (not including the manifest and data models). No compilation time (it is Python), no deployment time, or refresh of the user’s client. Whenever I make any code changes, I just have to stop the App server and run it again. The new Bindings will be fetched, and the new functionality will be executed. With Plugins, whenever I make a change in the code, I have to run `make deploy.` This compiles, uploads, and installs the Plugin on the server, which may take some time.

What would this look like as a Plugin?

If I decided to create a Plugin for this same functionality, I would fork the Plugin starter template, which already has a lot of code. The amount of extra code needed to add a feature such as this command isn’t very much, but a significant amount of that code is used for handling the commands instead of implementing the functionality.

func (p *Plugin) OnActivate() error {
   autocompleteData := model.NewAutocompleteData("blogapp", "", "Commands for Blog App")
   ping := model.NewAutocompleteData("ping", "", "")
   autocompleteData.AddCommand(ping)
   command := &model.Command{
       Trigger:          "blogapp",
       AutoComplete:     true,
       AutoCompleteDesc: "Commands for Blog App",
       AutocompleteData: autocompleteData,
   }
 
   err := p.API.RegisterCommand(command)
   if err != nil {
       return errors.Wrap(err, "failed to register command")
   }
 
   return nil
}
 
func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
   split := strings.Fields(args.Command)
   command := split[0]
   action := ""
 
   if command != "/blogapp" {
       return &model.CommandResponse{Text: fmt.Sprintf("Command '%s' is not /blogapp. Please try again.", command)}, nil
   }
 
   if len(split) > 1 {
       action = split[1]
   } else {
       return &model.CommandResponse{Text: "Please specify an action for /blogapp command."}, nil
   }
 
   switch action {
   case "ping":
       return p.runPing(), nil
   default:
       return &model.CommandResponse{Text: fmt.Sprintf("Unknown action %v", action)}, nil
   }
}
 
func (p *Plugin) runPing() *model.CommandResponse {
   return &model.CommandResponse{Text: "PONG"}
}

Modify the UI Via Apps

Before we finish, let’s add a channel header button that does the same thing as the slash command, but is easier for users to discover in the UI. Here is where Apps shine! 

Since it does the same thing, let’s reuse the `ping` command Binding. Nevertheless, since channel header buttons require an icon, let’s add it to the Binding. By doing so, the icon is displayed next to the autocomplete command for the user to recognize.

   pingCommand = Binding()
   pingCommand.location = "ping"
   pingCommand.label = "ping"
   pingCommand.call = Call()
   pingCommand.call.path = "/command/ping"
   pingCommand.form = Form()
   pingCommand.icon = "icon.png"

We also need to serve the icon, which is simple with bottle.

@route('/static/icon.png')
def icon():
   return static_file("icon.png", "./")

We need to create the Binding to show the icon in the channel header location for both the Desktop and Mobile clients:

   channelHeaderBinding = Binding()
   channelHeaderBinding.location = "/channel_header"
   channelHeaderBinding.bindings = []
   channelHeaderBinding.bindings.append(pingCommand)

And add it to the response:

   appBindings = []
   appBindings.append(commandBinding)
   appBindings.append(channelHeaderBinding)
 
   callResponse = CallResponse()
   callResponse.type = "ok"
   callResponse.data = appBindings
   return Encoder().encode(callResponse)

And that is it. We have a channel header button, working both on web and mobile clients.

If we were to have this same functionality using a Plugin, we would have to go to the webapp part of the code, register the channel header with the icon as a React component, along with the action, all with our Plugin’s own custom JavaScript code to run in the browser. In this case, the action was fairly simple, and we could have used the “executeCommand” action. However, if we were to do anything different, we would have to implement the client and server sides of the function, juggling between Go and Javascript. And after all that, the channel header button would only show in the webapp and not on mobile.

Build Your First Mattermost App Today

I hope this example helps to provide a better understanding of the power of Apps. In future articles, I will try to dig deeper into how to do more complex things with Apps, and how those are simpler to develop and maintain than Plugins.

Daniel Espino García is a Full Stack Software Design Engineer at the Messaging team at Mattermost, Inc. He joined the team in March 2020. His main focus are Integrations and Apps.