Mattermost Apps Framework - Part 4

Building an Immersive App Experience with the Mattermost Apps Framework

(Note: This is the last installment of a four-part series on the Mattermost Apps Framework. You might want to read part one, part two, and part three if you haven’t already.)

Key Takeaways: Why Develop the Mattermost Apps Framework?

‣ Webhooks allow a good experience for incoming data/payloads, which is why they are used often.

‣ The Mattermost Apps framework enables product owners and developers to take advantage of the inverted paradigm.

‣ They allow triggers to originate within Mattermost and make use of external API calls to complete actions on applications that reside outside the context of Mattermost.

‣ This has a generic term in the market: ChatOps.

Creating a Workplace Wellness program

In this post, we’ll examine a way to create a Workplace Wellness program that can be used within Mattermost. This program consists of four distinct apps which are meant to help companies that are embracing a remote work culture:

  1. Sports
  2. Holidays
  3. Nutrition
  4. Translation

Internally, this is a collection of different APIs that get called when distinct slash commands are issued. Each command invokes a different route, which will result in the corresponding REST-based endpoints being invoked. In turn, responses will be parsed by the business logic in these applications that serve the routes invoked from within Mattermost. An attempt has been made to make use of example applications written in different languages to demonstrate the compatibility of the Mattermost Apps Framework.

Now, let’s build each application and the different parts they need to function. Every application needs a manifest, an authentication mechanism, and the business logic. We will also limit the code examples to contain relevant contextual snippets only. For a more code-complete version of this tutorial and understand the interplay of the various parts in-depth, read Part 2 and Part 3 of this tutorial.

Capturing live sports scores

This app will extract and provide live scores from various sports. In this case, we will be using Rapid API as an aggregator to consume the source Livescore API. The manifest file would look as follows:

{
    	"app_id": "sports-app",
    	"display_name": "Live Scores",
    	"app_type": "http",
    	"root_url": "",
    	"requested_permissions": [
     	"act_as_bot"
 	],
    	"requested_locations": [
     	"/command"
 	]
}

Replace the values for serverURL and port with the corresponding endpoint of the Mattermost installation. Authentication for this example will be done using an .env file and an API key. 

# .env file
url=”livescore6.p.rapidapi.com”
apikey=”f93636e5c99p8a321913ae3ajsn1e5ff0mshfd7c56b448f9c”

The following code stub will call the REST API corresponding to the one that returns live scores. It will then resolve the response and extract the relevant portion of it and then write it back to the user interacting with the Mattermost instance.

This example is in JavaScript and makes use of basic fetch functions.

const sport = call.values.message;

info = fetch("https://livescore6.p.rapidapi.com/matches/v2/list-live?Category="+sport, {
	"method": "GET",
	"headers": {
    	"x-rapidapi-host": process.env.url,
    	"x-rapidapi-key": process.env.apikey
	}
})
.then(response => {
	return response.json()
})
.catch(err => {
	console.error(err);
});

message = "Games currently live are "+info.Ccg.Csnm;

const users = [
    	call.context.bot_user_id,
    	call.context.acting_user_id,
	];

	const options = {
    	method: 'POST',
    	headers: {
        	Authorization: 'BEARER ' + call.context.bot_access_token,
        	'Content-Type': 'application/json',
    	},
    	body: JSON.stringify(users),
	};

	const mattermostSiteURL = call.context.mattermost_site_url;

	const channel = await fetch(mattermostSiteURL + '/api/v4/channels/direct', options).
    	then((res) => res.json())

	const post = {
    	channel_id: channel.id,
    	message,
	};

	options.body = JSON.stringify(post);

	fetch(mattermostSiteURL + '/api/v4/posts', options);

Observing holidays around the world

Next, we will learn to work with multicultural teams. In order to facilitate an open workplace atmosphere that promotes oneness and awareness, it is important to take into account cultural diversity. One of the areas where this gets affected the most is with holidays in different countries. This app built using the Mattermost Apps Framework will introduce a means to query for holidays observed in different countries. It will require a parameter that identifies the country for which holidays are required, e.g., US (United States of America), UK (United Kingdom), NG (Nigeria), and CN (China).

For this example, we will examine a code stub written in PHP that does two functions. First, it sends a POST request to the Festivo API. Next, it then parses the results of the API and passes that along to the second part of the function which posts it to the Mattermost instance. For simplicity’s sake, sections about the setup of the app have been omitted.

This app uses an open API and therefore does not require an authentication step.

$ch = curl_init();
$year = date("Y");
$data = http_build_query(array(
  "country" => "US",
  "year" => $year
));
$getUrl = "https://api.getfestivo.com/v2/holidays?" . $data;
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_URL, $getUrl);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);

$response = curl_exec($ch);

if(curl_error($ch)){
  echo 'Request Error:' . curl_error($ch);
} else {
  echo $response;
}

$users = [
	call.context.bot_user_id,
	call.context.acting_user_id,
];

$url = call.context.mattermost_site_url;
$data = array('field1' => 'value', 'field2' => 'value');
$options = array(
    	'http' => array(
    	'header'  => 'Authorization: \'BEARER \'' + call.context.bot_access_token +"Content-type: application\/json',
    	'method'  => 'POST',
    	'content' => http_build_query($data),
	)
);

$context  = stream_context_create($options);
$result = file_get_contents($url, false, $context);
var_dump($result);

curl_close($ch);

Helping employees eat healthy meals

The second demo application will make use of a REST endpoint which will provide meal plans for the day. It is aimed at providing ideas for healthy and balanced diets so that employees find a happy and healthy workplace.

This example will contain an app stub written in Golang. It also demonstrates how to make use of Auth0 in order to enable authentication for the API.

You can set up authentication using Auth0 by creating a file named login.go inside a root level folder such as web/app/login, and finally adding a function named Handler to service the login route. Learn more about how to in Part 3: Authentication Methods for the Mattermost Apps Framework. Here are the relevant stubs.

The .env file will contain the following environment variables needed to function with Auth0.

AUTH0_DOMAIN='https://api.spoonacular.com/'
AUTH0_CLIENT_ID=''
AUTH0_CLIENT_SECRET=''
AUTH0_CALLBACK_URL=''

Inside main.go, configure the route to the handler. The login route can be invoked when initializing the app during the installation process within Mattermost.

//main.go
router.GET("/login", login.Handler(auth))

This method will redirect to the login.go file which will contain the handler function for authenticating against the Auth0 authenticator and returning the appropriate API keys needed by the application.

//login.go
//  The basic logic for the Handler function
func Handler(auth *authenticator.Authenticator) gin.HandlerFunc {
	return func(ctx *gin.Context) {
    	state, err := generateRandomState()
    	if err != nil {
        	ctx.String(http.StatusInternalServerError, err.Error())
        	return
    	}

    	// Save the state inside the session.
    	session := sessions.Default(ctx)
    	session.Set("state", state)
    	if err := session.Save(); err != nil {
        	ctx.String(http.StatusInternalServerError, err.Error())
        	return
    	}

    	ctx.Redirect(http.StatusTemporaryRedirect, auth.AuthCodeURL(state))
	}
}

Once login has been completed successfully, the call-back URL is called and further business logic can be handled from that route.

//Inside the callback function
client := &http.Client{}
 req, err := http.NewRequest("GET", "https://api.spoonacular.com/mealplanner/generate?diet="+message, nil)
 
 var responseObject Response
 json.Unmarshal(bodyBytes, &responseObject)
 for i := range responseObject {
   fmt.Printf("API Response as struct %+v\n", responseObject[i].title+"/n"+responseObject[i].sourceUrl)
}

Translating foreign languages automatically

A cliched and frequently used phrase among international and diverse teams is “lost in translation.” 

Expressions and colloquialisms differ greatly between different languages. Making a handy translation app available at, quite literally, the fingertips of employees built using the Mattermost Apps framework will make it easy to look up basic translations to phrases. It’s also quite endearing to receive messages from coworkers at times in their native languages. 

This is an example of a translation app built using the Google Translate API that uses OAuth to authenticate with the Google endpoints. It is written in JavaScript. 

The bindings required to enable this translation will require two parameters: one for choosing the target language to be translated into and the second to reference the source string that needs to be translated.

app.post('/bindings', (req, res) => {
	res.json({
    	type: 'ok',
    	data:
        	{
            	location: '/command',
            	bindings: [
                	{
                    	"location": "configure",
                    	"label": "configure",
                    	"submit": {
                            	"path": "/configure"
                        	}
                	},
                	{
                    	label: 'lang',
                    	description: 'Translation app',
                    	hint: '[Which Language?]',
                    	bindings: [
                        	{
                            	location: 'lang',
                            	label: 'lang',
                            	submit: {
                                	path: '/translate',
                            	},
                        	},
                    	],
                	},
                	{
                    	label: 'sourceString',
                    	description: 'Translation app',
                    	hint: '[Enter sentence to be translated]',
                    	bindings: [
                        	{
                            	location: 'sourceString',
                            	label: 'String value',
                            	submit: {
                                	path: '/translate',
                            	},
                        	},
                    	],
                	}
            	],
        	},
	});
});

The following code stub will perform the translation using the Google Translate API. It will then post the translation as a direct message to the user within their Mattermost instance.


const text = call.values.sourceString;
const target = call.values.lang;


let [translations] = await translate.translate(text, target);

const users = [
	call.context.bot_user_id,
    	call.context.acting_user_id,
];

const postPayload = {
    	method: 'POST',
    	headers: {
        	Authorization: 'BEARER ' + call.context.bot_access_token,
        	'Content-Type': 'application/json',
    	},
    	body: JSON.stringify(users),
};

const mattermostSiteURL = call.context.mattermost_site_url;

const channel = await fetch(mattermostSiteURL + '/api/v4/channels/direct', postPayload).
   	then((res) => res.json())

	const post = {
    	channel_id: channel.id,
    	translations,
};

	postPayload.body = JSON.stringify(post);
	fetch(mattermostSiteURL + '/api/v4/posts', postPayload);
}

This example uses an authorization with the OAuth framework. It will exchange the information with Google servers and keep the user authenticated. The manifest will require two permissions to be specified declaratively: (1) to act as an admin and (2) a remote OAuth2 permission.

{
	"app_id": "translate",
	"version":"full",
	"display_name": "Translate!",
	"app_type": "http",
	"root_url": "",
	"homepage_url": "",
	"requested_permissions": [
    	    "act_as_admin",
    	    "act_as_user",
    	    "remote_oauth2"
	 ],
	"requested_locations": [
    	    "/command"
	]
}

Using the OAuth2 protocol requires additional configuration to set up the permissions for the required Google APIs. You can check the relevant section in the previous post of this series about authentication methods when using the Mattermost Apps framework. 

Summary 

This post is meant to serve as a primer of all the different ways in which an app can be developed and integrated using the Mattermost Apps framework. The following table illustrates the index of all the topics that are covered in this tutorial.

App ExampleAPI endpointLanguage/frameworkAuth type
Sports scoreslivescore/rapidapiJavaScript.env-based API keys
International holidaysgetfestivoPHPNo Auth/open API
Meal plansspoonacularGoAuth0
TranslationGoogle TranslateJavaScriptOAuth2

Ideally, we would love to see an integration you build made available via the Mattermost Marketplace. This will enable all Mattermost users to make use of your application. 

During the process of building your integrations, if you would like any help or support, please join the Mattermost Community server — specifically the channel for Apps. There, you can be assured of the fastest response to any queries you may have about the apps you’re building.

In the meantime, here are links to the three previous parts of this series in case you haven’t had a chance to read them all yet:

mm

Ram Iyengar is an engineer by practice and an educator at heart. He enjoys helping engineering teams around the world discover new and creative ways to work.