42wim/matterbridge

View on GitHub
bridge/msteams/msteams.go

Summary

Maintainability
A
0 mins
Test Coverage
package bmsteams

import (
    "context"
    "fmt"
    "os"
    "regexp"
    "strings"
    "time"

    "github.com/42wim/matterbridge/bridge"
    "github.com/42wim/matterbridge/bridge/config"
    "github.com/davecgh/go-spew/spew"

    "github.com/mattn/godown"
    msgraph "github.com/yaegashi/msgraph.go/beta"
    "github.com/yaegashi/msgraph.go/msauth"

    "golang.org/x/oauth2"
)

var (
    defaultScopes = []string{"openid", "profile", "offline_access", "Group.Read.All", "Group.ReadWrite.All"}
    attachRE      = regexp.MustCompile(`<attachment id=.*?attachment>`)
)

type Bmsteams struct {
    gc    *msgraph.GraphServiceRequestBuilder
    ctx   context.Context
    botID string
    *bridge.Config
}

func New(cfg *bridge.Config) bridge.Bridger {
    return &Bmsteams{Config: cfg}
}

func (b *Bmsteams) Connect() error {
    tokenCachePath := b.GetString("sessionFile")
    if tokenCachePath == "" {
        tokenCachePath = "msteams_session.json"
    }
    ctx := context.Background()
    m := msauth.NewManager()
    m.LoadFile(tokenCachePath) //nolint:errcheck
    ts, err := m.DeviceAuthorizationGrant(ctx, b.GetString("TenantID"), b.GetString("ClientID"), defaultScopes, nil)
    if err != nil {
        return err
    }
    err = m.SaveFile(tokenCachePath)
    if err != nil {
        b.Log.Errorf("Couldn't save sessionfile in %s: %s", tokenCachePath, err)
    }
    // make file readable only for matterbridge user
    err = os.Chmod(tokenCachePath, 0o600)
    if err != nil {
        b.Log.Errorf("Couldn't change permissions for %s: %s", tokenCachePath, err)
    }
    httpClient := oauth2.NewClient(ctx, ts)
    graphClient := msgraph.NewClient(httpClient)
    b.gc = graphClient
    b.ctx = ctx

    err = b.setBotID()
    if err != nil {
        return err
    }
    b.Log.Info("Connection succeeded")
    return nil
}

func (b *Bmsteams) Disconnect() error {
    return nil
}

func (b *Bmsteams) JoinChannel(channel config.ChannelInfo) error {
    go func(name string) {
        for {
            err := b.poll(name)
            if err != nil {
                b.Log.Errorf("polling failed for %s: %s. retrying in 5 seconds", name, err)
            }
            time.Sleep(time.Second * 5)
        }
    }(channel.Name)
    return nil
}

func (b *Bmsteams) Send(msg config.Message) (string, error) {
    b.Log.Debugf("=> Receiving %#v", msg)
    if msg.ParentValid() {
        return b.sendReply(msg)
    }

    // Handle prefix hint for unthreaded messages.
    if msg.ParentNotFound() {
        msg.ParentID = ""
        msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
    }

    ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().Request()
    text := msg.Username + msg.Text
    content := &msgraph.ItemBody{Content: &text}
    rmsg := &msgraph.ChatMessage{Body: content}
    res, err := ct.Add(b.ctx, rmsg)
    if err != nil {
        return "", err
    }
    return *res.ID, nil
}

func (b *Bmsteams) sendReply(msg config.Message) (string, error) {
    ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().ID(msg.ParentID).Replies().Request()
    // Handle prefix hint for unthreaded messages.

    text := msg.Username + msg.Text
    content := &msgraph.ItemBody{Content: &text}
    rmsg := &msgraph.ChatMessage{Body: content}
    res, err := ct.Add(b.ctx, rmsg)
    if err != nil {
        return "", err
    }
    return *res.ID, nil
}

func (b *Bmsteams) getMessages(channel string) ([]msgraph.ChatMessage, error) {
    ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(channel).Messages().Request()
    rct, err := ct.Get(b.ctx)
    if err != nil {
        return nil, err
    }
    b.Log.Debugf("got %#v messages", len(rct))
    return rct, nil
}

//nolint:gocognit
func (b *Bmsteams) poll(channelName string) error {
    msgmap := make(map[string]time.Time)
    b.Log.Debug("getting initial messages")
    res, err := b.getMessages(channelName)
    if err != nil {
        return err
    }
    for _, msg := range res {
        msgmap[*msg.ID] = *msg.CreatedDateTime
        if msg.LastModifiedDateTime != nil {
            msgmap[*msg.ID] = *msg.LastModifiedDateTime
        }
    }
    time.Sleep(time.Second * 5)
    b.Log.Debug("polling for messages")
    for {
        res, err := b.getMessages(channelName)
        if err != nil {
            return err
        }
        for i := len(res) - 1; i >= 0; i-- {
            msg := res[i]
            if mtime, ok := msgmap[*msg.ID]; ok {
                if mtime == *msg.CreatedDateTime && msg.LastModifiedDateTime == nil {
                    continue
                }
                if msg.LastModifiedDateTime != nil && mtime == *msg.LastModifiedDateTime {
                    continue
                }
            }

            if b.GetBool("debug") {
                b.Log.Debug("Msg dump: ", spew.Sdump(msg))
            }

            // skip non-user message for now.
            if msg.From == nil || msg.From.User == nil {
                continue
            }

            if *msg.From.User.ID == b.botID {
                b.Log.Debug("skipping own message")
                msgmap[*msg.ID] = *msg.CreatedDateTime
                continue
            }

            msgmap[*msg.ID] = *msg.CreatedDateTime
            if msg.LastModifiedDateTime != nil {
                msgmap[*msg.ID] = *msg.LastModifiedDateTime
            }
            b.Log.Debugf("<= Sending message from %s on %s to gateway", *msg.From.User.DisplayName, b.Account)
            text := b.convertToMD(*msg.Body.Content)
            rmsg := config.Message{
                Username: *msg.From.User.DisplayName,
                Text:     text,
                Channel:  channelName,
                Account:  b.Account,
                Avatar:   "",
                UserID:   *msg.From.User.ID,
                ID:       *msg.ID,
                Extra:    make(map[string][]interface{}),
            }

            b.handleAttachments(&rmsg, msg)
            b.Log.Debugf("<= Message is %#v", rmsg)
            b.Remote <- rmsg
        }
        time.Sleep(time.Second * 5)
    }
}

func (b *Bmsteams) setBotID() error {
    req := b.gc.Me().Request()
    r, err := req.Get(b.ctx)
    if err != nil {
        return err
    }
    b.botID = *r.ID
    return nil
}

func (b *Bmsteams) convertToMD(text string) string {
    if !strings.Contains(text, "<div>") {
        return text
    }
    var sb strings.Builder
    err := godown.Convert(&sb, strings.NewReader(text), nil)
    if err != nil {
        b.Log.Errorf("Couldn't convert message to markdown %s", text)
        return text
    }
    return sb.String()
}