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

add slack notifications #31

Merged
merged 5 commits into from
Feb 5, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
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