Skip to content

Commit c7709f1

Browse files
authored
Merge pull request #92 from stahnma/meeting_links
Add meeting link to zoom-notifier
2 parents 2a7ea90 + 7749e98 commit c7709f1

File tree

6 files changed

+326
-19
lines changed

6 files changed

+326
-19
lines changed

zoom-notifier/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
index.html
22
zoom-notifier
33
go.sum
4+
*.creds

zoom-notifier/Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ build: tidy
1818
go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.buildDate=$(BUILDDATE)" -o $(NAME) .
1919

2020
clean:
21-
rm -rf $(NAME) bin
21+
rm -rf $(NAME) bin index.html
2222

2323
linux-arm64: tidy
2424
GOOS=linux GOARCH=arm64 go build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.buildDate=$(BUILDDATE)" -o bin/$(NAME).linux-arm64 .

zoom-notifier/TODO

+15-1
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,18 @@ slack can be disabled -- yes
2222
still need to verify all things CRC
2323
Then, add mulitplexling slack displatch
2424
then clean up and document and test
25-
then add channel switcher stuff (cci bs to general and back, mute unmute maybe)
25+
then add channel switcher stuff (cci bs to general and back, mute unmute maybe)
26+
27+
28+
slack slash command to query the meeting and status of the notifier
29+
Show routes and stuff
30+
Rename callZoomApi
31+
32+
33+
godoc
34+
document zoom api usage
35+
slack client with slash commands
36+
37+
don't print message twice. -- this may be because multiple web hooks are coming in
38+
39+
Have a debug mode to drop all web hook payloads in a directory

zoom-notifier/main.go

+115-9
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import (
66
"encoding/hex"
77
"flag"
88
"fmt"
9+
"io"
910
"net/http"
1011
"os"
12+
"path/filepath"
1113
"strings"
1214
"time"
1315

@@ -83,7 +85,7 @@ func zoomCrcValidation(jresp ZoomWebhook) (bool, ChallengeResponse) {
8385
func filterMeeting(jresp ZoomWebhook) bool {
8486
// If the meeting is outside the topic scope, just ignore.
8587
name := viper.GetString("meeting_name")
86-
log.Debugln("applyMeetingFilters) Topic " + jresp.Payload.Object.Topic)
88+
log.Debugln("(applyMeetingFilters) Topic " + jresp.Payload.Object.Topic)
8789
if name != jresp.Payload.Object.Topic && name != "" {
8890
log.Infoln("Received hook but dropping due to topic being filtered.")
8991
log.Debugln("(applyMeetingFilter) Hook had topic '" + jresp.Payload.Object.Topic + "'")
@@ -107,10 +109,58 @@ func setMessageSuffix(jresp ZoomWebhook) string {
107109
return msg
108110
}
109111

112+
// savePostRequestToFile writes the payload of a POST request to a file in the "hooks" directory.
113+
//
114+
// It operates silently, meaning it does not modify the HTTP response or interfere with request processing.
115+
// If the request is not a POST, has no body, or if an error occurs while reading or writing the body,
116+
// the function simply returns without taking any action.
117+
//
118+
// The request payload is saved in a file named "hook-<timestamp>.json", where <timestamp> follows the
119+
// "YYYYMMDD-HHMMSS.mmm" format to ensure unique filenames. The directory "hooks" is created if it does not exist.
120+
//
121+
// Parameters:
122+
// - c: A *gin.Context representing the HTTP request and response context.
123+
//
124+
// Usage:
125+
//
126+
// This function can be used as middleware in a Gin application to capture incoming POST request payloads.
127+
//
128+
// Example:
129+
//
130+
// r := gin.Default()
131+
// r.Use(savePostRequestToFile) // Logs POST request bodies to the "hooks" directory
132+
// r.POST("/hook", func(c *gin.Context) {
133+
// c.JSON(200, gin.H{"message": "Hook received"})
134+
// })
135+
// r.Run(":8080")
136+
func savePostRequestToFile(c *gin.Context) {
137+
if c.Request.Method != "POST" {
138+
return // Only process POST requests
139+
}
140+
141+
// Read the request body
142+
body, err := io.ReadAll(c.Request.Body)
143+
if err != nil || len(body) == 0 {
144+
return // If there's an error reading or body is empty, do nothing
145+
}
146+
147+
// Ensure the hooks directory exists
148+
hooksDir := "hooks"
149+
_ = os.MkdirAll(hooksDir, 0755)
150+
151+
// Generate a timestamp-based filename
152+
timestamp := time.Now().Format("20060102-150405.000")
153+
filename := filepath.Join(hooksDir, "hook-"+timestamp+".json")
154+
155+
// Write the body to a file (ignoring errors to avoid interfering with request processing)
156+
_ = os.WriteFile(filename, body, 0644)
157+
}
158+
110159
func processWebHook(c *gin.Context) {
111160

161+
log.Debugln("(processWebHook) Processing incoming webhook. at " + time.Now().String())
112162
if gin.IsDebugging() {
113-
// log incoming request if in DEBUG mode
163+
// savePostRequestToFile(c)
114164
}
115165
var jresp ZoomWebhook
116166
if err := c.BindJSON(&jresp); err != nil {
@@ -134,13 +184,42 @@ func processWebHook(c *gin.Context) {
134184
return
135185
}
136186

187+
// If the event type is not a participant join or leave, ignore it.
188+
if jresp.Event != "meeting.participant_joined" && jresp.Event != "meeting.participant_left" {
189+
log.Debugln("(processWebHook) Ignoring event type: " + jresp.Event)
190+
return
191+
}
192+
137193
msg := setMessageSuffix(jresp)
138-
// create a link for the zoom meeting in the message
139-
msg = msg + " [Zoom Meeting](https://zoom.us/j/" + jresp.Payload.Object.ID + ")"
140-
dispatchMessage(msg)
194+
log.Debugln("About to dispatch Message: " + msg)
195+
dispatchMessage(msg, jresp)
196+
}
197+
198+
func getJoinURL(meetingId string) string {
199+
/*
200+
To enable this feature, you must set the following environment variables:
201+
ZOOM_API_ENABLE=1
202+
ZOOM_API_CLIENT_ID
203+
ZOOM_API_CLIENT_SECRET
204+
ZOOM_API_ACCOUNT_ID
205+
*/
206+
joinurl := ""
207+
if os.Getenv("ZOOM_API_ENABLE") == "1" {
208+
// Get secret from the zoom API so we can get the meeting details
209+
// Check to see that ZOOM_API_CLIENT_ID, ZOOM_API_CLIENT_SECRET, and ZOOM_API_ACCOUNT_ID are set
210+
if os.Getenv("ZOOM_API_CLIENT_ID") != "" && os.Getenv("ZOOM_API_CLIENT_SECRET") != "" && os.Getenv("ZOOM_API_ACCOUNT_ID") != "" {
211+
log.Debugln(meetingId)
212+
joinurl := callZoomAPI(meetingId)
213+
log.Debugln("Join URL: " + joinurl)
214+
} else {
215+
// This should be unreachable code, but it's there for debugging and defense.
216+
log.Errorln("ZOOM_API environment credentials are not set. Skipping.")
217+
}
218+
}
219+
return joinurl
141220
}
142221

143-
func dispatchMessage(msg string) {
222+
func dispatchMessage(msg string, jresp ZoomWebhook) {
144223

145224
slack_enable := viper.GetString("slack_enable")
146225
irc_enable := viper.GetString("irc_enable")
@@ -150,9 +229,8 @@ func dispatchMessage(msg string) {
150229

151230
if strings.ToLower(slack_enable) == "true" {
152231
log.Debugln("(dispatchMessage) Sending a slack message")
153-
parseAndSplitSlackHooks(msg)
232+
parseAndSplitSlackHooks(msg, jresp)
154233
sent = 1
155-
156234
}
157235
if strings.ToLower(irc_enable) == "true" {
158236
log.Debugln("(dispatchMessage) Sending an IRC message")
@@ -174,6 +252,7 @@ func inititalize() {
174252
viper.SetDefault("slack_enable", "true")
175253
viper.SetDefault("irc_enable", "false")
176254
viper.SetDefault("msg_suffix", "the zoom meeting.")
255+
viper.SetDefault("zoom_api_enable", "false")
177256

178257
viper.BindEnv("port", "ZOOMWH_PORT")
179258
viper.BindEnv("slack_enable", "ZOOMWH_SLACK_ENABLE")
@@ -186,6 +265,34 @@ func inititalize() {
186265
viper.BindEnv("zoom_secret", "ZOOM_SECRET")
187266
}
188267

268+
// Zoom API Specifics
269+
viper.BindEnv("ZOOM_API_ENABLE")
270+
zoomApiEnabled := viper.GetBool("ZOOM_API_ENABLE")
271+
if zoomApiEnabled == false {
272+
log.Infoln("Zoom Web API is disabled. Disabling active meeting links and quieries")
273+
viper.Set("zoom_api_enable", "false")
274+
} else {
275+
log.Infoln("Zoom Web API is enabled.")
276+
viper.MustBindEnv("zoom_api_client_id", "ZOOM_API_CLIENT_ID")
277+
zoom_api_client_id := viper.GetString("zoom_api_client_id")
278+
if zoom_api_client_id == "" {
279+
log.Errorln("You must set ZOOM_API_CLIENT_ID environment variable if ZOOM_API_ENABLE=true.")
280+
bugout = true
281+
}
282+
viper.MustBindEnv("zoom_api_client_secret", "ZOOM_API_CLIENT_SECRET")
283+
zoom_api_client_secret := viper.GetString("zoom_api_client_secret")
284+
if zoom_api_client_secret == "" {
285+
log.Errorln("You must set ZOOM_API_CLIENT_SECRET environment variable if ZOOM_API_ENABLE=true.")
286+
bugout = true
287+
}
288+
viper.MustBindEnv("zoom_api_account_id", "ZOOM_API_ACCOUNT_ID")
289+
zoom_api_account_id := viper.GetString("zoom_api_account_id")
290+
if zoom_api_account_id == "" {
291+
log.Errorln("You must set ZOOM_API_ACCOUNT_ID environment variable if ZOOM_API_ENABLE=true.")
292+
bugout = true
293+
}
294+
}
295+
189296
// Slack Specifics
190297
viper.GetString("slack_enable")
191298
if value := os.Getenv("ZOOMWH_SLACK_ENABLE"); value == "false" {
@@ -255,7 +362,6 @@ func inititalize() {
255362

256363
func main() {
257364

258-
// Define a --version flag
259365
showVersion := flag.Bool("version", false, "Show version information")
260366
flag.Parse()
261367

zoom-notifier/slack.go

+62-8
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
package main
22

33
import (
4+
"bytes"
5+
"encoding/json"
46
"net/http"
5-
"net/url"
67
"strings"
78

89
log "github.com/sirupsen/logrus"
910
"github.com/spf13/viper"
1011
)
1112

12-
func parseAndSplitSlackHooks(msg string) {
13+
func parseAndSplitSlackHooks(msg string, jresp ZoomWebhook) {
1314
log.Debugln("(parseAndSplitSlackHooks) Parsing and splitting slack hooks.")
15+
log.Debugln("(parseAndSplitSlackHooks) The message is:", msg)
16+
log.Debugln("(parseAndSplitSlackHooks) The meeting ID is:", jresp.Payload.Object.ID)
1417
slackHooks := viper.GetString("slack_webhook_uri")
1518

1619
splitStrings := strings.Split(slackHooks, ",")
@@ -20,19 +23,70 @@ func parseAndSplitSlackHooks(msg string) {
2023
}
2124
size := len(splitStrings)
2225
log.Debugln("(parseAndSplitSlackHooks) Found", size, "slack hooks.")
26+
msg = formatSlackMessage(msg, jresp)
2327
for _, entry := range splitStrings {
2428
postToSlack(msg, entry)
2529
}
2630
}
2731

32+
func formatSlackMessage(msg string, jresp ZoomWebhook) string {
33+
// Read in suffix, and make that the hot link
34+
log.Debugln("XXXXXXX")
35+
log.Debugln("(formatSlackMessage) msg is ", msg)
36+
// return if ZOOM_API_ENABLE is not enabled
37+
zae := viper.GetBool("zoom_api_enable")
38+
log.Debugln("(formatSlackMessage) ZOOM_API_ENABLE zae is:", zae)
39+
if !zae {
40+
return msg
41+
}
42+
joinurl := callZoomAPI(jresp.Payload.Object.ID)
43+
log.Debugln("(formatSlackMessage) The join URL is:", joinurl)
44+
45+
msg_suffix := viper.GetString("msg_suffix")
46+
log.Debugln("(formatSlackMessage) The suffix is:", msg_suffix)
47+
msg_suffix = "<" + joinurl + "|" + msg_suffix + ">"
48+
49+
msg = msg + " " + msg_suffix
50+
log.Debugln("(formatSlackMessage) The message is:", msg)
51+
52+
switch jresp.Event {
53+
case "meeting.participant_left":
54+
msg = jresp.Payload.Object.Participant.UserName + " has left " + msg_suffix
55+
case "meeting.participant_joined":
56+
msg = jresp.Payload.Object.Participant.UserName + " has joined " + msg_suffix
57+
default:
58+
msg = msg
59+
60+
}
61+
return msg
62+
}
63+
2864
func postToSlack(msg string, uri string) {
29-
log.Debugln("(postToSlack) Posting to each slack hook.", uri, msg)
30-
data := url.Values{
31-
"payload": {"{\"text\": \"" + msg + "\"}"},
65+
log.Debugln("(postToSlack) The message is:", msg)
66+
log.Debugln("(postToSlack) The slack webhook uri is:", uri)
67+
68+
// Constructing payload with Markdown and unfurl prevention
69+
payload := map[string]interface{}{
70+
"text": msg,
71+
"mrkdwn": true,
72+
"unfurl_links": false,
73+
"unfurl_media": false,
3274
}
33-
resp, err := http.PostForm(uri, data)
75+
76+
// Convert to JSON
77+
payloadBytes, err := json.Marshal(payload)
3478
if err != nil {
35-
log.Errorln("Error posting to slack:", err)
79+
log.Println("Error marshaling JSON:", err)
80+
return
3681
}
37-
log.Debugln(resp.Status)
82+
83+
// Make HTTP POST request
84+
resp, err := http.Post(uri, "application/json", bytes.NewBuffer(payloadBytes))
85+
if err != nil {
86+
log.Println("Error posting to Slack:", err)
87+
return
88+
}
89+
defer resp.Body.Close()
90+
91+
log.Debugln("(postToSlack) Response Status:", resp.Status)
3892
}

0 commit comments

Comments
 (0)