2023-11-07 22:34:57 -05:00
package controllers
import (
2023-11-14 13:48:12 -05:00
"context"
2023-11-07 22:34:57 -05:00
"errors"
"fmt"
"regexp"
"sync"
2023-11-23 12:17:20 -05:00
"time"
2023-11-07 22:34:57 -05:00
2023-11-16 13:00:28 -05:00
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
2023-11-23 12:17:20 -05:00
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db"
2023-11-07 22:34:57 -05:00
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
2023-11-16 13:00:28 -05:00
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco"
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/services"
2023-11-07 22:34:57 -05:00
"git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/pco/webhooks"
2023-11-16 20:43:34 -05:00
yt_helpers "git.preston-baxter.com/Preston_PLB/capstone/webhook-service/vendors/youtube"
2023-11-07 22:34:57 -05:00
"github.com/gin-gonic/gin"
"github.com/google/jsonapi"
"go.mongodb.org/mongo-driver/bson/primitive"
2023-11-14 14:08:53 -05:00
"golang.org/x/oauth2"
2023-11-14 13:48:12 -05:00
"google.golang.org/api/option"
"google.golang.org/api/youtube/v3"
2023-11-07 22:34:57 -05:00
)
var (
eventRegexKeys = map [ string ] string { "plan" : ` ^services\.v2\.events\.plan\..* ` }
2023-11-16 20:43:34 -05:00
actionFuncMap = map [ string ] actionFunc { "youtube.livestream" : ScheduleBroadcastFromWebhook }
2023-11-23 12:17:20 -05:00
//Error definintions
errorOkMap = map [ error ] bool { NotSchedulableTime : true , UnknownEventErr : true , AlreadyScheduledBroadcast : true , NoBroadcastToDelete : true }
NotSchedulableTime = errors . New ( "This time is not schedulable" )
UnknownEventErr = errors . New ( "Event sent is unkown" )
AlreadyScheduledBroadcast = errors . New ( "This broadcast has already been scheduled" )
NoBroadcastToDelete = errors . New ( "No Broadcasts to destroy" )
)
const (
CREATED_BROADCAST = "Created Broadcast"
UPDATED_BROADCAST = "Updated Broadcast"
DELETED_BROADCAST = "Deleted Broadcast"
2023-11-07 22:34:57 -05:00
)
type actionFunc func ( * gin . Context , * webhooks . EventDelivery ) error
2023-11-18 21:16:44 -05:00
func userIdFromContext ( c * gin . Context ) * primitive . ObjectID {
2023-11-18 19:17:14 -05:00
if id , ok := c . Get ( "user_bson_id" ) ; ! ok {
userId := c . Param ( "userid" )
2023-11-07 22:34:57 -05:00
2023-11-18 19:17:14 -05:00
if userId == "" {
log . Warn ( "Webhook did not contain user id. Rejecting" )
c . AbortWithStatus ( 404 )
return nil
}
2023-11-07 22:34:57 -05:00
2023-11-18 19:17:14 -05:00
userObjectId , err := primitive . ObjectIDFromHex ( userId )
if err != nil {
log . WithError ( err ) . Warn ( "User Id was malformed" )
c . AbortWithStatus ( 400 )
return nil
}
c . Set ( "user_bson_id" , userObjectId )
return & userObjectId
} else {
if objId , ok := id . ( primitive . ObjectID ) ; ok {
return & objId
} else {
return nil
}
2023-11-07 22:34:57 -05:00
}
2023-11-18 19:17:14 -05:00
}
func ConsumePcoWebhook ( c * gin . Context ) {
userObjectId := userIdFromContext ( c )
2023-11-07 22:34:57 -05:00
//read body and handle io in parallel because IO shenanigains
wg := new ( sync . WaitGroup )
wg . Add ( 2 )
2023-11-18 19:17:14 -05:00
//get actions for user
2023-11-07 22:34:57 -05:00
var actionMappings [ ] models . ActionMapping
var webhookBody * webhooks . EventDelivery
errs := make ( [ ] error , 2 )
go func ( wg * sync . WaitGroup ) {
2023-11-23 09:05:44 -05:00
defer wg . Done ( )
2023-11-18 19:17:14 -05:00
actionMappings , errs [ 0 ] = mongo . FindActionMappingsByUser ( * userObjectId )
2023-11-07 22:34:57 -05:00
} ( wg )
go func ( wg * sync . WaitGroup ) {
2023-11-23 09:05:44 -05:00
defer wg . Done ( )
var payload [ ] webhooks . EventDelivery
payload , errs [ 1 ] = jsonapi . UnmarshalManyPayload [ webhooks . EventDelivery ] ( c . Request . Body )
webhookBody = & payload [ 0 ]
2023-11-07 22:34:57 -05:00
} ( wg )
wg . Wait ( )
if err := errors . Join ( errs ... ) ; err != nil {
log . WithError ( err ) . Errorf ( "Failed to do the IO parts" )
_ = c . AbortWithError ( 501 , err )
return
}
//perform actions
//loop through all actions a user has
for _ , mapping := range actionMappings {
//find the ones that are runable by this function
2023-11-23 09:05:44 -05:00
if mapping . SourceEvent . VendorName == models . PCO_VENDOR_NAME && eventMatch ( mapping . SourceEvent . Key , webhookBody . Name ) {
2023-11-07 22:34:57 -05:00
//generate lookup key for function
2023-11-23 09:05:44 -05:00
actionKey := fmt . Sprintf ( "%s.%s" , mapping . Action . VendorName , mapping . Action . Type )
2023-11-07 22:34:57 -05:00
//if function exists run the function
if action , ok := actionFuncMap [ actionKey ] ; ok {
2023-11-18 19:17:14 -05:00
err := action ( c , webhookBody )
2023-11-07 22:34:57 -05:00
//handle error
if err != nil {
2023-11-23 12:17:20 -05:00
//if err is in the ok map, return 200
if pass , ok := errorOkMap [ err ] ; ok && pass {
log . Warnf ( "Continueing after error: %s. From action: %s. From event source: %s:%s" , err , actionKey , mapping . SourceEvent . VendorName , mapping . SourceEvent . Key )
c . Status ( 200 )
} else {
log . WithError ( err ) . Errorf ( "Failed to execute action: %s. From event source: %s:%s" , actionKey , mapping . SourceEvent . VendorName , mapping . SourceEvent . Key )
_ = c . AbortWithError ( 501 , err )
}
2023-11-23 09:05:44 -05:00
} else {
log . Infof ( "Succesfully proccessed: %s for %s" , webhookBody . Name , userObjectId . Hex ( ) )
c . Status ( 200 )
2023-11-07 22:34:57 -05:00
}
2023-11-23 09:05:44 -05:00
return
2023-11-07 22:34:57 -05:00
}
}
}
2023-11-23 09:05:44 -05:00
log . Warnf ( "No errors, but also no work..." )
c . Status ( 200 )
2023-11-07 22:34:57 -05:00
}
2023-11-23 09:05:44 -05:00
func eventMatch ( key , event string ) bool {
if regexString , ok := eventRegexKeys [ key ] ; ok {
reg := regexp . MustCompile ( regexString ) //TODO: Make this regex cache-able
2023-11-07 22:34:57 -05:00
return reg . MatchString ( event )
} else {
return false
}
}
2023-11-16 13:00:28 -05:00
func pcoServiceForUser ( userId primitive . ObjectID ) ( * pco . PcoApiClient , error ) {
//add youtube client to map if its not there
if client , ok := pcoClientMap [ userId ] ; ! ok {
pcoAccount , err := mongo . FindVendorAccountByUser ( userId , models . PCO_VENDOR_NAME )
if err != nil {
return nil , err
}
//Build our fancy token source
tokenSource := oauth2 . ReuseTokenSource ( pcoAccount . Token ( ) , mongo . NewVendorTokenSource ( pcoAccount ) )
//init service
conf := config . Config ( )
client := pco . NewClientWithOauthConfig ( conf . Vendors [ models . PCO_VENDOR_NAME ] . OauthConfig ( ) , tokenSource )
//add user to map
pcoClientMap [ userId ] = client
return client , nil
} else {
return client , nil
}
}
2023-11-14 13:48:12 -05:00
func youtubeServiceForUser ( userId primitive . ObjectID ) ( * youtube . Service , error ) {
2023-11-16 13:00:28 -05:00
//add youtube client to map if its not there
2023-11-14 14:08:53 -05:00
if client , ok := ytClientMap [ userId ] ; ! ok {
ytAccount , err := mongo . FindVendorAccountByUser ( userId , models . YOUTUBE_VENDOR_NAME )
if err != nil {
return nil , err
}
2023-11-14 13:48:12 -05:00
2023-11-14 14:08:53 -05:00
//Build our fancy token source
tokenSource := oauth2 . ReuseTokenSource ( ytAccount . Token ( ) , mongo . NewVendorTokenSource ( ytAccount ) )
//init service
client , err := youtube . NewService ( context . Background ( ) , option . WithTokenSource ( tokenSource ) )
if err != nil {
log . WithError ( err ) . Error ( "Failed to init youtube service" )
return nil , err
}
//add user to map
ytClientMap [ userId ] = client
return client , nil
} else {
return client , nil
2023-11-14 13:48:12 -05:00
}
}
2023-11-23 13:22:16 -05:00
// TODO: Revisit the structure of this function
2023-11-16 20:15:30 -05:00
func ScheduleBroadcastFromWebhook ( c * gin . Context , body * webhooks . EventDelivery ) error {
2023-11-14 14:08:53 -05:00
//get uid from context. Lots of sanitizing just incase
2023-11-23 10:09:18 -05:00
uid := userIdFromContext ( c )
//Check if this is a redilivery.
2023-11-14 14:08:53 -05:00
2023-11-16 20:15:30 -05:00
//Load ytClient for user. It is fetched from cache or created
2023-11-23 10:09:18 -05:00
ytClient , err := youtubeServiceForUser ( * uid )
2023-11-14 14:08:53 -05:00
if err != nil {
log . WithError ( err ) . Error ( "Failed to initialize youtube client" )
return err
}
2023-11-14 13:48:12 -05:00
2023-11-16 20:15:30 -05:00
//Load pcoClient for user. It is fetched from cache or created
2023-11-23 10:09:18 -05:00
pcoClient , err := pcoServiceForUser ( * uid )
2023-11-16 13:00:28 -05:00
if err != nil {
log . WithError ( err ) . Error ( "Failed to initialize youtube client" )
return err
}
2023-11-16 20:15:30 -05:00
//deserialize the payload
payload := & services . Plan { }
err = body . UnmarshallPayload ( payload )
if err != nil {
log . WithError ( err ) . Error ( "Failed to unmarshall body" )
return err
}
2023-11-16 20:43:34 -05:00
//Save audit point
eventRecievedAudit := & models . EventRecieved {
2023-11-24 03:01:38 -05:00
UserId : * uid ,
VendorName : models . PCO_VENDOR_NAME ,
VendorId : body . ID ,
CorrelationId : payload . Id ,
Type : body . Name ,
2023-11-16 20:43:34 -05:00
}
2023-11-23 09:05:44 -05:00
2023-11-16 20:43:34 -05:00
if err := mongo . SaveModel ( eventRecievedAudit ) ; err != nil {
log . WithError ( err ) . WithField ( "EventRecieved" , eventRecievedAudit ) . Error ( "Failed to save audit event. Logging here and resuming" )
}
2023-11-23 12:17:20 -05:00
//Check to see if we have scheduled a broadcast befre
broadcasts , err := mongo . FindAllBroadcastsByCorrelationId ( * uid , payload . Id )
if err != nil {
return errors . Join ( fmt . Errorf ( "Failed to find broadcasts for user: %s and CorrelationId: %s" , uid . Hex ( ) , payload . Id ) , err )
}
var result string
if len ( broadcasts ) > 0 {
//What do we do when we have already scheduled the broadcast
switch body . Name {
//If we get plan created event for this, return already scheduled error
case "services.v2.events.plan.created" :
return AlreadyScheduledBroadcast
//update the broadcast
case "services.v2.events.plan.updated" :
//TODO: Update Broadcast
err := updateBroadcastFromWebhook ( c , broadcasts , payload , ytClient , pcoClient )
if err != nil {
log . WithError ( err ) . Error ( "Failed to update broadcast from updated event" )
return err
}
result = UPDATED_BROADCAST
//delete the broadcast
case "services.v2.events.plan.destroyed" :
//TODO: Delete broadcast
err := deleteBroadcastFromWebhook ( c , broadcasts , payload , ytClient , pcoClient )
if err != nil {
log . WithError ( err ) . Error ( "Failed to delete broadcast from updated event" )
return err
}
result = DELETED_BROADCAST
default :
return UnknownEventErr
}
actionTaken := & models . ActionTaken {
UserId : * uid ,
TriggeringEvent : eventRecievedAudit . MongoId ( ) ,
Result : result ,
2023-11-24 03:01:38 -05:00
CorrelationId : payload . Id ,
2023-11-23 12:17:20 -05:00
VendorName : models . YOUTUBE_VENDOR_NAME ,
}
//save audit trail
2023-11-23 13:22:16 -05:00
err = mongo . SaveModels ( actionTaken )
2023-11-23 09:05:44 -05:00
if err != nil {
2023-11-23 12:17:20 -05:00
log . WithError ( err ) . Error ( "Failed to save broadcastModel and actionTaken" )
2023-11-23 09:05:44 -05:00
return err
}
2023-11-23 12:17:20 -05:00
} else {
//No broadcast is scheduled
//create the broadcast
var broadcast * youtube . LiveBroadcast
switch body . Name {
case "services.v2.events.plan.created" :
broadcast , err = scheduleNewBroadcastFromWebhook ( c , payload , ytClient , pcoClient )
if err != nil {
log . WithError ( err ) . Error ( "Failed to schedule broadcast from created event" )
return err
}
result = CREATED_BROADCAST
case "services.v2.events.plan.updated" :
broadcast , err = scheduleNewBroadcastFromWebhook ( c , payload , ytClient , pcoClient )
if err != nil {
log . WithError ( err ) . Error ( "Failed to schedule broadcast from updated event" )
return err
}
result = CREATED_BROADCAST
case "services.v2.events.plan.destroyed" :
return NoBroadcastToDelete
default :
return fmt . Errorf ( "Unkown event error: %s" , body . Name )
}
2023-11-16 20:15:30 -05:00
2023-11-23 12:17:20 -05:00
//build audit trail after action was taken
broadcastModel := & models . YoutubeBroadcast {
UserId : * uid ,
CorrelationId : payload . Id ,
Details : broadcast ,
}
2023-11-16 20:15:30 -05:00
2023-11-23 12:17:20 -05:00
actionTaken := & models . ActionTaken {
UserId : * uid ,
TriggeringEvent : eventRecievedAudit . MongoId ( ) ,
Result : result ,
2023-11-24 02:00:28 -05:00
CorrelationId : payload . Id ,
2023-11-23 12:17:20 -05:00
VendorName : models . YOUTUBE_VENDOR_NAME ,
}
2023-11-16 20:43:34 -05:00
2023-11-23 12:17:20 -05:00
//save audit trail
err = mongo . SaveModels ( broadcastModel , actionTaken )
if err != nil {
log . WithError ( err ) . Error ( "Failed to save broadcastModel and actionTaken" )
return err
}
2023-11-16 20:15:30 -05:00
}
2023-11-16 13:00:28 -05:00
2023-11-07 22:34:57 -05:00
return nil
}
2023-11-16 20:43:34 -05:00
func scheduleNewBroadcastFromWebhook ( c * gin . Context , plan * services . Plan , ytClient * youtube . Service , pcoClient * pco . PcoApiClient ) ( * youtube . LiveBroadcast , error ) {
times , err := pcoClient . GetPlanTimes ( plan . ServiceType . Id , plan . Id )
if err != nil {
return nil , err
}
2023-11-23 09:24:30 -05:00
startTime := times [ 0 ] . StartsAt
// endTime := times[len(times) - 1].EndsAt TODO: this will be used later
2023-11-23 12:17:20 -05:00
//if starttime is before now, skip with a passable error
if startTime . Before ( time . Now ( ) ) {
return nil , NotSchedulableTime
}
2023-11-23 10:09:18 -05:00
var title string
if plan . Title == "" {
title = "Live Stream Scheduled By Capstone"
} else {
title = plan . Title
}
2023-11-23 09:24:30 -05:00
2023-11-23 12:17:20 -05:00
log . Debugf ( "Trying to schedule time at: %s" , startTime . Format ( yt_helpers . ISO_8601 ) )
2023-11-23 10:09:18 -05:00
return yt_helpers . InsertBroadcast ( ytClient , title , startTime , yt_helpers . STATUS_PRIVATE )
2023-11-16 20:43:34 -05:00
}
2023-11-23 12:17:20 -05:00
func updateBroadcastFromWebhook ( c * gin . Context , broadcasts [ ] models . YoutubeBroadcast , plan * services . Plan , ytClient * youtube . Service , pcoClient * pco . PcoApiClient ) error {
times , err := pcoClient . GetPlanTimes ( plan . ServiceType . Id , plan . Id )
if err != nil {
return err
}
startTime := times [ 0 ] . StartsAt
// endTime := times[len(times) - 1].EndsAt TODO: this will be used later
//if starttime is before now, skip with a passable error
if startTime . Before ( time . Now ( ) ) {
return NotSchedulableTime
}
var title string
if plan . Title == "" {
title = "Live Stream Scheduled By Capstone"
} else {
title = plan . Title
}
//create list of errors to process all of the broadcasts and then error
errs := make ( [ ] error , 0 , len ( broadcasts ) )
bcs := make ( [ ] * models . YoutubeBroadcast , 0 , len ( broadcasts ) )
for index , broadcast := range broadcasts {
liveBroadcast , err := yt_helpers . UpdateBroadcast ( ytClient , broadcast . Details . Id , title , startTime , yt_helpers . STATUS_PRIVATE )
if err != nil {
errs = append ( errs , err )
} else {
broadcasts [ index ] . Details = liveBroadcast
bcs = append ( bcs , & broadcasts [ index ] )
}
}
if err := errors . Join ( errs ... ) ; err != nil {
return err
}
return db . SaveModelSlice ( mongo , bcs ... )
}
func deleteBroadcastFromWebhook ( c * gin . Context , broadcasts [ ] models . YoutubeBroadcast , plan * services . Plan , ytClient * youtube . Service , pcoClient * pco . PcoApiClient ) error {
errs := make ( [ ] error , 0 , len ( broadcasts ) )
bcs := make ( [ ] * models . YoutubeBroadcast , 0 , len ( broadcasts ) )
for index , broadcast := range broadcasts {
err := yt_helpers . DeleteBroadcast ( ytClient , broadcast . Details . Id )
if err != nil {
errs = append ( errs , err )
} else {
bcs = append ( bcs , & broadcasts [ index ] )
}
}
if err := errors . Join ( errs ... ) ; err != nil {
return err
}
return db . DeleteModelSlice ( mongo , bcs ... )
}