github.com/livekit/protocol@v1.39.3/webhook/notifier.go (about) 1 // Copyright 2023 LiveKit, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package webhook 16 17 import ( 18 "context" 19 "fmt" 20 "sync" 21 "time" 22 23 "github.com/livekit/protocol/auth" 24 "github.com/livekit/protocol/livekit" 25 "github.com/livekit/protocol/logger" 26 "google.golang.org/protobuf/types/known/timestamppb" 27 ) 28 29 type WebHookConfig struct { 30 URLs []string `yaml:"urls,omitempty"` 31 APIKey string `yaml:"api_key,omitempty"` 32 URLNotifier URLNotifierConfig `yaml:"url_notifier,omitempty"` 33 ResourceURLNotifier ResourceURLNotifierConfig `yaml:"resource_url_notifier,omitempty"` 34 } 35 36 var DefaultWebHookConfig = WebHookConfig{ 37 URLNotifier: DefaultURLNotifierConfig, 38 ResourceURLNotifier: DefaultResourceURLNotifierConfig, 39 } 40 41 type NotifyParams struct { 42 ExtraWebhooks []*livekit.WebhookConfig 43 Secret string 44 } 45 46 type NotifyOption func(*NotifyParams) 47 48 func WithExtraWebhooks(wh []*livekit.WebhookConfig) NotifyOption { 49 return func(p *NotifyParams) { 50 p.ExtraWebhooks = wh 51 } 52 } 53 54 func WithSecret(secret string) NotifyOption { 55 return func(p *NotifyParams) { 56 p.Secret = secret 57 } 58 } 59 60 type QueuedNotifier interface { 61 RegisterProcessedHook(f func(ctx context.Context, whi *livekit.WebhookInfo)) 62 SetKeys(apiKey, apiSecret string) 63 SetFilter(params FilterParams) 64 QueueNotify(ctx context.Context, event *livekit.WebhookEvent, opts ...NotifyOption) error 65 Stop(force bool) 66 } 67 68 type DefaultNotifier struct { 69 kp auth.KeyProvider 70 71 notifiers []QueuedNotifier 72 extraWebhookNotifier QueuedNotifier 73 } 74 75 func NewDefaultNotifier(config WebHookConfig, kp auth.KeyProvider) (QueuedNotifier, error) { 76 apiSecret := kp.GetSecret(config.APIKey) 77 if apiSecret == "" && len(config.URLs) > 0 { 78 return nil, fmt.Errorf("unknown api key in webhook config") 79 } 80 81 n := &DefaultNotifier{ 82 kp: kp, 83 } 84 for _, url := range config.URLs { 85 u := NewResourceURLNotifier(ResourceURLNotifierParams{ 86 URL: url, 87 Logger: logger.GetLogger().WithComponent("webhook"), 88 APIKey: config.APIKey, 89 APISecret: apiSecret, 90 Config: config.ResourceURLNotifier, 91 }) 92 n.notifiers = append(n.notifiers, u) 93 } 94 95 n.extraWebhookNotifier = NewResourceURLNotifier(ResourceURLNotifierParams{ 96 Logger: logger.GetLogger().WithComponent("webhook"), 97 APIKey: config.APIKey, 98 APISecret: apiSecret, 99 Config: config.ResourceURLNotifier, 100 }) 101 102 return n, nil 103 } 104 105 func (n *DefaultNotifier) Stop(force bool) { 106 wg := sync.WaitGroup{} 107 for _, u := range n.notifiers { 108 wg.Add(1) 109 go func(u QueuedNotifier) { 110 defer wg.Done() 111 u.Stop(force) 112 }(u) 113 } 114 115 wg.Add(1) 116 go func() { 117 defer wg.Done() 118 n.extraWebhookNotifier.Stop(force) 119 }() 120 121 wg.Wait() 122 } 123 124 func (n *DefaultNotifier) QueueNotify(ctx context.Context, event *livekit.WebhookEvent, opts ...NotifyOption) error { 125 for _, u := range n.notifiers { 126 // No override for static notifiers 127 if err := u.QueueNotify(ctx, event); err != nil { 128 return err 129 } 130 } 131 132 p := &NotifyParams{} 133 for _, o := range opts { 134 o(p) 135 } 136 137 for _, wh := range p.ExtraWebhooks { 138 lopts := []NotifyOption{ 139 WithExtraWebhooks([]*livekit.WebhookConfig{wh}), 140 } 141 142 if wh.SigningKey != "" { 143 // empty signing key means default 144 k := n.kp.GetSecret(wh.SigningKey) 145 if k == "" { 146 return fmt.Errorf("no secret for provided signing key") 147 } 148 149 lopts = append(lopts, WithSecret(k)) 150 } 151 152 if err := n.extraWebhookNotifier.QueueNotify(ctx, event, lopts...); err != nil { 153 return err 154 } 155 } 156 157 return nil 158 } 159 160 func (n *DefaultNotifier) RegisterProcessedHook(hook func(ctx context.Context, whi *livekit.WebhookInfo)) { 161 for _, u := range n.notifiers { 162 u.RegisterProcessedHook(hook) 163 } 164 } 165 166 func (n *DefaultNotifier) SetKeys(apiKey, apiSecret string) { 167 for _, u := range n.notifiers { 168 u.SetKeys(apiKey, apiSecret) 169 } 170 } 171 172 func (n *DefaultNotifier) SetFilter(params FilterParams) { 173 for _, u := range n.notifiers { 174 u.SetFilter(params) 175 } 176 } 177 178 // --------------------------------- 179 180 type HTTPClientParams struct { 181 RetryWaitMin time.Duration 182 RetryWaitMax time.Duration 183 MaxRetries int 184 ClientTimeout time.Duration 185 } 186 187 type FilterParams struct { 188 IncludeEvents []string 189 ExcludeEvents []string 190 } 191 192 // --------------------------------- 193 194 type logAdapter struct{} 195 196 func (l *logAdapter) Printf(string, ...interface{}) {} 197 198 // --------------------------------- 199 200 func eventKey(event *livekit.WebhookEvent) string { 201 if event.EgressInfo != nil { 202 return event.EgressInfo.EgressId 203 } 204 if event.IngressInfo != nil { 205 return event.IngressInfo.IngressId 206 } 207 if event.Room != nil { 208 return event.Room.Name 209 } 210 if event.Participant != nil { 211 return event.Participant.Identity 212 } 213 if event.Track != nil { 214 return event.Track.Sid 215 } 216 logger.Warnw("webhook using default event", nil, "event", logger.Proto(event)) 217 return "default" 218 } 219 220 func logFields(event *livekit.WebhookEvent, url string) []interface{} { 221 fields := make([]interface{}, 0, 20) 222 fields = append(fields, 223 "event", event.Event, 224 "id", event.Id, 225 "webhookTime", event.CreatedAt, 226 "url", url, 227 ) 228 229 if event.Room != nil { 230 fields = append(fields, 231 "room", event.Room.Name, 232 "roomID", event.Room.Sid, 233 ) 234 } 235 if event.Participant != nil { 236 fields = append(fields, 237 "participant", event.Participant.Identity, 238 "pID", event.Participant.Sid, 239 ) 240 } 241 if event.Track != nil { 242 fields = append(fields, 243 "trackID", event.Track.Sid, 244 ) 245 } 246 if event.EgressInfo != nil { 247 fields = append(fields, 248 "egressID", event.EgressInfo.EgressId, 249 "status", event.EgressInfo.Status, 250 ) 251 if event.EgressInfo.Error != "" { 252 fields = append(fields, "error", event.EgressInfo.Error) 253 } 254 } 255 if event.IngressInfo != nil { 256 fields = append(fields, 257 "ingressID", event.IngressInfo.IngressId, 258 ) 259 if event.IngressInfo.State != nil { 260 fields = append(fields, "status", event.IngressInfo.State.Status) 261 if event.IngressInfo.State.Error != "" { 262 fields = append(fields, "error", event.IngressInfo.State.Error) 263 } 264 } 265 } 266 return fields 267 } 268 269 func webhookInfo( 270 event *livekit.WebhookEvent, 271 queuedAt time.Time, 272 queueDuration time.Duration, 273 sentAt time.Time, 274 sendDuration time.Duration, 275 url string, 276 isDropped bool, 277 sendError error, 278 ) *livekit.WebhookInfo { 279 whi := &livekit.WebhookInfo{ 280 EventId: event.Id, 281 Event: event.Event, 282 CreatedAt: timestamppb.New(time.Unix(event.CreatedAt, 0)), 283 QueuedAt: timestamppb.New(queuedAt), 284 QueueDurationNs: queueDuration.Nanoseconds(), 285 SentAt: timestamppb.New(sentAt), 286 SendDurationNs: sendDuration.Nanoseconds(), 287 Url: url, 288 NumDropped: event.NumDropped, 289 IsDropped: isDropped, 290 } 291 if !queuedAt.IsZero() { 292 whi.QueuedAt = timestamppb.New(queuedAt) 293 } 294 if !sentAt.IsZero() { 295 whi.SentAt = timestamppb.New(sentAt) 296 } 297 if event.Room != nil { 298 whi.RoomName = event.Room.Name 299 whi.RoomId = event.Room.Sid 300 } 301 if event.Participant != nil { 302 whi.ParticipantIdentity = event.Participant.Identity 303 whi.ParticipantId = event.Participant.Sid 304 } 305 if event.Track != nil { 306 whi.TrackId = event.Track.Sid 307 } 308 if event.EgressInfo != nil { 309 whi.EgressId = event.EgressInfo.EgressId 310 whi.ServiceStatus = event.EgressInfo.Status.String() 311 if event.EgressInfo.Error != "" { 312 whi.ServiceErrorCode = event.EgressInfo.ErrorCode 313 whi.ServiceError = event.EgressInfo.Error 314 } 315 } 316 if event.IngressInfo != nil { 317 whi.IngressId = event.IngressInfo.IngressId 318 if event.IngressInfo.State != nil { 319 whi.ServiceStatus = event.IngressInfo.State.Status.String() 320 if event.IngressInfo.State.Error != "" { 321 whi.ServiceError = event.IngressInfo.State.Error 322 } 323 } 324 } 325 if sendError != nil { 326 whi.SendError = sendError.Error() 327 } 328 return whi 329 }