Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MM-19893] Outlook user status sync intial implementation #18

Merged
merged 19 commits into from
Jan 20, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified assets/profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ require (
github.com/jarcoal/httpmock v1.0.4
github.com/mattermost/mattermost-server/v5 v5.18.0-rc.test
github.com/pkg/errors v0.8.1
github.com/robfig/cron/v3 v3.0.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.4.0
github.com/yaegashi/msgraph.go v0.0.0-20191104022859-3f9096c750b2
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
Expand Down Expand Up @@ -343,6 +346,7 @@ github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmq
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
Expand All @@ -369,6 +373,7 @@ github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEAB
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
github.com/yaegashi/msgraph.go v0.0.0-20191104022859-3f9096c750b2 h1:37LbK2gAU+1oaWKC5NTz+fNOsR2LgdRj/SAFVMucgss=
github.com/yaegashi/msgraph.go v0.0.0-20191104022859-3f9096c750b2/go.mod h1:tso14hwzqX4VbnWTNsxiL0DvMb2OwbGISFA7jDibdWc=
github.com/yaegashi/msgraph.go v0.0.0-20191206184644-860e82e7ce3b h1:cDzhOBSEXM4yhv5oBG13+PxlVhIzwtcS5affFh7VNJk=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
Expand Down
6 changes: 3 additions & 3 deletions plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "com.mattermost.msoffice",
"name": "TODO:name",
"description": "TODO: description.",
"name": "Microsoft Calendar",
levb marked this conversation as resolved.
Show resolved Hide resolved
"description": "Microsoft Calendar",
"version": "0.1.0",
"min_server_version": "5.16.0",
"server": {
Expand Down Expand Up @@ -82,4 +82,4 @@
}
]
}
}
}
9 changes: 9 additions & 0 deletions server/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"context"
"time"

"github.com/mattermost/mattermost-server/v5/plugin"

"github.com/mattermost/mattermost-plugin-msoffice/server/config"
"github.com/mattermost/mattermost-plugin-msoffice/server/remote"
"github.com/mattermost/mattermost-plugin-msoffice/server/store"
Expand All @@ -20,6 +22,8 @@ type OAuth2 interface {

type Subscriptions interface {
CreateUserEventSubscription() (*store.Subscription, error)
GetUserAvailability() (string, error)
GetAllUsersAvailability() (string, error)
RenewUserEventSubscription() (*store.Subscription, error)
DeleteOrphanedSubscription(ID string) error
DeleteUserEventSubscription() error
Expand Down Expand Up @@ -65,6 +69,7 @@ type Dependencies struct {
Poster bot.Poster
Remote remote.Remote
IsAuthorizedAdmin func(userId string) (bool, error)
API plugin.API
levb marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably solve this now. Also maybe change to PluginAPI because it doesn't read well with the whole package being called api.

}

type Config struct {
Expand Down Expand Up @@ -96,6 +101,10 @@ func (api *api) MakeClient() (remote.Client, error) {
return api.Remote.NewClient(context.Background(), api.user.OAuth2Token), nil
}

func (api *api) MakeAppClient() (remote.Client, error) {
mickmister marked this conversation as resolved.
Show resolved Hide resolved
return api.Remote.NewAppLevelClient(context.Background()), nil
}

func (api *api) Filter(filters ...filterf) error {
for _, filter := range filters {
err := filter(api)
Expand Down
167 changes: 167 additions & 0 deletions server/api/availability.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved.
// See License for license information.

package api

import (
"fmt"
"time"

"github.com/robfig/cron/v3"

"github.com/mattermost/mattermost-plugin-msoffice/server/job"
"github.com/mattermost/mattermost-plugin-msoffice/server/remote"
"github.com/mattermost/mattermost-plugin-msoffice/server/utils"
"github.com/mattermost/mattermost-plugin-msoffice/server/utils/bot"
)

const (
AVAILABILITY_VIEW_FREE = '0'
mickmister marked this conversation as resolved.
Show resolved Hide resolved
AVAILABILITY_VIEW_TENTATIVE = '1'
AVAILABILITY_VIEW_BUSY = '2'
AVAILABILITY_VIEW_OUT_OF_OFFICE = '3'
AVAILABILITY_VIEW_WORKING_ELSEWHERE = '4'
)

type availabilityJob struct {
api API
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to avoid this pattern in notification.go - the thinking was that api is for an "acting user", has (at least) the current user's MattermostUserID etc. associated with it.

As I was working on solar lottery, i realized that this totally makes sense, because the api should just be operating in the context of the bot user. In fact, I am thinking that maybe the Bot User is the right concept to use for the "application-level" client. Can talk offline about it, no immediate change request here.

}

func NewAvailabilityJob(api API) job.RecurringJob {
return &availabilityJob{api: api}
}

func (j *availabilityJob) Run() {
c := cron.New()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need (a separate ticket) for HA-proofing this by using a distributed lock to run the cron job on one server only. (failover??? does the lock expire?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://mattermost.atlassian.net/browse/MM-21649

Removed cron dependency in favor of using a ticker select loop, so we have more control.

c.AddFunc("* * * * *", j.Work)
c.Start()
}

func (j *availabilityJob) getLogger() bot.Logger {
return j.api.(*api).Logger
levb marked this conversation as resolved.
Show resolved Hide resolved
}

func (j *availabilityJob) Work() {
log := j.getLogger()
log.Debugf("Availability job beginning")

_, err := j.api.GetUserAvailability()
if err != nil {
log.Errorf("Error during Availability job", "error", err.Error())
}

log.Debugf("Availability job finished")
}

func (api *api) GetUserAvailability() (string, error) {
levb marked this conversation as resolved.
Show resolved Hide resolved
client, err := api.MakeClient()
if err != nil {
return "", err
}

u, err := api.UserStore.LoadUser(api.mattermostUserID)
if err != nil {
return "", err
}

scheduleIDs := []string{u.Remote.Mail}

start, end, timeWindow := getTimeInfoForAvailability()

sched, err := client.GetSchedule(u.Remote.ID, scheduleIDs, start, end, timeWindow)
if err != nil {
return "", err
}

if len(sched) == 0 {
return "No schedule info found", nil
}

s := sched[0]
av := s.AvailabilityView[0]
return api.setUserStatusFromAvailability(api.mattermostUserID, av), nil
}

func (api *api) GetAllUsersAvailability() (string, error) {
mickmister marked this conversation as resolved.
Show resolved Hide resolved
client, err := api.MakeAppClient()
levb marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return "", err
}

users, err := api.UserStore.LoadAllUsers()
if err != nil {
return "", err
}

if len(users) == 0 {
return "No connected users found", nil
}

scheduleIDs := []string{}
for _, u := range users {
scheduleIDs = append(scheduleIDs, u.Email)
mickmister marked this conversation as resolved.
Show resolved Hide resolved
}

start, end, timeWindow := getTimeInfoForAvailability()

sched, err := client.GetSchedule(users[0].RemoteID, scheduleIDs, start, end, timeWindow)
if err != nil {
return "", err
}

mickmister marked this conversation as resolved.
Show resolved Hide resolved
if len(sched) == 0 {
return "No schedule info found", nil
}

var res string
for i, s := range sched {
userID := users[i].MattermostUserID
av := s.AvailabilityView[0]
res = api.setUserStatusFromAvailability(userID, av)
}

if res != "" {
return res, nil
}

return utils.JSONBlock(sched), nil
}

func getTimeInfoForAvailability() (start, end *remote.DateTime, timeWindow int) {
start = remote.NewDateTime(time.Now())
end = remote.NewDateTime(time.Now().Add(15 * time.Minute))
mickmister marked this conversation as resolved.
Show resolved Hide resolved
timeWindow = 15 // minutes
return start, end, timeWindow
}

func (api *api) setUserStatusFromAvailability(mattermostUserID string, av byte) string {
currentStatus, _ := api.API.GetUserStatus(mattermostUserID)

switch av {
case AVAILABILITY_VIEW_FREE:
if currentStatus.Status == "dnd" {
api.API.UpdateUserStatus(mattermostUserID, "online")
return fmt.Sprintf("User is free. Setting user from %s to online.", currentStatus.Status)
} else {
return fmt.Sprintf("User is free, and is already set to %s.", currentStatus.Status)
}
case AVAILABILITY_VIEW_TENTATIVE, AVAILABILITY_VIEW_BUSY:
if currentStatus.Status != "dnd" {
api.API.UpdateUserStatus(mattermostUserID, "dnd")
return fmt.Sprintf("User is busy. Setting user from %s to dnd.", currentStatus.Status)
} else {
return fmt.Sprintf("User is busy, and is already set to %s.", currentStatus.Status)
}
case AVAILABILITY_VIEW_OUT_OF_OFFICE:
if currentStatus.Status != "offline" {
api.API.UpdateUserStatus(mattermostUserID, "offline")
return fmt.Sprintf("User is out of office. Setting user from %s to offline", currentStatus.Status)
} else {
return fmt.Sprintf("User is out of office, and is already set to %s.", currentStatus.Status)
}
case AVAILABILITY_VIEW_WORKING_ELSEWHERE:
return fmt.Sprintf("User is working elsewhere. Pending implementation.")
}

return fmt.Sprintf("Availability view doesn't match %d", av)
}
74 changes: 74 additions & 0 deletions server/api/mock_api/mock_calendar.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions server/api/mock_api/mock_subscriptions.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions server/config/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ package config

const (
BotUserName = "msoffice"
BotDisplayName = "Microsoft Office TODO"
BotDescription = "Created by the Microsoft Office Plugin. TODO"
BotDisplayName = "Microsoft Calendar"
BotDescription = "Created by the Microsoft Calendar Plugin."

ApplicationName = "Microsoft Office"
ApplicationName = "Microsoft Calendar"
Repository = "mattermost-plugin-msoffice"
CommandTrigger = "msoffice"

Expand Down
5 changes: 5 additions & 0 deletions server/job/recurring_job.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package job

type RecurringJob interface {
Run()
mickmister marked this conversation as resolved.
Show resolved Hide resolved
}
Loading