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  }