Skip to content

Commit

Permalink
add slack notifications (#31)
Browse files Browse the repository at this point in the history
Co-authored-by: Andrew Wilkins <axwalk@gmail.com>
  • Loading branch information
graphaelli and axw authored Feb 5, 2021
1 parent a7d5b0f commit 3c41f14
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 16 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ zat
zat.yml
google.config.json
google.creds.json
slack.config.json
zoom.config.json
zoom.creds.json
.idea/
.vscode/
.vscode/
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ Once tokens have been obtained, `zat -no-server` will perform only archival duti
"oauth_redirect": "http://127.0.0.1.ip.es.io:8080/oauth/zoom"
}
```
* [Optional] Obtain Slack credentials
* [Create an App](https://api.slack.com/apps?new_app=1)
* Add Permissions > Scopes > Bot Token Scopes > Add An Oauth Scope granting: `channels:read`, `chat:write`, `chat:write.public`
* Save the Bot User OAuth Access Token to `slack.config.json` with content:
```json
{
"token": "your-token"
}
```

Once the credentials are in place, re-run `zat` and use the web server at http://localhost:8080/ to login to both Google and Zoom to create the `*.creds.json` files zat will use for the next run.

Expand Down Expand Up @@ -97,7 +106,7 @@ The zoom configuration is the meeting ID - the dashes are optional.

```
$ go build ./cmd/zoom/listrecordings
$ $ ./listrecordings -since 96h
$ ./listrecordings -since 96h
2019/11/25 12:22:54 listrecordings.go:41: 2 recordings found
2019-11-21 945106202 UI Weekly
audio_transcript https://zoom.us/recording/download/wwww
Expand All @@ -115,6 +124,21 @@ $ $ ./listrecordings -since 96h

zat provides a web interface with similar functionality at http://localhost:8080/zoom.

#### Slack

The slack configuration is the ID of the channel where the message should be sent.

`cmd/slack/listchannels` can assist in tracking down channel IDs like:

```
$ go build ./cmd/slack/listchannels
$ ./listchannels
0: {GroupConversation:{Conversation:{ID:CAAAAAAAA Created:"Sat Mar 10" IsOpen:false LastRead: Latest:<nil> UnreadCount:0 UnreadCountDisplay:0 IsGroup:false IsShared:false IsIM:false IsExtShared:false IsOrgShared:false IsPendingExtShared:false IsPrivate:false IsMpIM:false Unlinked:0 NameNormalized:zat NumMembers:1 Priority:0 User:} Name:zat Creator:U99999999 IsArchived:false Members:[] Topic:{Value: Creator: LastSet:"Wed Dec 31"} Purpose:{Value:Zat Discussion Creator:U99999999 LastSet:"Sat Mar 10"}} IsChannel:true IsGeneral:false IsMember:false Locale:}
1: {GroupConversation:{Conversation:{ID:CAAAAAAAA Created:"Sat Mar 10" IsOpen:false LastRead: Latest:<nil> UnreadCount:0 UnreadCountDisplay:0 IsGroup:false IsShared:false IsIM:false IsExtShared:false IsOrgShared:false IsPendingExtShared:false IsPrivate:false IsMpIM:false Unlinked:0 NameNormalized:welcome NumMembers:1 Priority:0 User:} Name:welcome Creator:U99999999 IsArchived:false Members:[] Topic:{Value: Creator:U99999999 LastSet:"Sat Mar 10"} Purpose:{Value:This channel is for workspace-wide communication and announcements. All members are in this channel. Creator:U99999999 LastSet:"Sat Mar 10"}} IsChannel:true IsGeneral:true IsMember:false Locale:}
```

`cmd/slack/chat` can assist in verifying permissions are correct.

#### Scheduling

On macOS pre-10.15 (Catalina) and Linux, `cron` is sufficient, eg:
Expand Down
2 changes: 2 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const (
// zoom OAuth persistence - oauth2.Token{}, read/write
GoogleCredsPath = "google.creds.json"
// zoom API credentials - zoom.Config{}, read only ok
// optional slack API credentials, read only ok
SlackConfigPath = "slack.config.json"
ZoomConfigPath = "zoom.config.json"
// zoom OAuth persistence - oauth2.Token{}, read/write
ZoomCredsPath = "zoom.creds.json"
Expand Down
43 changes: 43 additions & 0 deletions cmd/slack/chat/chat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
"flag"
"fmt"
"log"
"os"
"path"
"strings"

slackapi "github.com/slack-go/slack"

"github.com/graphaelli/zat/cmd"
"github.com/graphaelli/zat/slack"
)

func main() {
cfgDir := cmd.FlagConfigDir()
noEscape := flag.Bool("n", false, "don't escape message text")
flag.Parse()

if flag.NArg() < 1 {
fmt.Fprintf(os.Stderr, "usage: %s channel [text ...]\n", os.Args[0])
os.Exit(1)
}
channel := flag.Arg(0)
text := "Hello, zat"
if flag.NArg() > 1 {
text = strings.Join(flag.Args()[1:], " ")
}

logger := log.New(os.Stderr, "", cmd.LogFmt)
api, _ := slack.NewClientFromEnvOrFile(logger, path.Join(*cfgDir, cmd.SlackConfigPath), slackapi.OptionDebug(true))
if api == nil {
logger.Fatal("failed to create slack api client")
}

channel, ts, text, err := api.SendMessage(channel, slackapi.MsgOptionText(text, !*noEscape))
if err != nil {
panic(err)
}
fmt.Println(channel, ts, text)
}
33 changes: 33 additions & 0 deletions cmd/slack/listchannels/listchannels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"flag"
"fmt"
"log"
"os"
"path"

slackapi "github.com/slack-go/slack"

"github.com/graphaelli/zat/cmd"
"github.com/graphaelli/zat/slack"
)

func main() {
cfgDir := cmd.FlagConfigDir()
flag.Parse()

logger := log.New(os.Stderr, "", cmd.LogFmt)
api, _ := slack.NewClientFromEnvOrFile(logger, path.Join(*cfgDir, cmd.SlackConfigPath), slackapi.OptionDebug(true))
if api == nil {
logger.Fatal("failed to create slack api client")
}

channels, _, err := api.GetConversations(&slackapi.GetConversationsParameters{})
if err != nil {
panic(err)
}
for i, channel := range channels {
fmt.Printf("%d: %+v\n", i, channel)
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ module github.com/graphaelli/zat
go 1.12

require (
github.com/slack-go/slack v0.8.0
github.com/stretchr/testify v1.4.0
go.elastic.co/apm v1.8.0
go.elastic.co/apm/module/apmhttp v1.8.0
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ github.com/elastic/go-sysinfo v1.1.1 h1:ZVlaLDyhVkDfjwPGU55CQRCRolNpc7P0BbyhhQZQ
github.com/elastic/go-sysinfo v1.1.1/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6eh0ikPT9F0=
github.com/elastic/go-windows v1.0.0 h1:qLURgZFkkrYyTTkvYpsZIgf83AUsdIHfvlJaqaZ7aSY=
github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
Expand All @@ -34,6 +35,8 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
Expand All @@ -56,7 +59,10 @@ github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURm
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
github.com/slack-go/slack v0.8.0 h1:ANyLY5KHLV+MxLJDQum2IuHTLwbCbDtaWY405X1EU9U=
github.com/slack-go/slack v0.8.0/go.mod h1:FGqNzJBmxIsZURAxh2a8D21AnOVvvXZvGligs4npPUM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
Expand Down
50 changes: 39 additions & 11 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import (
"sync"
"time"

slackapi "github.com/slack-go/slack"
"go.elastic.co/apm"
"go.elastic.co/apm/module/apmhttp"
"google.golang.org/api/drive/v3"
"gopkg.in/yaml.v2"

"github.com/graphaelli/zat/cmd"
"github.com/graphaelli/zat/google"
"github.com/graphaelli/zat/slack"
"github.com/graphaelli/zat/zoom"
)

Expand Down Expand Up @@ -167,17 +169,23 @@ type Directive struct {
Name string `json:"name"`
Google string `json:"google"`
Zoom string `json:"zoom"`
Slack string `json:"slack"`
}

// use invalid json to avoid conflict
var skipDirective = Directive{Name: "{skip"}

// zoom meeting -> action
type Config struct {
logger *log.Logger
copies map[int64]string // for now
copies map[int64]Directive
googleClient *google.Client
slackClient *slackapi.Client
zoomClient *zoom.Client
}

func NewConfigFromFile(logger *log.Logger, path string, googleClient *google.Client, zoomClient *zoom.Client) (*Config, error) {
func NewConfigFromFile(logger *log.Logger, path string, googleClient *google.Client, zoomClient *zoom.Client,
slackClient *slackapi.Client) (*Config, error) {
f, err := os.Open(path)

if err != nil && os.IsExist(err) {
Expand All @@ -193,31 +201,33 @@ func NewConfigFromFile(logger *log.Logger, path string, googleClient *google.Cli
defer f.Close()
}

return NewConfigFromReader(logger, r, googleClient, zoomClient)
return NewConfigFromReader(logger, r, googleClient, zoomClient, slackClient)
}

func NewConfigFromReader(logger *log.Logger, r io.Reader, googleClient *google.Client, zoomClient *zoom.Client) (*Config, error) {
func NewConfigFromReader(logger *log.Logger, r io.Reader, googleClient *google.Client, zoomClient *zoom.Client,
slackClient *slackapi.Client) (*Config, error) {
var directives []Directive
if err := yaml.NewDecoder(r).Decode(&directives); err != nil && err != io.EOF {
return nil, err
}
c := map[int64]string{}
c := map[int64]Directive{}
for _, d := range directives {
key, err := strconv.ParseInt(strings.ReplaceAll(d.Zoom, "-", ""), 10, 64)
if err != nil {
return nil, err
}
if _, exists := c[key]; exists {
logger.Printf("config for %d already exists, disabling any action", key)
c[key] = "skip"
c[key] = skipDirective
continue
}
c[key] = d.Google
c[key] = d
}
return &Config{
logger: logger,
copies: c,
googleClient: googleClient,
slackClient: slackClient,
zoomClient: zoomClient,
}, nil
}
Expand Down Expand Up @@ -302,8 +312,13 @@ func (z *Config) Archive(ctx context.Context, meeting zoom.Meeting, params runPa
if err != nil {
return fmt.Errorf("while creating gdrive client: %w", err)
}
action := z.copies[meeting.ID]
if action == skipDirective {
curArchMeeting.status = "error"
return fmt.Errorf("skipped mapping meeting %d %q", meeting.ID, meeting.Topic)
}
// parent folder of all meetings
parentFolderName := z.copies[meeting.ID]
parentFolderName := action.Google
if parentFolderName == "" {
curArchMeeting.status = "error"
return fmt.Errorf("no mapping found for meeting %d %q", meeting.ID, meeting.Topic)
Expand Down Expand Up @@ -360,8 +375,8 @@ func (z *Config) Archive(ctx context.Context, meeting zoom.Meeting, params runPa
// download & upload serially for now
z.logger.Printf("archiving meeting %d to %s (https://drive.google.com/drive/folders/%s)",
meeting.ID, meetingFolder.Name, meetingFolder.Id)
uploaded := false
for _, f := range meeting.RecordingFiles {

//check if recording file duration is shorter than minimum
start, err := time.Parse(time.RFC3339, f.RecordingStart)
if err != nil {
Expand Down Expand Up @@ -425,6 +440,19 @@ func (z *Config) Archive(ctx context.Context, meeting zoom.Meeting, params runPa
}
curArchMeeting.fileNumber++
z.logger.Printf("uploaded %q to %s/%s", name, parent.Name, meetingFolder.Name)
uploaded = true
}
if uploaded && action.Slack != "" && z.slackClient != nil {
slackSpan, ctx := apm.StartSpan(ctx, "slack", "app")
body := fmt.Sprintf("%s recording now available: https://drive.google.com/drive/folders/%s", meeting.Topic, meetingFolder.Id)
channel, _, text, err := z.slackClient.SendMessageContext(ctx, action.Slack, slackapi.MsgOptionText(body, true))
if err != nil {
z.logger.Printf("failed to notify slack %q: %s", action.Slack, err)
apm.CaptureError(ctx, err).Send()
} else {
z.logger.Printf("notified slack %q: %s", channel, text)
}
slackSpan.End()
}
curArchMeeting.status = "done"
return nil
Expand Down Expand Up @@ -547,13 +575,13 @@ func main() {
if err != nil {
logger.Fatal(err)
}

slackClient, _ := slack.NewClientFromEnvOrFile(logger, *cfgDir, slackapi.OptionHTTPClient(http.DefaultClient))
rp := runParams{
minDuration: *minDuration,
since: *since,
}

zat, err := NewConfigFromFile(logger, path.Join(*cfgDir, cmd.ZatConfigPath), googleClient, zoomClient)
zat, err := NewConfigFromFile(logger, path.Join(*cfgDir, cmd.ZatConfigPath), googleClient, zoomClient, slackClient)
if err != nil {
// ok to continue without config, just can't do archival
logger.Println("failed to load config", err)
Expand Down
6 changes: 3 additions & 3 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestGoogleOauth(t *testing.T) {

zat := &Config{
logger: muxLog,
copies: map[int64]string{},
copies: map[int64]Directive{},
googleClient: googleClient,
zoomClient: nopZoomClient,
}
Expand Down Expand Up @@ -139,7 +139,7 @@ func TestZoomOauth(t *testing.T) {

zat := &Config{
logger: muxLog,
copies: map[int64]string{},
copies: map[int64]Directive{},
googleClient: nopGoogleClient,
zoomClient: zoomClient,
}
Expand Down Expand Up @@ -308,7 +308,7 @@ func TestRecordingFileName(t *testing.T) {
}

func TestConfigFromFile(t *testing.T) {
c, err := NewConfigFromFile(nil, "does-not-exist", nopGoogleClient, nopZoomClient)
c, err := NewConfigFromFile(nil, "does-not-exist", nopGoogleClient, nopZoomClient, nil)
require.NoError(t, err)
assert.NotNil(t, c)
}
Loading

0 comments on commit 3c41f14

Please sign in to comment.