Skip to content

Commit

Permalink
Merge branch 'master' of github.com:mattermost-community/mattermost-p…
Browse files Browse the repository at this point in the history
…lugin-msteams-meetings into MM-676
  • Loading branch information
raghavaggarwal2308 committed Aug 26, 2024
2 parents c5cfd94 + 6c0a32b commit 90bda6d
Show file tree
Hide file tree
Showing 13 changed files with 114 additions and 121 deletions.
97 changes: 6 additions & 91 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
# Disclaimer

**This repository is community supported and not maintained by Mattermost. Mattermost disclaims liability for integrations, including Third Party Integrations and Mattermost Integrations. Integrations may be modified or discontinued at any time.**

# Mattermost MS Teams Meetings Plugin

[![Build Status](https://img.shields.io/circleci/project/github/mattermost/mattermost-plugin-msteams-meetings/master)](https://circleci.com/gh/mattermost/mattermost-plugin-msteams-meetings)
[![Code Coverage](https://img.shields.io/codecov/c/github/mattermost/mattermost-plugin-msteams-meetings/master)](https://codecov.io/gh/mattermost/mattermost-plugin-msteams-meetings)
[![Release](https://img.shields.io/github/v/release/mattermost/mattermost-plugin-msteams-meetings)](https://github.com/mattermost/mattermost-plugin-msteams-meetings/releases/latest)
[![HW](https://img.shields.io/github/issues/mattermost/mattermost-plugin-msteams-meetings/Up%20For%20Grabs?color=dark%20green&label=Help%20Wanted)](https://github.com/mattermost/mattermost-plugin-msteams-meetings/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22Up+For+Grabs%22+label%3A%22Help+Wanted%22)

**Maintainer:** [@mickmister](https://github.com/mickmister)

Start and join voice calls, video calls, and use screen sharing with your team members via MS Teams Meetings.
Start and join voice calls, video calls, and use screen sharing with your team members in Microsoft Teams Meetings.

## Admin guide

Expand All @@ -21,94 +15,15 @@ Mattermost Server v5.26+ is required.

### Installation

Download the latest [plugin binary release](https://github.com/mattermost/mattermost-plugin-msteams-meetings/releases) and upload it to your server via **System Console > Plugin Management**.

Once enabled, selecting the video icon in a Mattermost channel invites team members to join an MS Teams meeting, hosted using the credentials of the user who initiated the call.

### Configuration

#### Step 1: Create a Mattermost App in Azure

1. Sign in to [the Azure portal](https://portal.azure.com) using an admin Azure account.
2. Navigate to [App Registrations](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps).
3. Select **New registration** at the top of the page.

<img width="300" src="https://user-images.githubusercontent.com/6913320/76347903-be67f580-62dd-11ea-829e-236dd45865a8.png"/>

4. Fill out the form with the following values:

- Name: **Mattermost MS Teams Meetings Plugin**
- Supported account types: **Default value (Single tenant)**
- Redirect URI: **https://(MM_SITE_URL)/plugins/com.mattermost.msteamsmeetings/oauth2/complete**. Replace `(MM_SITE_URL)` with your Mattermost server's URL.

5. Select **Register** to submit the form.

<img width="500" src="https://user-images.githubusercontent.com/6913320/76348298-55cd4880-62de-11ea-8e0e-4ace3a8f8fcb.png"/>

6. Navigate to **Certificates & secrets** in the left pane.

<img width="300" src="https://user-images.githubusercontent.com/6913320/76348833-3d116280-62df-11ea-8b13-d39a0a2f2024.png"/>

7. Select **New client secret > Add**, then copy the new secret in the bottom right corner of the screen. We'll use this value later in the Mattermost System Console.

<img width="300" src="https://user-images.githubusercontent.com/6913320/76349025-9da09f80-62df-11ea-8c8f-0b39cad4597e.png"/>

8. Navigate to **API permissions** in the left pane.

<img width="300" src="https://user-images.githubusercontent.com/6913320/76349582-a9d92c80-62e0-11ea-9414-5efd12c09b3f.png"/>

9. Select **Add a permission** and choose **Microsoft Graph** in the right pane.

<img width="500" src="https://user-images.githubusercontent.com/6913320/76350226-c2961200-62e1-11ea-9080-19a9b75c2aee.png"/>
From Mattermost v10, this plugin is pre-packaged with the Mattermost Server.

10. Select **Delegated permissions**, and scroll down to select the `OnlineMeetings.ReadWrite` permissions.
If your Mattermost deployment is on a release prior to v10, download the latest [plugin binary release](https://github.com/mattermost/mattermost-plugin-msteams-meetings/releases), and upload it to your server via **System Console > Plugin Management**.

<img width="300" src="https://user-images.githubusercontent.com/6913320/76350551-5a93fb80-62e2-11ea-8eb3-812735691af9.png"/>

11. Select **Add permissions** to submit the form.

<img width="300" src="https://user-images.githubusercontent.com/6913320/80412303-abb07c80-889b-11ea-9640-7c2f264c790f.png"/>

12. Select **Grant admin consent for...** to grant the permissions for the application.

You're all set for configuration inside of the Azure portal.

#### Step 2: Configure plugin settings

1. Copy the **Client ID** and **Tenant ID** from the Azure portal.

<img width="300" src="https://user-images.githubusercontent.com/6913320/76779336-9109c480-6781-11ea-8cde-4b79e5b2f3cd.png"/>

2. Go to **System Console > Plugins > MS Teams Meetings**.
3. Enter the following values in the fields provided:

- `tenantID` - Copy from the Azure portal
- `clientID` - Copy from the Azure portal
- `Client Secret` - Copy from the Azure portal (generated in **Certificates & secrets** earlier in these instructions)

4. Choose **Save** to apply the configuration.

### Onboard users

When you’ve tested the plugin and confirmed it’s working, notify your team so they can get started. Copy and paste the text below, edit it to suit your requirements, and send it out.

> Hi team,
> The MS Teams Meetings plugin has been configured so you can use it for calls from within Mattermost. To get started, run the `/mstmeetings connect` slash command from any channel within Mattermost. Visit the documentation for more information.
## User guide

### Connect an MS Teams Account to Mattermost

Use the `/mstmeetings connect` slash command to connect an MS Teams account to Mattermost.

## Start a call

Start a call either by selecting the video icon in a Mattermost channel or by using the `/mstmeetings start` slash command. Every meeting you start creates a new meeting room in MS Teams. If you start two meetings less than 30 seconds apart you'll be prompted to confirm that you want to create the meeting.
Once enabled, selecting the video icon in a Mattermost channel invites team members to join an MS Teams meeting, hosted using the credentials of the user who initiated the call.

## Disconnect an MS Teams account from Mattermost
### Configuration, Setup, and Usage

Use the `/mstmeetings disconnect` slash command to disconnect an MS Teams account from Mattermost.
See the Mattermost Product Documentation for details on [setting up](https://docs.mattermost.com/integrate/microsoft-teams-meetings-interoperability.html#setup), [configuring](https://docs.mattermost.com/integrate/microsoft-teams-meetings-interoperability.html#enable-and-configure-the-microsoft-teams-meetings-integration-in-mattermost), and [using](https://docs.mattermost.com/integrate/microsoft-teams-meetings-interoperability.html#usage) the Mattermost for Microsoft Teams Meetings integration.

## Development

Expand Down
24 changes: 14 additions & 10 deletions server/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"fmt"
"net/url"

msgraph "github.com/yaegashi/msgraph.go/beta"
"golang.org/x/oauth2"
Expand All @@ -19,23 +20,26 @@ func (ae *authError) Error() string {
return string(errorString)
}

var oAuthMessage string = "[Click here to link your Microsoft account.](%s/plugins/" + manifest.Id + "/oauth2/connect?channelID=%s)"
func (p *Plugin) getOauthMessage(channelID string) (string, error) {
pluginOauthURL, err := p.getPluginOauthURL()
if err != nil {
return "", err
}

return fmt.Sprintf("[Click here to link your Microsoft account.](%s/connect?channelID=%s)", pluginOauthURL, url.QueryEscape(channelID)), nil
}

func (p *Plugin) authenticateAndFetchUser(userID, channelID string) (*msgraph.User, *authError) {
var user *msgraph.User
var err error

siteURL, err := p.getSiteURL()
oauthMsg, err := p.getOauthMessage(channelID)
if err != nil {
p.API.LogError("authenticateAndFetchUser, cannot get site URL", "error", err.Error())
return nil, &authError{Message: "Cannot get Site URL. Contact your sys admin.", Err: err}
p.API.LogError("authenticateAndFetchUser, cannot get oauth message", "error", err.Error())
return nil, &authError{Message: "Error getting oauth messsage.", Err: err}
}

userInfo, apiErr := p.GetUserInfo(userID)
oauthMsg := fmt.Sprintf(
oAuthMessage,
siteURL, channelID)

if apiErr != nil || userInfo == nil {
return nil, &authError{Message: oauthMsg, Err: apiErr}
}
Expand All @@ -58,12 +62,12 @@ func (p *Plugin) getOAuthConfig() (*oauth2.Config, error) {
clientSecret := config.OAuth2ClientSecret
clientAuthority := config.OAuth2Authority

siteURL, err := p.getSiteURL()
pluginOauthURL, err := p.getPluginOauthURL()
if err != nil {
return nil, err
}

redirectURL := fmt.Sprintf("%s/plugins/%s/oauth2/complete", siteURL, manifest.Id)
redirectURL := fmt.Sprintf("%s/complete", pluginOauthURL)

return &oauth2.Config{
ClientID: clientID,
Expand Down
50 changes: 50 additions & 0 deletions server/authorization_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"testing"

"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin/plugintest"
"github.com/stretchr/testify/require"
)

func TestGetOauthMessage(t *testing.T) {
for _, testCase := range []struct {
description string
siteURL string
setupFunc func(p *Plugin)
}{
{
description: "successful",
siteURL: "https://example-url.com",
setupFunc: func(p *Plugin) {
msg, err := p.getOauthMessage("mockChannelID")
require.NoError(t, err)
require.EqualValues(t, "[Click here to link your Microsoft account.](https://example-url.com/plugins/com.mattermost.msteamsmeetings/oauth2/connect?channelID=mockChannelID)", msg)
},
},
{
description: "missing site URL",
siteURL: "",
setupFunc: func(p *Plugin) {
msg, err := p.getOauthMessage("mockChannelID")
require.EqualError(t, err, "error fetching siteUrl")
require.EqualValues(t, "", msg)
},
},
} {
t.Run(testCase.description, func(t *testing.T) {
p := &Plugin{}
api := &plugintest.API{}
api.On("GetConfig").Return(&model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: model.NewString(testCase.siteURL),
},
})

p.SetAPI(api)

testCase.setupFunc(p)
})
}
}
7 changes: 4 additions & 3 deletions server/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,9 @@ func (p *Plugin) handleHelp() (string, error) {
}

func (p *Plugin) handleStart(args []string, extra *model.CommandArgs) (string, error) {
topic := ""
if len(args) > 1 {
return tooManyParametersText, nil
topic = strings.Join(args[1:], " ")
}
userID := extra.UserId
user, appErr := p.API.GetUser(userID)
Expand All @@ -125,7 +126,7 @@ func (p *Plugin) handleStart(args []string, extra *model.CommandArgs) (string, e
}

if recentMeeting {
p.postConfirmCreateOrJoin(recentMeetingURL, extra.ChannelId, "", userID, creatorName, provider)
p.postConfirmCreateOrJoin(recentMeetingURL, extra.ChannelId, topic, userID, creatorName, provider)
p.trackMeetingDuplication(extra.UserId)
return "", nil
}
Expand All @@ -140,7 +141,7 @@ func (p *Plugin) handleStart(args []string, extra *model.CommandArgs) (string, e
return authErr.Message, authErr.Err
}

_, _, err := p.postMeeting(user, extra.ChannelId, "")
_, _, err := p.postMeeting(user, extra.ChannelId, topic)
if err != nil {
return "Failed to post message. Please try again.", errors.Wrap(err, "cannot post message")
}
Expand Down
9 changes: 8 additions & 1 deletion server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func (p *Plugin) completeUserOAuth(w http.ResponseWriter, r *http.Request) {
conf, err := p.getOAuthConfig()
if err != nil {
http.Error(w, "error in oauth config", http.StatusInternalServerError)
return
}

code := r.URL.Query().Get("code")
Expand Down Expand Up @@ -261,12 +262,18 @@ func (p *Plugin) handleStartMeeting(w http.ResponseWriter, r *http.Request) {
if _, err = w.Write([]byte(`{"meeting_url": ""}`)); err != nil {
p.API.LogWarn("failed to write response", "error", err.Error())
}
p.postConnect(req.ChannelID, userID)

if _, err = p.postConnect(req.ChannelID, userID); err != nil {
p.API.LogWarn("failed to create connect post", "error", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

// the user state will be needed later while connecting the user to MS teams meeting via OAuth
if _, err = p.StoreState(userID, req.ChannelID, false); err != nil {
p.API.LogWarn("failed to store user state", "error", err.Error())
}

return
}

Expand Down
6 changes: 4 additions & 2 deletions server/meeting.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import (
msgraph "github.com/yaegashi/msgraph.go/beta"
)

func (c *Client) CreateMeeting(creator *UserInfo, attendeesIDs []*UserInfo) (*msgraph.OnlineMeeting, error) {
func (c *Client) CreateMeeting(creator *UserInfo, attendeesIDs []*UserInfo, subject string) (*msgraph.OnlineMeeting, error) {
ctx := context.Background()
start := time.Now()
end := start.Add(1 * time.Hour)
subject := "Mattermost Meeting"
attendees := []msgraph.MeetingParticipantInfo{}
if subject == "" {
subject = "MS Teams Meeting"
}
for _, attendee := range attendeesIDs {
attendees = append(attendees, msgraph.MeetingParticipantInfo{
Identity: &msgraph.IdentitySet{
Expand Down
14 changes: 8 additions & 6 deletions server/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (p *Plugin) postMeeting(creator *model.User, channelID string, topic string

client := p.NewClient(conf, userInfo.OAuthToken)

meeting, err := client.CreateMeeting(userInfo, attendees)
meeting, err := client.CreateMeeting(userInfo, attendees, topic)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -102,16 +102,18 @@ func (p *Plugin) postConfirmCreateOrJoin(meetingURL string, channelID string, to
return p.API.SendEphemeralPost(userID, post)
}

func (p *Plugin) postConnect(channelID string, userID string) *model.Post {
oauthMsg := fmt.Sprintf(
oAuthMessage,
*p.API.GetConfig().ServiceSettings.SiteURL, channelID)
func (p *Plugin) postConnect(channelID string, userID string) (*model.Post, error) {
oauthMsg, err := p.getOauthMessage(channelID)
if err != nil {
p.API.LogError("postConnect, cannot get oauth message", "error", err.Error())
return nil, err
}

post := &model.Post{
UserId: p.botUserID,
ChannelId: channelID,
Message: oauthMsg,
}

return p.API.SendEphemeralPost(userID, post)
return p.API.SendEphemeralPost(userID, post), nil
}
12 changes: 12 additions & 0 deletions server/utils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"fmt"
"net/url"
"time"

"github.com/mattermost/mattermost/server/public/model"
Expand Down Expand Up @@ -52,3 +54,13 @@ func getString(key string, props model.StringInterface) string {
}
return value
}

func (p *Plugin) getPluginOauthURL() (string, error) {
siteURL, err := p.getSiteURL()
if err != nil {
return "", err
}

pluginID := url.PathEscape(manifest.Id)
return fmt.Sprintf("%s/plugins/%s/oauth2", siteURL, pluginID), nil
}
4 changes: 2 additions & 2 deletions webapp/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import {GetStateFunc} from 'mattermost-redux/types/actions';

import Client from '../client';

export function startMeeting(channelId: string, force = false) {
export function startMeeting(channelId: string, force = false, topic: string) {
return async (dispatch: Dispatch, getState: GetStateFunc) => {
try {
const startFunction = force ? Client.forceStartMeeting : Client.startMeeting;
const meetingURL = await startFunction(channelId, true);
const meetingURL = await startFunction(channelId, true, topic);
if (meetingURL) {
window.open(meetingURL);
}
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ export default class Client {
this.url = url + '/plugins/' + id;
}

startMeeting = async (channelId: string, personal = true, topic = '', meetingId = 0, force = false) => {
startMeeting = async (channelId: string, personal = true, topic: string, meetingId = 0, force = false) => {
const res = await doPost(`${this.url}/api/v1/meetings${force ? '?force=true' : ''}`, {channel_id: channelId, personal, topic, meeting_id: meetingId});
return res.meeting_url;
}

forceStartMeeting = async (channelId: string, personal = true, topic = '', meetingId = 0) => {
forceStartMeeting = async (channelId: string, personal = true, topic: string, meetingId = 0) => {
const meetingUrl = await this.startMeeting(channelId, personal, topic, meetingId, true);
return meetingUrl;
}
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/components/post_type_mstmeetings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type OwnProps = {
}

type Actions = {
startMeeting: (channelID: string, force: boolean) => ActionResult;
startMeeting: (channelID: string, force: boolean, topic: string) => ActionResult;
}

function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
Expand Down
Loading

0 comments on commit 90bda6d

Please sign in to comment.