-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathzoom.go
196 lines (169 loc) · 4.96 KB
/
zoom.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
package signup
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/operationspark/service-signup/zoom/meeting"
)
type (
zoomService struct {
// Base API endpoint. Default: "https://api.zoom.us/v2"
baseURL string
// Base OAuth endpoint. Default: "https://zoom.us/oauth"
oauthURL string
// HTTP client for making Zoom API requests.
// https://marketplace.zoom.us/docs/api-reference/zoom-api/methods/#overview
client http.Client
accountID string
clientID string
clientSecret string
}
tokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
TokenType string `json:"token_type"`
// Calculated from ExpiresIn
ExpiresAt time.Time
}
ZoomOptions struct {
// Overrides Zoom API base URL for testing. Default: "https://api.zoom.us/v2"
baseAPIOverride string
// Overrides Zoom OAuth base URL for testing. Default: "https://zoom.us/oauth"
baseOAuthOverride string
clientID string
clientSecret string
accountID string
}
)
func NewZoomService(o ZoomOptions) *zoomService {
apiURL := "https://api.zoom.us/v2"
if len(o.baseAPIOverride) > 0 {
apiURL = o.baseAPIOverride
}
oauthURL := "https://zoom.us/oauth"
if len(o.baseOAuthOverride) > 0 {
oauthURL = o.baseOAuthOverride
}
return &zoomService{
baseURL: apiURL,
oauthURL: oauthURL,
client: *http.DefaultClient,
clientID: o.clientID,
clientSecret: o.clientSecret,
accountID: o.accountID,
}
}
func (z *zoomService) run(ctx context.Context, su *Signup) error {
// Do nothing if the user has not signed up for a specific session
if su.StartDateTime.IsZero() {
return nil
}
return z.registerUser(ctx, su)
}
func (z *zoomService) name() string {
return "zoom service"
}
// RegisterUser creates and submits a user's registration to a meeting. The specific meeting is decided from the Signup's startDateTime.
func (z *zoomService) registerUser(ctx context.Context, su *Signup) error {
// Authenticate client
token, err := z.authenticate(ctx)
if err != nil {
return fmt.Errorf("authenticate: %w", err)
}
// Send Zoom API req to register user to meeting
reqBody := meeting.RegistrantRequest{
FirstName: su.NameFirst,
LastName: su.NameLast,
Email: su.Email,
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("marshall: %w", err)
}
// Register for a specific occurrence for the recurring meeting
url := fmt.Sprintf(
"%s/meetings/%d/registrants?occurrence_id=%d",
z.baseURL,
su.ZoomMeetingID(),
su.StartDateTime.Unix()*int64(time.Microsecond),
)
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
url,
bytes.NewBuffer(jsonBody),
)
if err != nil {
return fmt.Errorf("newRequestWithContext: %w", err)
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
req.Header.Add("Content-Type", "application/json")
resp, err := z.client.Do(req)
if err != nil {
return fmt.Errorf("client.Do: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return handleHTTPError(resp)
}
var respBody meeting.RegistrationResponse
d := json.NewDecoder(resp.Body)
err = d.Decode(&respBody)
if err != nil {
return fmt.Errorf("decode: %w", err)
}
su.SetZoomJoinURL(respBody.JoinURL)
return nil
}
// Authenticate requests an access token.
func (z *zoomService) authenticate(ctx context.Context) (tokenResponse, error) {
url := fmt.Sprintf("%s/token?grant_type=account_credentials&account_id=%s", z.oauthURL, z.accountID)
// Make a HTTP req to authenticate the client
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
url,
nil,
)
if err != nil {
return tokenResponse{}, fmt.Errorf("NewRequestWithContext: %w", err)
}
req.Header.Add("Authorization", "Basic "+z.encodeCredentials())
resp, err := z.client.Do(req)
if err != nil {
return tokenResponse{}, fmt.Errorf("client.do: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return tokenResponse{}, handleHTTPError(resp)
}
var body tokenResponse
d := json.NewDecoder(resp.Body)
err = d.Decode(&body)
if err != nil {
return tokenResponse{}, fmt.Errorf("decode: %w", err)
}
creds := tokenResponse{
AccessToken: body.AccessToken,
ExpiresIn: body.ExpiresIn,
ExpiresAt: time.Now().Add(time.Second * time.Duration(body.ExpiresIn)),
}
return creds, nil
}
func (z *zoomService) isAuthenticated(creds tokenResponse) bool {
// Shave 5 min off expiration date as a buffer to request a new token
expirationDate := creds.ExpiresAt.Add(time.Minute * -5)
return len(creds.AccessToken) > 0 &&
time.Now().Before(expirationDate)
}
// EncodeCredentials base64 encodes the client ID and secret, separated by a colon.
// ie: Base64Encode([clientID]:[clientSecret])
func (z *zoomService) encodeCredentials() string {
creds := fmt.Sprintf("%s:%s", z.clientID, z.clientSecret)
return base64.StdEncoding.EncodeToString([]byte(creds))
}