diff --git a/.middleware.air.toml b/.middleware.air.toml new file mode 100644 index 00000000..3b4d1446 --- /dev/null +++ b/.middleware.air.toml @@ -0,0 +1,33 @@ +root = "." +tmp_dir = "tmp" + +[build] +cmd = "go build -o ./bin/provider-middleware ./provider-middleware/." +bin = "bin/provider-middleware" +include_dir = ["backend", "provider-middleware"] +include_file = [] +exclude_dir = ["frontend", "config", "backend/tasks"] +exclude_regex = ["_test\\.go"] +exclude_unchanged = true + +log = "logs/middleware.air.log" +stop_on_error = true +send_interrupt = true +rerun_delay = 500 + +[log] +time = false +main_only = false + +[color] +main = "magenta" +watcher = "cyan" +build = "yellow" +runner = "green" + +[misc] +clean_on_exit = true + +[screen] +clear_on_rebuild = true +keep_scroll = true diff --git a/.tasks.air.toml b/.tasks.air.toml new file mode 100644 index 00000000..36303542 --- /dev/null +++ b/.tasks.air.toml @@ -0,0 +1,33 @@ +root = "." +tmp_dir = "tmp" + +[build] +cmd = "go build -o ./bin/cron_tasks ./backend/tasks/." +bin = "bin/cron_tasks" +include_dir = ["backend"] +include_file = [] +exclude_dir = ["frontend", "provider-middleware"] +exclude_regex = ["_test\\.go"] +exclude_unchanged = true + +log = "logs/tasks.air.log" +stop_on_error = true +send_interrupt = true +rerun_delay = 500 + +[log] +time = false +main_only = false + +[color] +main = "magenta" +watcher = "cyan" +build = "yellow" +runner = "green" + +[misc] +clean_on_exit = true + +[screen] +clear_on_rebuild = true +keep_scroll = true diff --git a/backend/migrations/00010_add_open_content_provider_id_to_tasks.sql b/backend/migrations/00010_add_open_content_provider_id_to_tasks.sql new file mode 100644 index 00000000..039e68c5 --- /dev/null +++ b/backend/migrations/00010_add_open_content_provider_id_to_tasks.sql @@ -0,0 +1,13 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE public.runnable_tasks +ALTER COLUMN provider_platform_id DROP NOT NULL, +ADD COLUMN open_content_provider_id integer REFERENCES open_content_providers(id); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.runnable_tasks +ALTER COLUMN provider_platform_id SET NOT NULL, +DROP COLUMN open_content_provider_id; +-- +goose StatementEnd diff --git a/backend/migrations/00011_add_base_url_to_open_content_providers.sql b/backend/migrations/00011_add_base_url_to_open_content_providers.sql new file mode 100644 index 00000000..26ced964 --- /dev/null +++ b/backend/migrations/00011_add_base_url_to_open_content_providers.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE public.open_content_providers RENAME COLUMN url TO base_url; +ALTER TABLE public.libraries RENAME COLUMN url TO path; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.open_content_providers RENAME COLUMN base_url TO url; +ALTER TABLE public.libraries RENAME COLUMN path TO url; +-- +goose StatementEnd diff --git a/backend/migrations/00012_increase_languages_length_libraries.sql b/backend/migrations/00012_increase_languages_length_libraries.sql new file mode 100644 index 00000000..6c14f45b --- /dev/null +++ b/backend/migrations/00012_increase_languages_length_libraries.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE public.libraries ALTER COLUMN language TYPE VARCHAR(512); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE public.libraries ALTER COLUMN language TYPE VARCHAR(255); +-- +goose StatementEnd diff --git a/backend/seeder/main.go b/backend/seeder/main.go index 5c73c9ec..3fdc8ff2 100644 --- a/backend/seeder/main.go +++ b/backend/seeder/main.go @@ -70,7 +70,7 @@ func seedTestData(db *gorm.DB) { State: models.Enabled, AccessKey: "testing_key_replace_me", }, { - Name: "kolibri_testing", + Name: "Kolibri", BaseUrl: "https://kolibri.staging.unlockedlabs.xyz", AccountID: "1234567890", Type: models.Kolibri, @@ -78,23 +78,18 @@ func seedTestData(db *gorm.DB) { AccessKey: "testing_key_replace_me", }} for idx := range platforms { - accessKey, err := platforms[idx].EncryptAccessKey() - platforms[idx].AccessKey = accessKey - if err != nil { - log.Printf("Failed to create access key") - } if err := db.Create(&platforms[idx]).Error; err != nil { log.Printf("Failed to create platform: %v", err) } } kiwix := models.OpenContentProvider{ - Url: "unlockedlabs.org", - Name: "Kiwix", + BaseUrl: "https://library.kiwix.org", + Name: models.Kiwix, Thumbnail: "https://images.fineartamerica.com/images/artworkimages/mediumlarge/3/llamas-wearing-party-hats-in-a-circle-looking-down-john-daniels.jpg", CurrentlyEnabled: true, Description: "Kiwix open content provider", } - log.Printf("Creating Open Content Provider %s", kiwix.Url) + log.Printf("Creating Open Content Provider %s", kiwix.BaseUrl) if err := db.Create(&kiwix).Error; err != nil { log.Printf("Failed to create open content provider: %v", err) } @@ -105,10 +100,9 @@ func seedTestData(db *gorm.DB) { Name: "TED ted connects", Language: models.StringPtr("eng,spa,ara"), Description: models.StringPtr("A collection of TED videos about ted connects"), - Url: "/content/ted_mul_ted-connects_2024-08", + Path: "/content/ted_mul_ted-connects_2024-08", ImageUrl: models.StringPtr("/catalog/v2/illustration/67440563-a62b-fabe-415c-4c3ee4546f78/?size=48"), VisibilityStatus: true, - OpenContentProvider: &kiwix, }, { OpenContentProviderID: kiwix.ID, @@ -116,10 +110,9 @@ func seedTestData(db *gorm.DB) { Name: "Python Documentation", Language: models.StringPtr("eng"), Description: models.StringPtr("All documentation for Python"), - Url: "/content/docs.python.org_en_2024-09", + Path: "/content/docs.python.org_en_2024-09", ImageUrl: models.StringPtr("/catalog/v2/illustration/84812c13-fa65-feb7-c206-4f22cc2e0f9a/?size=48"), VisibilityStatus: true, - OpenContentProvider: &kiwix, }, { OpenContentProviderID: kiwix.ID, @@ -127,10 +120,9 @@ func seedTestData(db *gorm.DB) { Name: "Finiki", Language: models.StringPtr("eng"), Description: models.StringPtr("The Canadian financial wiki"), - Url: "/content/finiki_en_all_maxi_2024-06", + Path: "/content/finiki_en_all_maxi_2024-06", ImageUrl: models.StringPtr("/catalog/v2/illustration/19e6fe12-09a9-0a38-5be4-71c0eba0a72d/?size=48"), VisibilityStatus: true, - OpenContentProvider: &kiwix, }} for idx := range kiwixLibraries { log.Printf("Creating library %s", kiwixLibraries[idx].Name) diff --git a/backend/src/database/open_content.go b/backend/src/database/open_content.go index 7001e1a7..1050cd3e 100644 --- a/backend/src/database/open_content.go +++ b/backend/src/database/open_content.go @@ -33,7 +33,7 @@ func (db *DB) ToggleContentProvider(id int) error { func (db *DB) CreateContentProvider(url, thumbnail, description string, id int) error { provider := models.OpenContentProvider{ - Url: url, + BaseUrl: url, Thumbnail: thumbnail, Description: description, } diff --git a/backend/src/database/provider_platforms.go b/backend/src/database/provider_platforms.go index 5b0e77a0..db19eac2 100644 --- a/backend/src/database/provider_platforms.go +++ b/backend/src/database/provider_platforms.go @@ -15,7 +15,6 @@ func (db *DB) GetAllProviderPlatforms(page, perPage int) (int64, []models.Provid Offset(offset).Limit(perPage).Find(&platforms).Error; err != nil { return 0, nil, newGetRecordsDBError(err, "provider_platforms") } - toReturn := iterMap(func(prov models.ProviderPlatform) models.ProviderPlatform { if prov.OidcClient != nil { prov.OidcID = prov.OidcClient.ID @@ -68,11 +67,11 @@ func (db *DB) CreateProviderPlatform(platform *models.ProviderPlatform) (*models } if platform.Type == models.Kolibri { contentProv := models.OpenContentProvider{ - Url: platform.BaseUrl, + BaseUrl: platform.BaseUrl, ProviderPlatformID: &platform.ID, CurrentlyEnabled: true, Description: models.KolibriDescription, - Thumbnail: "https://learningequality.org/static/assets/kolibri-ecosystem-logos/blob-logo.svg", + Thumbnail: models.KolibriThumbnailUrl, } if err := db.Create(&contentProv).Error; err != nil { log.Errorln("unable to create relevant content provider for new kolibri instance") diff --git a/backend/src/handlers/auth.go b/backend/src/handlers/auth.go index 4cc12659..65f7f478 100644 --- a/backend/src/handlers/auth.go +++ b/backend/src/handlers/auth.go @@ -36,7 +36,6 @@ func (srv *Server) registerAuthRoutes() { srv.Mux.Handle("POST /api/reset-password", srv.applyMiddleware(srv.handleResetPassword)) /* only use auth middleware, user activity bloats the database + results */ srv.Mux.Handle("GET /api/auth", srv.applyMiddleware(srv.handleCheckAuth)) - srv.Mux.Handle("PUT /api/admin/facility-context/{id}", srv.applyAdminMiddleware(srv.handleChangeAdminFacility)) } func (claims *Claims) getTraits() map[string]interface{} { @@ -80,21 +79,6 @@ func (s *Server) authMiddleware(next http.Handler) http.Handler { }) } -func (srv *Server) handleChangeAdminFacility(w http.ResponseWriter, r *http.Request, log sLog) error { - id, err := strconv.Atoi(r.PathValue("id")) - if err != nil { - return newInvalidIdServiceError(err, "facility ID") - } - claims := r.Context().Value(ClaimsKey).(*Claims) - claims.FacilityID = uint(id) - if err := srv.updateUserTraitsInKratos(claims); err != nil { - log.add("facilityId", id) - return newInternalServerServiceError(err, "error updating user traits in kratos") - } - w.WriteHeader(http.StatusOK) - return nil -} - func (s *Server) clearKratosCookies(w http.ResponseWriter, r *http.Request) { cookies := r.Cookies() for _, cookie := range cookies { diff --git a/backend/src/handlers/facilities_handler.go b/backend/src/handlers/facilities_handler.go index 448600a9..02e8652d 100644 --- a/backend/src/handlers/facilities_handler.go +++ b/backend/src/handlers/facilities_handler.go @@ -13,6 +13,7 @@ func (srv *Server) registerFacilitiesRoutes() { srv.Mux.Handle("POST /api/facilities", srv.applyAdminMiddleware(srv.handleCreateFacility)) srv.Mux.Handle("DELETE /api/facilities/{id}", srv.applyAdminMiddleware(srv.handleDeleteFacility)) srv.Mux.Handle("PATCH /api/facilities/{id}", srv.applyAdminMiddleware(srv.handleUpdateFacility)) + srv.Mux.Handle("PUT /api/admin/facility-context/{id}", srv.applyAdminMiddleware(srv.handleChangeAdminFacility)) } func (srv *Server) handleIndexFacilities(w http.ResponseWriter, r *http.Request, log sLog) error { @@ -37,6 +38,21 @@ func (srv *Server) handleShowFacility(w http.ResponseWriter, r *http.Request, lo return writeJsonResponse(w, http.StatusOK, facility) } +func (srv *Server) handleChangeAdminFacility(w http.ResponseWriter, r *http.Request, log sLog) error { + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + return newInvalidIdServiceError(err, "facility ID") + } + claims := r.Context().Value(ClaimsKey).(*Claims) + claims.FacilityID = uint(id) + if err := srv.updateUserTraitsInKratos(claims); err != nil { + log.add("facilityId", id) + return newInternalServerServiceError(err, "error updating user traits in kratos") + } + w.WriteHeader(http.StatusOK) + return nil +} + func (srv *Server) handleCreateFacility(w http.ResponseWriter, r *http.Request, log sLog) error { var facility models.Facility err := json.NewDecoder(r.Body).Decode(&facility) diff --git a/backend/src/models/jobs.go b/backend/src/models/jobs.go index 7432f6d8..8212c2fb 100644 --- a/backend/src/models/jobs.go +++ b/backend/src/models/jobs.go @@ -25,16 +25,28 @@ type ( func (CronJob) TableName() string { return "cron_jobs" } +func (cj *CronJob) BeforeCreate(tx *gorm.DB) error { + if len(cj.ID) == 0 { + cj.ID = uuid.NewString() + } + if len(cj.Schedule) == 0 { + cj.Schedule = os.Getenv("MIDDLEWARE_CRON_SCHEDULE") + } + return nil +} + type RunnableTask struct { - ID uint `gorm:"primaryKey" json:"id"` - JobID string `gorm:"size 50" json:"job_id"` - Parameters map[string]interface{} `gorm:"-" json:"parameters"` - LastRun time.Time `json:"last_run"` - ProviderPlatformID uint `json:"provider_platform_id"` - Status JobStatus `json:"status"` + ID uint `gorm:"primaryKey" json:"id"` + JobID string `gorm:"size 50" json:"job_id"` + Parameters map[string]interface{} `gorm:"-" json:"parameters"` + LastRun time.Time `json:"last_run"` + ProviderPlatformID *uint `json:"provider_platform_id"` + OpenContentProviderID *uint `json:"open_content_provider_id"` + Status JobStatus `json:"status"` - Provider *ProviderPlatform `gorm:"foreignKey:ProviderPlatformID" json:"-"` - Job *CronJob `gorm:"foreignKey:JobID" json:"-"` + Provider *ProviderPlatform `gorm:"foreignKey:ProviderPlatformID" json:"-"` + ContentProvider *OpenContentProvider `gorm:"foreignKey:OpenContentProviderID" json:"-"` + Job *CronJob `gorm:"foreignKey:JobID" json:"-"` } func (RunnableTask) TableName() string { return "runnable_tasks" } @@ -45,19 +57,28 @@ const ( GetActivityJob JobType = "get_activity" // GetOutcomesJob JobType = "get_outcomes" + ScrapeKiwixJob JobType = "scrape_kiwix" + StatusPending JobStatus = "pending" StatusRunning JobStatus = "running" ) -func (jt JobType) GetParams(db *gorm.DB, provId uint) (map[string]interface{}, error) { +// provider id can be nil pointer +func (jt JobType) GetParams(db *gorm.DB, provId *uint, jobId string) (map[string]interface{}, error) { var skip bool + if jt == ScrapeKiwixJob { + return map[string]interface{}{ + "open_content_provider_id": *provId, + "job_id": jobId, + }, nil + } users := []map[string]interface{}{} - if err := db.Model(ProviderUserMapping{}).Select("user_id, external_user_id").Find(&users, "provider_platform_id = ?", provId).Error; err != nil { + if err := db.Model(ProviderUserMapping{}).Select("user_id, external_user_id").Joins("JOIN users u on provider_user_mappings.user_id = u.id").Find(&users, "provider_platform_id = ? AND u.role = 'student'", *provId).Error; err != nil { log.Errorf("failed to fetch users: %v", err) skip = true } courses := []map[string]interface{}{} - if err := db.Model(Course{}).Select("id as course_id, external_id as external_course_id").Find(&courses, "provider_platform_id = ?", provId).Error; err != nil { + if err := db.Model(Course{}).Select("id as course_id, external_id as external_course_id").Find(&courses, "provider_platform_id = ?", *provId).Error; err != nil { log.Errorf("failed to fetch courses: %v", err) skip = true } @@ -69,39 +90,43 @@ func (jt JobType) GetParams(db *gorm.DB, provId uint) (map[string]interface{}, e return map[string]interface{}{ "user_mappings": users, "courses": courses, - "provider_platform_id": provId, + "provider_platform_id": *provId, "job_type": jt, + "job_id": jobId, }, nil case GetCoursesJob: return map[string]interface{}{ "provider_platform_id": provId, "job_type": jt, + "job_id": jobId, }, nil case GetActivityJob: if skip { return nil, errors.New("no users or courses found for provider platform") } return map[string]interface{}{ - "provider_platform_id": provId, + "provider_platform_id": *provId, "courses": courses, "user_mappings": users, "job_type": jt, + "job_id": jobId, }, nil // case GetOutcomesJob: // if skip { // return nil, errors.New("no users or courses found for provider platform") // } // return map[string]interface{}{ - // "provider_platform_id": provId, + // "provider_platform_id": *provId, // "user_mappings": users, // "courses": courses, // "job_type": jt, // }, nil } - return nil, nil + return nil, errors.New("job type not found") } -var AllDefaultJobs = []JobType{GetCoursesJob, GetMilestonesJob, GetActivityJob /* GetOutcomesJob */} +var AllDefaultProviderJobs = []JobType{GetCoursesJob, GetMilestonesJob, GetActivityJob /* GetOutcomesJob */} +var AllOtherJobs = []JobType{ScrapeKiwixJob} func NewCronJob(name JobType) *CronJob { return &CronJob{ diff --git a/backend/src/models/library.go b/backend/src/models/library.go index b29e1a25..4fec1d31 100644 --- a/backend/src/models/library.go +++ b/backend/src/models/library.go @@ -7,7 +7,7 @@ type Library struct { Name string `gorm:"size:255;not null" json:"name"` Language *string `gorm:"size:255" json:"language"` Description *string `json:"description"` - Url string `gorm:"not null" json:"url"` + Path string `gorm:"not null" json:"url"` ImageUrl *string `json:"image_url"` VisibilityStatus bool `gorm:"default:false;not null" json:"visibility_status"` diff --git a/backend/src/models/open_content.go b/backend/src/models/open_content.go index feb70eeb..a8844c30 100644 --- a/backend/src/models/open_content.go +++ b/backend/src/models/open_content.go @@ -1,6 +1,7 @@ package models import ( + "fmt" "strings" "gorm.io/gorm" @@ -8,22 +9,26 @@ import ( type OpenContentProvider struct { DatabaseFields - Name string `gorm:"size:255" json:"name"` - Url string `gorm:"size:255;not null;unique" json:"url"` - ProviderPlatformID *uint `json:"provider_platform_id"` - Thumbnail string `json:"thumbnail_url"` - CurrentlyEnabled bool `json:"currently_enabled"` - Description string `json:"description"` - ProviderPlatform *ProviderPlatform `gorm:"foreignKey:ProviderPlatformID;constraint:OnDelete SET NULL" json:"-"` + Name string `gorm:"size:255" json:"name"` + BaseUrl string `gorm:"size:255;not null;unique" json:"url"` + ProviderPlatformID *uint `json:"provider_platform_id"` + Thumbnail string `json:"thumbnail_url"` + CurrentlyEnabled bool `json:"currently_enabled"` + Description string `json:"description"` + + ProviderPlatform *ProviderPlatform `gorm:"foreignKey:ProviderPlatformID;constraint:OnDelete SET NULL" json:"-"` + Tasks []RunnableTask `gorm:"foreignKey:OpenContentProviderID" json:"-"` } const ( - KolibriDescription string = "Kolibri provides an extensive library of educational content suitable for all learning levels." + KolibriThumbnailUrl string = "https://learningequality.org/static/assets/kolibri-ecosystem-logos/blob-logo.svg" + Kiwix string = "kiwix" + KolibriDescription string = "Kolibri provides an extensive library of educational content suitable for all learning levels." ) func (cp *OpenContentProvider) BeforeCreate(tx *gorm.DB) error { - if !strings.HasPrefix(cp.Url, "http") { - cp.Url = "https://" + cp.Url + if !strings.HasPrefix(cp.BaseUrl, "http") { + cp.BaseUrl = fmt.Sprintf("https://%s", cp.BaseUrl) } return nil } diff --git a/backend/src/models/provider_platforms.go b/backend/src/models/provider_platforms.go index 22680eb3..cbf23b49 100644 --- a/backend/src/models/provider_platforms.go +++ b/backend/src/models/provider_platforms.go @@ -122,11 +122,11 @@ func (provider *ProviderPlatform) GetDefaultRedirectURI() []string { return []string{} } -func (prov *ProviderPlatform) GetDefaultCronJobs() []*CronJob { - // this is only here because at some point this may be different per provider.Type - jobs := []*CronJob{} - for _, job := range AllDefaultJobs { - jobs = append(jobs, NewCronJob(job)) +func (prov *ProviderPlatform) GetDefaultCronJobs() []JobType { + jobs := []JobType{} + // at some point these may differ per provider, for now they all return the default jobs + for _, job := range AllDefaultProviderJobs { + jobs = append(jobs, job) log.WithFields(log.Fields{"job": job, "provider": prov.Name}).Info("Job added for provider") } return jobs diff --git a/backend/src/models/resource.go b/backend/src/models/resource.go index d1326909..d1f89f1a 100644 --- a/backend/src/models/resource.go +++ b/backend/src/models/resource.go @@ -1,6 +1,8 @@ package models -import "reflect" +import ( + "reflect" +) type PaginatedResource[T any] struct { Message string `json:"message"` diff --git a/backend/src/service.go b/backend/src/service.go index b5f8e715..e8de966a 100644 --- a/backend/src/service.go +++ b/backend/src/service.go @@ -56,7 +56,6 @@ func GetProviderService(prov *models.ProviderPlatform, client *http.Client) (*Pr func (serv *ProviderService) Request(url string) (*http.Request, error) { log.Println("Init request for provider service") - serviceKey := os.Getenv("PROVIDER_SERVICE_KEY") finalUrl := serv.ServiceURL + url + "?id=" + strconv.Itoa(int(serv.ProviderPlatformID)) log.Printf("url: %s \n", finalUrl) request, err := http.NewRequest("GET", finalUrl, nil) @@ -64,7 +63,6 @@ func (serv *ProviderService) Request(url string) (*http.Request, error) { log.Printf("error creating request %v", err.Error()) return nil, err } - request.Header.Set("Authorization", serviceKey) log.Printf("request: %v", request) return request, nil } diff --git a/backend/tasks/database.go b/backend/tasks/database.go index 9e5267ca..a01cd914 100644 --- a/backend/tasks/database.go +++ b/backend/tasks/database.go @@ -13,6 +13,12 @@ import ( "gorm.io/gorm" ) +const ( + GetCourses string = "tasks.get_courses" + GetActivity string = "tasks.get_activity" + GetMilestones string = "tasks.get_milestones" +) + func initDB() *gorm.DB { log.Info("initializing database") dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=allow", @@ -27,13 +33,14 @@ func initDB() *gorm.DB { return db } -func (jr *JobRunner) createIfNotExists(cj *models.CronJob, prov *models.ProviderPlatform) error { - if err := jr.db.Where("name = ?", cj.Name).FirstOrCreate(cj).Error; err != nil { +func (jr *JobRunner) createIfNotExists(cj models.JobType) (*models.CronJob, error) { + job := models.CronJob{Name: string(cj)} + if err := jr.db.Model(&models.CronJob{}).Where("name = ?", cj).FirstOrCreate(&job).Error; err != nil { log.Errorf("failed to find or create job: %v", err) - return err + return nil, err } - log.Infof("CronJob %s has ID: %s", cj.Name, cj.ID) - return nil + log.Infof("CronJob %s has ID: %s", job.Name, job.ID) + return &job, nil } func (jr *JobRunner) checkFirstRun(prov *models.ProviderPlatform) error { @@ -44,32 +51,20 @@ func (jr *JobRunner) checkFirstRun(prov *models.ProviderPlatform) error { } if len(courses) == 0 { done := make(chan bool) - params := map[string]interface{}{ "provider_platform_id": prov.ID, } - jobs := prov.GetDefaultCronJobs() - var courseJob *models.CronJob - for _, job := range jobs { - if job.Name == string(models.GetCoursesJob) { - courseJob = job - break - } - } - if courseJob == nil { - return fmt.Errorf("GetCoursesJob not found in default jobs") - } - if err := jr.createIfNotExists(courseJob, prov); err != nil { + created, err := jr.createIfNotExists(models.GetCoursesJob) + if err != nil { log.Errorf("Failed to create or find CronJob: %v", err) return err } - params["job_id"] = courseJob.ID - + params["job_id"] = created.ID if err := jr.db.Create(&models.RunnableTask{ - JobID: courseJob.ID, + JobID: created.ID, Parameters: params, Status: models.StatusPending, - ProviderPlatformID: prov.ID, + ProviderPlatformID: &prov.ID, }).Error; err != nil { log.Errorf("failed to create task: %v", err) return err @@ -81,7 +76,7 @@ func (jr *JobRunner) checkFirstRun(prov *models.ProviderPlatform) error { log.Errorf("failed to unmarshal completion message: %v", err) return } - if completedParams["job_id"] == courseJob.ID { + if completedParams["job_id"] == created.ID { done <- true } }) @@ -96,13 +91,13 @@ func (jr *JobRunner) checkFirstRun(prov *models.ProviderPlatform) error { } }() - params["job_id"] = courseJob.ID + params["job_id"] = created.ID body, err := json.Marshal(¶ms) if err != nil { log.Errorf("failed to marshal params: %v", err) return err } - msg := nats.NewMsg("tasks.get_courses") + msg := nats.NewMsg(GetCourses) msg.Data = body err = jr.nats.PublishMsg(msg) if err != nil { diff --git a/backend/tasks/dev.Dockerfile b/backend/tasks/dev.Dockerfile new file mode 100644 index 00000000..e998f74e --- /dev/null +++ b/backend/tasks/dev.Dockerfile @@ -0,0 +1,9 @@ +FROM golang:1.23-alpine + +WORKDIR /app + +RUN go install github.com/air-verse/air@latest + +COPY backend/tasks/go.mod backend/tasks/go.sum ./ +RUN go mod download +CMD ["air", "-c", ".tasks.air.toml"] diff --git a/backend/tasks/main.go b/backend/tasks/main.go index 32c98fcc..6b31b0f6 100644 --- a/backend/tasks/main.go +++ b/backend/tasks/main.go @@ -22,19 +22,21 @@ func main() { log.Fatalf("Failed to create scheduler: %v", err) return } - newJob, err := scheduler.NewJob(gocron.CronJob(os.Getenv("MIDDLEWARE_CRON_SCHEDULE"), false), gocron.NewTask(runner.execute)) + tasks, err := runner.generateTasks() if err != nil { - log.Fatalf("Failed to create job: %v", err) + log.Fatalf("failed to generate tasks: %v", err) + return } - scheduler.Start() - err = newJob.RunNow() - if err != nil { - log.Errorln("Failed to run job now: ", err) + for _, task := range tasks { + _, err := scheduler.NewJob(gocron.CronJob(task.Job.Schedule, false), gocron.NewTask(runner.runTask, task)) + if err != nil { + log.Errorf("Failed to create job: %v", err) + continue + } } - log.Infof("Scheduler started, running %s", newJob.ID()) + scheduler.Start() shutdown := make(chan os.Signal, 1) signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM) - <-shutdown } diff --git a/backend/tasks/scheduler.go b/backend/tasks/scheduler.go index 52a31cc6..58919594 100644 --- a/backend/tasks/scheduler.go +++ b/backend/tasks/scheduler.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "sync" - "time" "github.com/nats-io/nats.go" log "github.com/sirupsen/logrus" @@ -36,61 +35,104 @@ func newJobRunner() *JobRunner { } func (jr *JobRunner) generateTasks() ([]models.RunnableTask, error) { + allTasks := make([]models.RunnableTask, 0) + if otherTasks, err := jr.generateOpenContentProviderTasks(); err == nil { + allTasks = append(allTasks, otherTasks...) + } else { + log.Println("Failed to generate other tasks") + } + if providerTasks, err := jr.generateProviderTasks(); err == nil { + allTasks = append(allTasks, providerTasks...) + } else { + log.Println("Failed to generate provider tasks") + } + return allTasks, nil +} + +func (jr *JobRunner) generateOpenContentProviderTasks() ([]models.RunnableTask, error) { + otherTasks := make([]models.RunnableTask, 0) + var id *uint + for _, jobType := range models.AllOtherJobs { + job := models.CronJob{Name: string(jobType)} + switch jobType { + case models.ScrapeKiwixJob: + if err := jr.db.Model(&models.OpenContentProvider{}).Select("id").Where("name = ?", models.Kiwix).Scan(&id).Error; err != nil { + log.Errorf("failed to fetch kiwix provider: %v", err) + continue + } + if err := jr.db.Model(&models.CronJob{}).Where("name = ?", string(job.Name)).FirstOrCreate(&job).Error; err != nil { + log.Errorf("failed to create job: %v", err) + continue + } + default: + continue + } + task := models.RunnableTask{OpenContentProviderID: id, JobID: job.ID} + if err := jr.intoOpenContentTask(&job, id, &task); err != nil { + log.Errorf("failed to create task: %v", err) + return nil, err + } + otherTasks = append(otherTasks, task) + } + return otherTasks, nil +} + +func (jr *JobRunner) generateProviderTasks() ([]models.RunnableTask, error) { var providers []models.ProviderPlatform if err := jr.db.Find(&providers, "state = 'enabled'").Error; err != nil { log.Errorln("failed to fetch all provider platforms") return nil, err } log.Infof("Found %d active providers", len(providers)) - // TODO: make this dynamic, but for now all providers use the same jobs - var tasksToRun []models.RunnableTask + tasksToRun := make([]models.RunnableTask, 0) for _, provider := range providers { provJobs := provider.GetDefaultCronJobs() - log.Debug("provJobs: ", provJobs) for idx := range provJobs { log.Infof("Checking job: %v", provJobs[idx]) - if err := jr.createIfNotExists(provJobs[idx], &provider); err != nil { + created, err := jr.createIfNotExists(models.JobType(provJobs[idx])) + if err != nil { log.Errorf("failed to create job: %v", err) return nil, err } - task, err := jr.intoTask(&provider, provJobs[idx]) + newTask := models.RunnableTask{JobID: created.ID, ProviderPlatformID: &provider.ID, Status: models.StatusPending} + err = jr.intoProviderPlatformTask(created, &provider.ID, &newTask) if err != nil { log.Errorf("failed to create task: %v", err) return nil, err } - log.Debugf("generated task: %v", task) - tasksToRun = append(tasksToRun, *task) + tasksToRun = append(tasksToRun, newTask) } - log.Debugf("Generated %d tasks for provider: %s", len(tasksToRun), provider.Name) } - log.Infof("Generated %d tasks", len(tasksToRun)) + log.Infof("Generated %d total tasks for %d providers", len(tasksToRun), len(providers)) return tasksToRun, nil } -func (jr *JobRunner) intoTask(prov *models.ProviderPlatform, cj *models.CronJob) (*models.RunnableTask, error) { - task := models.RunnableTask{} - err := jr.db.Model(&models.RunnableTask{}).First(&task, "provider_platform_id = ? AND job_id = ?", prov.ID, cj.ID).Error - if err != nil { - // Record not found, create a new task - task = models.RunnableTask{ - ProviderPlatformID: prov.ID, - JobID: cj.ID, - Status: models.StatusPending, - LastRun: time.Now().AddDate(0, -6, 0), - } - if err := jr.db.Create(&task).Error; err != nil { - log.Errorf("failed to create task: %v", err) - return nil, err - } +func (jr *JobRunner) intoProviderPlatformTask(cj *models.CronJob, provId *uint, task *models.RunnableTask) error { + if err := jr.db.Model(&models.RunnableTask{}).Preload("Job").Where(models.RunnableTask{ProviderPlatformID: provId, JobID: cj.ID}).FirstOrCreate(&task).Error; err != nil { + log.Errorf("failed to create task for job: %v. error: %v", cj.Name, err) + return err } - params, err := models.JobType(cj.Name).GetParams(jr.db, prov.ID) + params, err := models.JobType(cj.Name).GetParams(jr.db, provId, cj.ID) if err != nil { log.Errorf("failed to get params for job: %v", err) - return nil, err + return err + } + task.Parameters = params + return nil +} + +func (jr *JobRunner) intoOpenContentTask(cj *models.CronJob, provId *uint, task *models.RunnableTask) error { + if err := jr.db.Model(&models.RunnableTask{}).Preload("Job").Where("job_id = ? AND open_content_provider_id = ?", cj.ID, provId).FirstOrCreate(&task).Error; err != nil { + log.Errorln("failed to create non-provider task from cronjob") + return err + } + params, err := models.JobType(cj.Name).GetParams(jr.db, provId, cj.ID) + if err != nil { + log.Errorln("failed to get params for non-provider job") + return err } - params["job_id"] = cj.ID task.Parameters = params - return &task, nil + return nil } func (jr *JobRunner) runTask(task *models.RunnableTask) error { diff --git a/canvas-seeder/.env.example b/canvas-seeder/.env.example deleted file mode 100644 index 6fe8f8f6..00000000 --- a/canvas-seeder/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -API_TOKEN={YOUR_API_TOKEN} -CANVAS_URL=https://my.canvas.instance.edu -TAB_FILE_PATH=./credentials.tab -TARGET_ASSIGNMENTS=10 -TARGET_COURSES=3 -TARGET_USERS=10 diff --git a/canvas-seeder/.gitignore b/canvas-seeder/.gitignore deleted file mode 100644 index cb7fc3c7..00000000 --- a/canvas-seeder/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.env -credentials.tab -node_modules \ No newline at end of file diff --git a/canvas-seeder/activity.js b/canvas-seeder/activity.js deleted file mode 100644 index 5d619825..00000000 --- a/canvas-seeder/activity.js +++ /dev/null @@ -1,101 +0,0 @@ -const puppeteer = require('puppeteer'); -const fs = require('fs'); - -const BASE_URL = process.env.CANVAS_BASE_URL || 'https://staging.canvas.unlockedlabs.xyz'; -const COURSES_API_URL = `${BASE_URL}/api/v1/courses`; -const LOGIN_URL = `${BASE_URL}/login/canvas`; -const LOGOUT_URL = `${BASE_URL}/logout`; -const USER_FILE = './users.txt'; -const MAX_MINUTES = parseInt(process.env.MAX_MINUTES || '5'); - -function readUsers() { - return fs.readFileSync(USER_FILE, 'utf-8') - .split('\n') - .filter(Boolean) - .map(line => { - const [username, password] = line.split(';'); - return { username, password }; - }); -} - -function getRandomWaitTime(maxMinutes) { - const minMilliseconds = 1 * 60 * 1000; - const maxMilliseconds = maxMinutes * 60 * 1000; - return Math.floor(Math.random() * (maxMilliseconds - minMilliseconds + 1)) + minMilliseconds; -} - -async function waitForTimeout(page, timeout) { - await page.evaluate((timeout) => { - return new Promise(resolve => setTimeout(resolve, timeout)); - }, timeout); -} - -async function fetchEnrolledCourses(page) { - try { - const courses = await page.evaluate(async (apiUrl) => { - const response = await fetch(apiUrl, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - credentials: 'same-origin' - }); - if (!response.ok) throw new Error('Failed to fetch courses'); - return await response.json(); - }, COURSES_API_URL); - console.log(courses); - return courses.map(course => course.id); - } catch (error) { - console.error('Error fetching courses:', error); - return []; - } -} - -async function visitCourses(page, courseIds) { - for (const courseId of courseIds) { - const courseUrl = `${BASE_URL}/courses/${courseId}`; - console.log(`Visiting course: ${courseUrl}`); - await page.goto(courseUrl); - const waitTime = getRandomWaitTime(MAX_MINUTES); - console.log(`Waiting for ${waitTime / 1000 / 60} minutes on course ${courseId}`); - await waitForTimeout(page, waitTime); - } -} - -async function loginAndFetchCourses(page, username, password) { - try { - await page.goto(LOGIN_URL); - await page.type('#pseudonym_session_unique_id', username); - await page.type('#pseudonym_session_password', password); - - await page.click('input[name="commit"]'), - await page.waitForNavigation() - - console.log(`Logged in as ${username}`); - const courseIds = await fetchEnrolledCourses(page); - - await visitCourses(page, courseIds).then(() => { - console.log(`Visited courses for ${username}`); - }).catch((error) => { - console.error(`Error visiting courses for ${username}:`, error); - }); - - await page.goto(LOGOUT_URL); - console.log(`Logged out ${username}`); - } catch (error) { - console.error(`Error with user ${username}:`, error); - } -} - -(async () => { - const browser = await puppeteer.launch({ headless: false }); - const users = readUsers(); - - for (const { username, password } of users) { - const page = await browser.newPage(); - await loginAndFetchCourses(page, username, password); - await page.close(); - } - await browser.close(); -})(); diff --git a/canvas-seeder/go.mod b/canvas-seeder/go.mod deleted file mode 100644 index afea78b6..00000000 --- a/canvas-seeder/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module canvas_seeder - -go 1.22.3 - -require ( - github.com/brianvoe/gofakeit/v6 v6.28.0 - github.com/joho/godotenv v1.5.1 -) diff --git a/canvas-seeder/go.sum b/canvas-seeder/go.sum deleted file mode 100644 index 7cac7b2d..00000000 --- a/canvas-seeder/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= -github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/canvas-seeder/index.js b/canvas-seeder/index.js deleted file mode 100644 index 7e764cd1..00000000 --- a/canvas-seeder/index.js +++ /dev/null @@ -1,133 +0,0 @@ -const axios = require('axios'); -const dotenv = require('dotenv'); -const { faker } = require('@faker-js/faker'); -const fs = require('fs'); -const path = require('path'); -const puppeteer = require('puppeteer'); - -async function retrieveCourses(apiToken, canvasURL) { - try { - const endPoint = "/api/v1/accounts/self/courses"; - const url = canvasURL + endPoint; - - const response = await axios.get(url, { - headers: { - 'Authorization': `Bearer ${apiToken}` - } - }); - - return response.data; - } catch (error) { - throw new Error(`Error retrieving courses: ${error.message}`); - } -} - -// Specify the path to the .env file for the local directory -const envFilePath = path.resolve(__dirname, '.env'); - -// Load the .env file -const result = dotenv.config({ path: envFilePath }); - -if (result.error) { - console.error('Error loading .env file:', result.error); -} else { - console.log('Successfully loaded .env file'); -} - -async function postDiscussion(username, password, courseId, index) { - const browser = await puppeteer.launch({ headless: true }); - const page = await browser.newPage(); - console.log('postDiscussion Course Id:', courseId); - try { - // Navigate to the Canvas login page - await page.goto(process.env.CANVAS_URL); - - // Perform login - // Wait for the first textbox to become visible and type username - await page.waitForSelector('#pseudonym_session_unique_id_forgot'); - await page.type('#pseudonym_session_unique_id_forgot', username); - - // Wait for the second textbox to become visible and type password - await page.waitForSelector('#pseudonym_session_password'); - await page.type('#pseudonym_session_password', password); - - // Wait for the first button to become visible and click - await page.waitForSelector('.Button'); - await page.click('.Button'); - // Wait for navigation to complete - await page.waitForNavigation(); - - - // Navigate to course and post discussion - const courseURL = `${process.env.CANVAS_URL}/courses/${courseId}/discussion_topics`; - await page.goto(courseURL); - await page.waitForSelector('#add_discussion'); - await page.click('#add_discussion'); - - await page.waitForSelector('#discussion-title'); - await page.type('#discussion-title', faker.lorem.sentence()); - - await page.waitForSelector('#discussion-topic-message2_ifr'); - const iframeElement = await page.$('#discussion-topic-message2_ifr'); - const iframe = await iframeElement.contentFrame(); - await iframe.waitForSelector('body'); - await iframe.type('body', faker.lorem.sentences(8)); - - // Click the "Save" button - await page.waitForSelector('button.btn.btn-primary.submit_button'); - await page.click('button.btn.btn-primary.submit_button'); - console.log('Hit Save button') - - } catch (error) { - console.error(`Failed to post discussion for user ${username}:`, error); - } finally { - // Close the browser - await browser.close(); - } -} - -function randomChoice(array) { - const randomIndex = Math.floor(Math.random() * array.length); - return array[randomIndex]; -} - -let courseIds = []; - -retrieveCourses(process.env.API_TOKEN, process.env.CANVAS_URL) - .then(courses => { - for (let i = 0; i < courses.length; i++) { - const courseId = courses[i].id; - courseIds.push(courseId); - } - randomCourseId = randomChoice(courseIds); - console.log('Random Course Id:', randomCourseId); - // Read and parse the tab-delimited credentials file - tab_file = path.join(__dirname, process.env.TAB_FILE_PATH) - console.log(`Reading the file: ${tab_file}`); - fs.readFile(tab_file, 'utf8', (err, data) => { - if (err) { - console.error('Error reading the file:', err); - return; - } - - // Split the file content by lines - const lines = data.trim().split('\n'); - - // Process each line (skipping blank lines and comment lines) - lines.forEach((line, index) => { - line = line.trim(); - if (line === '' || line.startsWith('#')) { - return; - } - - // Split each line by tab to get username and password - const [username, password] = line.split('\t'); - postDiscussion(username, password, randomCourseId, index + 1); - }); - - console.log('Tab-delimited text file successfully processed'); - }); - }) - .catch(error => { - console.error('Error:', error.message); - }); diff --git a/canvas-seeder/main.go b/canvas-seeder/main.go deleted file mode 100644 index 815da93f..00000000 --- a/canvas-seeder/main.go +++ /dev/null @@ -1,451 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "log" - "math/rand" - "net/http" - "net/url" - "os" - "strconv" - "strings" - - "github.com/brianvoe/gofakeit/v6" - "github.com/joho/godotenv" -) - -const ( - post = "POST" - put = "PUT" -) - -const ( - defaultAssignments = 10 - defaultCourses = 3 - defaultUsers = 10 -) - -func createCourse(apiToken string, - canvasURL string, -) ([]map[string]interface{}, error) { - coursesUrl := fmt.Sprintf("%s/api/v1/accounts/self/courses", canvasURL) - payload := url.Values{} - payload.Set("course[name]", gofakeit.Sentence(5)) - payload.Set("course[course_code]", fmt.Sprintf("%s%d", gofakeit.LetterN(2), gofakeit.Number(100, 500))) - payload.Set("course[description]", gofakeit.Sentence(8)) - payload.Set("course[is_public]", "True") - payload.Set("course[workflow_state]", "available") - payload.Set("course[default_view]", "assignments") - payload.Set("offer", "True") - - resp, err := requestUrlTokenPayload(post, coursesUrl, apiToken, payload.Encode(), nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - data, err := jsonRespToMapSlice(resp) - if err != nil { - return nil, err - } - - return data, nil -} - -func createCourseAssignments(apiToken string, - canvasURL string, - users []map[string]interface{}, - courseId string, - targetAssignments int, -) error { - assignmentsUrl := fmt.Sprintf("%s/api/v1/courses/%v/assignments", canvasURL, courseId) - - submission_limit := make(map[int]int) - for u := range users { - submission_limit[u] = rand.Intn(targetAssignments) + 1 - } - for i := 1; i <= targetAssignments; i++ { - payload := url.Values{} - payload.Set("assignment[position]", strconv.Itoa(i)) - payload.Set("assignment[name]", fmt.Sprintf("%d. %s", i, gofakeit.Sentence(5))) - payload.Set("assignment[description]", gofakeit.Sentence(7)) - payload.Set("assignment[submission_types]", "online_text_entry") - payload.Set("assignment[points_possible]", "100") - payload.Set("assignment[published]", "true") - payload.Set("assignment[hide_in_gradebook]", "false") - payload.Set("assignment[omit_from_final_grade]", "false") - - resp, err := requestUrlTokenPayload(post, assignmentsUrl, apiToken, payload.Encode(), nil) - if err != nil { - return err - } - defer resp.Body.Close() - - assignment, err := jsonRespToMapSlice(resp) - if err != nil { - return err - } - - assignmentId, err := interfaceToString(assignment[0]["id"]) - if err != nil { - return err - } - - fmt.Printf("Created assignment_id %v for course_id %v\n", assignmentId, courseId) - - for u, user := range users { - // Skip Teacher - if u == 0 { - continue - } - userId, err := interfaceToString(user["id"]) - if err != nil { - return err - } - if i <= submission_limit[u] { - createCourseAssignmentSubmission(apiToken, canvasURL, courseId, assignmentId, userId) - createCourseAssignmentSubmissionGrade(apiToken, canvasURL, courseId, assignmentId, userId) - } - } - } - - return nil -} - -func createCourseAssignmentSubmission(apiToken string, - canvasURL string, - courseId string, - assignmentId string, - userId string, -) error { - submissionsUrl := fmt.Sprintf("%s/api/v1/courses/%v/assignments/%v/submissions", canvasURL, courseId, assignmentId) - payload := url.Values{} - payload.Set("submission[submission_type]", "online_text_entry") - payload.Set("submission[body]", gofakeit.Paragraph(1, 5, 30, "")) - payload.Set("submission[user_id]", userId) - - resp, err := requestUrlTokenPayload(post, submissionsUrl, apiToken, payload.Encode(), nil) - if err != nil { - return err - } - defer resp.Body.Close() - - submission, err := jsonRespToMapSlice(resp) - if err != nil { - return err - } - - fmt.Printf("Submitted assignment ID %v for course ID %v for user ID %v with submission ID %v\n", assignmentId, courseId, userId, submission[0]["id"]) - - return nil -} - -func createCourseAssignmentSubmissionGrade(apiToken string, - canvasURL string, - courseId string, - assignmentId string, - userId string, -) error { - submissionsUrl := fmt.Sprintf("%s/api/v1/courses/%v/assignments/%v/submissions/%v", canvasURL, courseId, assignmentId, userId) - grade := gofakeit.Number(60, 100) - body := fmt.Sprintf("{\"submission\": {\"posted_grade\":\"%v\"}}", grade) - - headers := make(map[string]string) - headers["Content-Type"] = "application/json" - resp, err := requestUrlTokenPayload(put, submissionsUrl, apiToken, body, headers) - if err != nil { - return err - } - defer resp.Body.Close() - - fmt.Printf("Graded %v assignment ID %v for course ID %v for user ID %v\n", grade, assignmentId, courseId, userId) - - return nil -} - -func createUser(apiToken string, - canvasURL string, -) ([]map[string]interface{}, error) { - usersUrl := fmt.Sprintf("%s/api/v1/accounts/self/users", canvasURL) - - email := gofakeit.Email() - password := gofakeit.Password(true, true, true, false, false, 12) - - payload := url.Values{} - payload.Set("user[name]", gofakeit.Name()) - payload.Set("pseudonym[unique_id]", email) - payload.Set("pseudonym[password]", password) - payload.Set("pseudonym[send_confirmation]", "false") - payload.Set("user[skip_registration]", "true") - payload.Set("communication_channel[skip_confirmation]", "true") - - resp, err := requestUrlTokenPayload("POST", usersUrl, apiToken, payload.Encode(), nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - userData, err := jsonRespToMapSlice(resp) - if err != nil { - return nil, err - } - - // Append email and password to a text file - file, err := os.OpenFile("credentials.tab", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, err - } - defer file.Close() - - _, err = file.WriteString(fmt.Sprintf("%s\t%s\n", email, password)) - if err != nil { - return nil, err - } - - return userData, nil -} - -func enrollAllUsers(apiToken string, - canvasURL string, - users []map[string]interface{}, - courseId interface{}, -) error { - enrollmentsUrl := fmt.Sprintf("%s/api/v1/courses/%v/enrollments", canvasURL, courseId) - - for i, user := range users { - userId, err := interfaceToString(user["id"]) - if err != nil { - return err - } - - enrollmentType := "StudentEnrollment" - if i == 0 { - enrollmentType = "TeacherEnrollment" - } - - user["enrollment_type"] = enrollmentType - payload := url.Values{} - payload.Set("enrollment[user_id]", userId) - payload.Set("enrollment[type]", enrollmentType) - payload.Set("enrollment[enrollment_state]", "active") - - fmt.Printf("Enrolling %v course_id %v as %s\n", userId, courseId, enrollmentType) - _, err = requestUrlTokenPayload(post, enrollmentsUrl, apiToken, payload.Encode(), nil) - if err != nil { - return err - } - } - - return nil -} - -func interfaceToString(value interface{}) (string, error) { - switch v := value.(type) { - case int: - return strconv.Itoa(v), nil - case float64: - return strconv.FormatFloat(v, 'f', -1, 64), nil - case string: - return v, nil - default: - return "", fmt.Errorf("unsupported type: %T", value) - } -} - -func jsonRespToMapSlice(resp *http.Response) ([]map[string]interface{}, error) { - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("error reading response body: %s", err) - } - - // Check if the response is an array or a single object - var dataSlice []map[string]interface{} - var dataMap map[string]interface{} - err = json.Unmarshal(body, &dataSlice) - if err == nil { - // Response is an array - return dataSlice, nil - } - - // Try parsing as a single object - err = json.Unmarshal(body, &dataMap) - if err != nil { - return nil, fmt.Errorf("error parsing JSON: %s", err) - } - - // Convert single object to a slice containing one element - dataSlice = append(dataSlice, dataMap) - return dataSlice, nil -} - -func requestUrlTokenPayload(method string, - url string, - apiToken string, - body string, - headers map[string]string, -) (*http.Response, error) { - req, err := http.NewRequest(method, url, strings.NewReader(body)) - if err != nil { - return nil, fmt.Errorf("error creating request: %s", err) - } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiToken)) - for key, value := range headers { - req.Header.Set(key, value) - } - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error retrieving data: %s", err) - } - - return resp, nil -} - -func retrieveCourses(apiToken string, - canvasURL string, -) ([]map[string]interface{}, error) { - endPoint := "/api/v1/accounts/self/courses" - url := canvasURL + endPoint - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, fmt.Errorf("error creating request: %s", err) - } - req.Header.Set("Authorization", "Bearer "+apiToken) - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %s", err) - } - defer resp.Body.Close() - - data, err := jsonRespToMapSlice(resp) - if err != nil { - return nil, err - } - - return data, nil -} - -func retrieveUsers(apiToken string, - canvasURL string, -) ([]map[string]interface{}, error) { - endPoint := "/api/v1/accounts/self/users" - url := canvasURL + endPoint - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, fmt.Errorf("error creating request: %s", err) - } - req.Header.Set("Authorization", "Bearer "+apiToken) - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("error sending request: %s", err) - } - defer resp.Body.Close() - - data, err := jsonRespToMapSlice(resp) - if err != nil { - return nil, err - } - - return data, nil -} - -func main() { - err := godotenv.Load() - if err != nil { - log.Fatal("Error loading .env file") - } - - apiToken := os.Getenv("API_TOKEN") - canvasURL := os.Getenv("CANVAS_URL") - targetAssignmentsStr := os.Getenv("TARGET_ASSIGNMENTS") - targetCoursesStr := os.Getenv("TARGET_COURSES") - targetUsersStr := os.Getenv("TARGET_USERS") - - targetAssignments := defaultAssignments - targetCourses := defaultCourses - targetUsers := defaultUsers - - if apiToken == "" || canvasURL == "" { - log.Printf("API_TOKEN: %s\n", apiToken) - log.Printf("CANVAS_URL: %s\n", canvasURL) - log.Printf("TARGET_ASSIGNMENTS: %s\n", targetAssignmentsStr) - log.Printf("TARGET_COURSES: %s\n", targetCoursesStr) - log.Fatal("API_TOKEN or CANVAS_URL not set in environment variables") - } - - if targetAssignmentsStr != "" { - targetAssignments, err = strconv.Atoi(targetAssignmentsStr) - if err != nil { - log.Fatal("error parsing TARGET_ASSIGNMENTS:", err) - } - } - - if targetCoursesStr != "" { - targetCourses, err = strconv.Atoi(targetCoursesStr) - if err != nil { - log.Fatal("error parsing TARGET_COURSES:", err) - } - } - - if targetUsersStr != "" { - targetUsers, err = strconv.Atoi(targetUsersStr) - if err != nil { - log.Fatal("error parsing TARGET_USERS:", err) - } - } - - users, err := retrieveUsers(apiToken, canvasURL) - if err != nil { - log.Fatal("error retrieving users:", err) - } - - // Create users up to targetUsers - for i := len(users); i < targetUsers; i++ { - _, err := createUser(apiToken, canvasURL) - if err != nil { - log.Fatal("error creating user:", err) - } - log.Printf("Created user %v\n", i) - } - - users, err = retrieveUsers(apiToken, canvasURL) - if err != nil { - log.Fatal("error retrieving users:", err) - } - - courses, err := retrieveCourses(apiToken, canvasURL) - if err != nil { - log.Fatal("error retrieving courses:", err) - } - // Create courses up to targetCourses - for i := len(courses); i < targetCourses; i++ { - newCourses, err := createCourse(apiToken, canvasURL) - if err != nil { - log.Fatal("error creating courses:", err) - } - for _, course := range newCourses { - courseId, err := interfaceToString(course["id"]) - if err != nil { - log.Fatal("error converting course ID:", err) - } - fmt.Printf("Created course_id %v %s\n", courseId, course["name"]) - err = enrollAllUsers(apiToken, canvasURL, users, courseId) - if err != nil { - log.Fatal("error enrolling users:", err) - } - err = createCourseAssignments(apiToken, canvasURL, users, courseId, targetAssignments) - if err != nil { - log.Fatal("error creating course assignments:", err) - } - } - } -} diff --git a/canvas-seeder/package-lock.json b/canvas-seeder/package-lock.json deleted file mode 100644 index b1d09253..00000000 --- a/canvas-seeder/package-lock.json +++ /dev/null @@ -1,2320 +0,0 @@ -{ - "name": "canvas-discussion-bot", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "canvas-discussion-bot", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "axios": "^1.7.2", - "dotenv": "^16.4.5", - "puppeteer": "^23.3.0" - }, - "devDependencies": { - "@faker-js/faker": "^8.4.1" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", - "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", - "dependencies": { - "@babel/highlight": "^7.24.6", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", - "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", - "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.6", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@faker-js/faker": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", - "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/fakerjs" - } - ], - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0", - "npm": ">=6.14.13" - } - }, - "node_modules/@puppeteer/browsers": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.4.0.tgz", - "integrity": "sha512-x8J1csfIygOwf6D6qUAZ0ASk3z63zPb7wkNeHRerCMh82qWKUrOgkuP005AJC8lDL6/evtXETGEJVcwykKT4/g==", - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.3.6", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.4.0", - "semver": "^7.6.3", - "tar-fs": "^3.0.6", - "unbzip2-stream": "^1.4.3", - "yargs": "^17.7.2" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.5.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", - "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", - "license": "MIT", - "optional": true, - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/b4a": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", - "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", - "license": "Apache-2.0" - }, - "node_modules/bare-events": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", - "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", - "license": "Apache-2.0", - "optional": true - }, - "node_modules/bare-fs": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.4.tgz", - "integrity": "sha512-7YyxitZEq0ey5loOF5gdo1fZQFF7290GziT+VbAJ+JbYTJYaPZwuEz2r/Nq23sm4fjyTgUf2uJI2gkT3xAuSYA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-events": "^2.0.0", - "bare-path": "^2.0.0", - "bare-stream": "^2.0.0" - } - }, - "node_modules/bare-os": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.3.tgz", - "integrity": "sha512-FjkNiU3AwTQNQkcxFOmDcCfoN1LjjtU+ofGJh5DymZZLTqdw2i/CzV7G0h3snvh6G8jrWtdmNSgZPH4L2VOAsQ==", - "license": "Apache-2.0", - "optional": true - }, - "node_modules/bare-path": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", - "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-os": "^2.1.0" - } - }, - "node_modules/bare-stream": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.0.tgz", - "integrity": "sha512-pVRWciewGUeCyKEuRxwv06M079r+fRjAQjBEK2P6OYGrO43O+Z0LrPZZEjlc4mB6C2RpZ9AxJ1s7NLEtOHO6eA==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "b4a": "^1.6.6", - "streamx": "^2.20.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/basic-ftp": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chromium-bidi": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.5.tgz", - "integrity": "sha512-RuLrmzYrxSb0s9SgpB+QN5jJucPduZQ/9SIe76MDxYJuecPW5mxMdacJ1f4EtgiV+R0p3sCkznTMvH0MPGFqjA==", - "license": "Apache-2.0", - "dependencies": { - "mitt": "3.0.1", - "urlpattern-polyfill": "10.0.0", - "zod": "3.23.8" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/devtools-protocol": { - "version": "0.0.1330662", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1330662.tgz", - "integrity": "sha512-pzh6YQ8zZfz3iKlCvgzVCu22NdpZ8hNmwU6WnQjNVquh0A9iVosPtNLWDwaWVGyrntQlltPFztTMK5Cg6lfCuw==", - "license": "BSD-3-Clause" - }, - "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "license": "MIT" - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-uri": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", - "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4", - "fs-extra": "^11.2.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/pac-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", - "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.5", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-agent": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", - "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.3", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.1", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/puppeteer": { - "version": "23.3.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.3.0.tgz", - "integrity": "sha512-e2jY8cdWSUGsrLxqGm3hIbJq/UIk1uOY8XY7SM51leXkH7shrIyE91lK90Q9byX6tte+cyL3HKqlWBEd6TjWTA==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.4.0", - "chromium-bidi": "0.6.5", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1330662", - "puppeteer-core": "23.3.0", - "typed-query-selector": "^2.12.0" - }, - "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/puppeteer-core": { - "version": "23.3.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.3.0.tgz", - "integrity": "sha512-sB2SsVMFs4gKad5OCdv6w5vocvtEUrRl0zQqSyRPbo/cj1Ktbarmhxy02Zyb9R9HrssBcJDZbkrvBnbaesPyYg==", - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.4.0", - "chromium-bidi": "0.6.5", - "debug": "^4.3.6", - "devtools-protocol": "0.0.1330662", - "typed-query-selector": "^2.12.0", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "license": "MIT" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "engines": { - "node": ">=4" - } - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", - "license": "MIT", - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", - "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.1", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause" - }, - "node_modules/streamx": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.0.tgz", - "integrity": "sha512-ZGd1LhDeGFucr1CUCTBOS58ZhEendd0ttpGT3usTvosS4ntIwKN9LJFp+OeCSprsCPL14BXVRZlHGRY1V9PVzQ==", - "license": "MIT", - "dependencies": { - "fast-fifo": "^1.3.2", - "queue-tick": "^1.0.1", - "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tar-fs": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", - "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^2.1.1", - "bare-path": "^2.1.0" - } - }, - "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/text-decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz", - "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD" - }, - "node_modules/typed-query-selector": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", - "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", - "license": "MIT" - }, - "node_modules/unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "license": "MIT", - "dependencies": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, - "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "license": "MIT", - "optional": true - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/urlpattern-polyfill": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", - "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", - "license": "MIT" - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/wrap-ansi/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", - "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", - "requires": { - "@babel/highlight": "^7.24.6", - "picocolors": "^1.0.0" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", - "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==" - }, - "@babel/highlight": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", - "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.24.6", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - } - }, - "@faker-js/faker": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", - "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", - "dev": true - }, - "@puppeteer/browsers": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.4.0.tgz", - "integrity": "sha512-x8J1csfIygOwf6D6qUAZ0ASk3z63zPb7wkNeHRerCMh82qWKUrOgkuP005AJC8lDL6/evtXETGEJVcwykKT4/g==", - "requires": { - "debug": "^4.3.6", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.4.0", - "semver": "^7.6.3", - "tar-fs": "^3.0.6", - "unbzip2-stream": "^1.4.3", - "yargs": "^17.7.2" - } - }, - "@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" - }, - "@types/node": { - "version": "22.5.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", - "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", - "optional": true, - "requires": { - "undici-types": "~6.19.2" - } - }, - "@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "optional": true, - "requires": { - "@types/node": "*" - } - }, - "agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "requires": { - "debug": "^4.3.4" - } - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "requires": { - "tslib": "^2.0.1" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", - "requires": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "b4a": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", - "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==" - }, - "bare-events": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", - "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", - "optional": true - }, - "bare-fs": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.4.tgz", - "integrity": "sha512-7YyxitZEq0ey5loOF5gdo1fZQFF7290GziT+VbAJ+JbYTJYaPZwuEz2r/Nq23sm4fjyTgUf2uJI2gkT3xAuSYA==", - "optional": true, - "requires": { - "bare-events": "^2.0.0", - "bare-path": "^2.0.0", - "bare-stream": "^2.0.0" - } - }, - "bare-os": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.3.tgz", - "integrity": "sha512-FjkNiU3AwTQNQkcxFOmDcCfoN1LjjtU+ofGJh5DymZZLTqdw2i/CzV7G0h3snvh6G8jrWtdmNSgZPH4L2VOAsQ==", - "optional": true - }, - "bare-path": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", - "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", - "optional": true, - "requires": { - "bare-os": "^2.1.0" - } - }, - "bare-stream": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.0.tgz", - "integrity": "sha512-pVRWciewGUeCyKEuRxwv06M079r+fRjAQjBEK2P6OYGrO43O+Z0LrPZZEjlc4mB6C2RpZ9AxJ1s7NLEtOHO6eA==", - "optional": true, - "requires": { - "b4a": "^1.6.6", - "streamx": "^2.20.0" - } - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "basic-ftp": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==" - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chromium-bidi": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.5.tgz", - "integrity": "sha512-RuLrmzYrxSb0s9SgpB+QN5jJucPduZQ/9SIe76MDxYJuecPW5mxMdacJ1f4EtgiV+R0p3sCkznTMvH0MPGFqjA==", - "requires": { - "mitt": "3.0.1", - "urlpattern-polyfill": "10.0.0", - "zod": "3.23.8" - } - }, - "cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "requires": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - } - }, - "data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==" - }, - "debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "requires": { - "ms": "^2.1.3" - } - }, - "degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "requires": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "devtools-protocol": { - "version": "0.0.1330662", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1330662.tgz", - "integrity": "sha512-pzh6YQ8zZfz3iKlCvgzVCu22NdpZ8hNmwU6WnQjNVquh0A9iVosPtNLWDwaWVGyrntQlltPFztTMK5Cg6lfCuw==" - }, - "dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "requires": { - "once": "^1.4.0" - } - }, - "env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, - "escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "requires": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "source-map": "~0.6.1" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "requires": { - "@types/yauzl": "^2.9.1", - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - } - }, - "fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "requires": { - "pend": "~1.2.0" - } - }, - "follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "requires": { - "pump": "^3.0.0" - } - }, - "get-uri": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", - "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", - "requires": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4", - "fs-extra": "^11.2.0" - } - }, - "graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - }, - "http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "requires": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - } - }, - "https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "requires": { - "agent-base": "^7.0.2", - "debug": "4" - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "requires": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "requires": { - "argparse": "^2.0.1" - } - }, - "jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "requires": { - "wrappy": "1" - } - }, - "pac-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", - "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", - "requires": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.5", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.4" - } - }, - "pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "requires": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" - }, - "picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" - }, - "proxy-agent": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", - "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", - "requires": { - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.3", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.1", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.2" - } - }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "puppeteer": { - "version": "23.3.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.3.0.tgz", - "integrity": "sha512-e2jY8cdWSUGsrLxqGm3hIbJq/UIk1uOY8XY7SM51leXkH7shrIyE91lK90Q9byX6tte+cyL3HKqlWBEd6TjWTA==", - "requires": { - "@puppeteer/browsers": "2.4.0", - "chromium-bidi": "0.6.5", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1330662", - "puppeteer-core": "23.3.0", - "typed-query-selector": "^2.12.0" - } - }, - "puppeteer-core": { - "version": "23.3.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.3.0.tgz", - "integrity": "sha512-sB2SsVMFs4gKad5OCdv6w5vocvtEUrRl0zQqSyRPbo/cj1Ktbarmhxy02Zyb9R9HrssBcJDZbkrvBnbaesPyYg==", - "requires": { - "@puppeteer/browsers": "2.4.0", - "chromium-bidi": "0.6.5", - "debug": "^4.3.6", - "devtools-protocol": "0.0.1330662", - "typed-query-selector": "^2.12.0", - "ws": "^8.18.0" - } - }, - "queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - }, - "semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" - }, - "smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" - }, - "socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", - "requires": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - } - }, - "socks-proxy-agent": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", - "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", - "requires": { - "agent-base": "^7.1.1", - "debug": "^4.3.4", - "socks": "^2.8.3" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true - }, - "sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" - }, - "streamx": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.0.tgz", - "integrity": "sha512-ZGd1LhDeGFucr1CUCTBOS58ZhEendd0ttpGT3usTvosS4ntIwKN9LJFp+OeCSprsCPL14BXVRZlHGRY1V9PVzQ==", - "requires": { - "bare-events": "^2.2.0", - "fast-fifo": "^1.3.2", - "queue-tick": "^1.0.1", - "text-decoder": "^1.1.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "tar-fs": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", - "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", - "requires": { - "bare-fs": "^2.1.1", - "bare-path": "^2.1.0", - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - } - }, - "tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "requires": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "text-decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz", - "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==", - "requires": { - "b4a": "^1.6.4" - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" - }, - "tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" - }, - "typed-query-selector": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", - "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==" - }, - "unbzip2-stream": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", - "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", - "requires": { - "buffer": "^5.2.1", - "through": "^2.3.8" - } - }, - "undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "optional": true - }, - "universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" - }, - "urlpattern-polyfill": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", - "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==" - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "requires": {} - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" - }, - "yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "requires": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - } - }, - "yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" - }, - "yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==" - } - } -} diff --git a/canvas-seeder/package.json b/canvas-seeder/package.json deleted file mode 100644 index 34e1c26e..00000000 --- a/canvas-seeder/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "canvas-discussion-bot", - "version": "1.0.0", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "keywords": [], - "dependencies": { - "axios": "^1.7.2", - "dotenv": "^16.4.5", - "puppeteer": "^23.3.0" - }, - "description": "", - "devDependencies": { - "@faker-js/faker": "^8.4.1" - } -} diff --git a/canvas-seeder/users.txt b/canvas-seeder/users.txt deleted file mode 100644 index 1a91ce7e..00000000 --- a/canvas-seeder/users.txt +++ /dev/null @@ -1,6 +0,0 @@ -HalvorsonEuna1;starfish22 -RichardSchoen131;starfish22 -OpheliaNienow131;starfish22 -BlandaMax;starfish22 -LavadaLittle121;starfish22 -OrvalKoelpin121;starfish22 diff --git a/docker-compose.yml b/docker-compose.yml index 596c2961..e0274465 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -54,11 +54,10 @@ services: volumes: - ./:/app environment: - - APP_DSN=postgres://unlocked:dev@postgres:5432/unlocked?log_duration=true&log_min_duration_statement=100 + - APP_DSN=postgres://unlocked:dev@postgres:5432/unlocked - APP_ENV=production - LOG_LEVEL=debug - APP_URL=http://127.0.0.1 - - PROVIDER_SERVICE_KEY=NTQxODNmNDMyM2YzNzdiNzM3NDMzYTFlOTgyMjllYWQwZmRjNjg2ZjkzYmFiMDU3ZWNiNjEyZGFhOTQwMDJiNSAgLQo= - APP_KEY=base64:NTQxODNmNDMyM2YzNzdiNzM3NDMzYTFlOTgyMjllYWQwZmRjNjg2ZjkzYmFiMDU3ZWNiNjEyZGFhOTQwMDJiNSAgLQo= - PROVIDER_SERVICE_URL=http://provider-service:8081 - HYDRA_ADMIN_URL=http://hydra:4445 @@ -82,10 +81,9 @@ services: provider-service: build: context: . - dockerfile: provider-middleware/Dockerfile + dockerfile: provider-middleware/dev.Dockerfile environment: - APP_ENV=production - - PROVIDER_SERVICE_KEY=NTQxODNmNDMyM2YzNzdiNzM3NDMzYTFlOTgyMjllYWQwZmRjNjg2ZjkzYmFiMDU3ZWNiNjEyZGFhOTQwMDJiNSAgLQo= - APP_KEY=base64:NTQxODNmNDMyM2YzNzdiNzM3NDMzYTFlOTgyMjllYWQwZmRjNjg2ZjkzYmFiMDU3ZWNiNjEyZGFhOTQwMDJiNSAgLQo= - DB_HOST=postgres - DB_PORT=5432 @@ -96,11 +94,13 @@ services: - NATS_URL=nats:4222 - NATS_USER=unlocked - NATS_PASSWORD=dev - command: ./provider-service networks: - intranet volumes: - logs:/logs + - ./:/app + ports: + - 8081:8081 restart: unless-stopped depends_on: postgres: @@ -110,7 +110,7 @@ services: cron-tasks: build: context: . - dockerfile: ./backend/tasks/Dockerfile + dockerfile: ./backend/tasks/dev.Dockerfile environment: - LOG_LEVEL=debug - NATS_URL=nats:4222 @@ -126,6 +126,7 @@ services: - intranet volumes: - logs:/logs + - ./:/app depends_on: provider-service: condition: service_started diff --git a/go.work b/go.work index c2f73bb0..db621d4e 100644 --- a/go.work +++ b/go.work @@ -5,6 +5,5 @@ use ( ./backend/migrations ./backend/seeder ./backend/tasks - ./canvas-seeder ./provider-middleware ) diff --git a/provider-middleware/dev.Dockerfile b/provider-middleware/dev.Dockerfile new file mode 100644 index 00000000..3d377fea --- /dev/null +++ b/provider-middleware/dev.Dockerfile @@ -0,0 +1,10 @@ +FROM golang:1.23-alpine + +WORKDIR /app + +RUN go install github.com/air-verse/air@latest + +COPY provider-middleware/go.mod provider-middleware/go.sum ./ +RUN go mod download +EXPOSE 8081 +CMD ["air", "-c", ".middleware.air.toml"] diff --git a/provider-middleware/handlers.go b/provider-middleware/handlers.go index 7157a5bc..2e4c7ce3 100644 --- a/provider-middleware/handlers.go +++ b/provider-middleware/handlers.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "net/http" "time" @@ -10,34 +11,35 @@ import ( ) func (sh *ServiceHandler) registerRoutes() { - sh.Mux.Handle("/", sh.authMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sh.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - }))) - sh.Mux.Handle("GET /api/users", sh.authMiddleware(http.HandlerFunc(sh.handleUsers))) + }) + sh.Mux.HandleFunc("GET /api/users", sh.handleUsers) } +const CANCEL_TIMEOUT = 30 * time.Minute + func (sh *ServiceHandler) initSubscription() error { - _, err := sh.nats.Subscribe("tasks.get_courses", func(msg *nats.Msg) { - go sh.handleCourses(msg) - }) - if err != nil { - log.Fatalf("Error subscribing to NATS topic: %v", err) - return err - } - _, err = sh.nats.Subscribe("tasks.get_milestones", func(msg *nats.Msg) { - go sh.handleMilestonesForCourseUser(msg) - }) - if err != nil { - log.Fatalf("Error subscribing to NATS topic: %v", err) - return err + subscriptions := []struct { + topic string + fn func(ctx context.Context, msg *nats.Msg) + }{ + {"tasks.get_courses", sh.handleCourses}, + {"tasks.get_milestones", sh.handleMilestonesForCourseUser}, + {"tasks.get_activity", sh.handleAcitivityForCourse}, + {"tasks.scrape_kiwix", sh.handleScrapeLibraries}, } - _, err = sh.nats.Subscribe("tasks.get_activity", func(msg *nats.Msg) { - go sh.handleAcitivityForCourse(msg) - }) - if err != nil { - log.Fatalf("Error subscribing to NATS topic: %v", err) - return err + + for _, sub := range subscriptions { + _, err := sh.nats.Subscribe(sub.topic, func(msg *nats.Msg) { + go sub.fn(sh.ctx, msg) + }) + if err != nil { + log.Fatalf("Error subscribing to NATS topic %s: %v", sub.topic, err) + return err + } } + return nil } @@ -46,8 +48,10 @@ func (sh *ServiceHandler) initSubscription() error { * This handler will be responsible for importing courses from Providers * to the UnlockEd platform, mapping their Content objects to our Course object */ -func (sh *ServiceHandler) handleCourses(msg *nats.Msg) { - service, err := sh.initService(msg) +func (sh *ServiceHandler) handleCourses(ctx context.Context, msg *nats.Msg) { + contxt, cancel := context.WithTimeout(ctx, CANCEL_TIMEOUT) + defer cancel() + service, err := sh.initProviderPlatformService(contxt, msg) if err != nil { log.WithFields(log.Fields{"error": err.Error()}).Error("Failed to initialize service") return @@ -57,7 +61,7 @@ func (sh *ServiceHandler) handleCourses(msg *nats.Msg) { providerPlatformId := int(params["provider_platform_id"].(float64)) err = service.ImportCourses(sh.db) if err != nil { - sh.cleanupJob(providerPlatformId, jobId, false) + sh.cleanupJob(contxt, providerPlatformId, jobId, false) log.Println("error fetching provider service from msg parameters", err) return } @@ -67,7 +71,33 @@ func (sh *ServiceHandler) handleCourses(msg *nats.Msg) { if err != nil { log.Errorln("Failed to publish message to NATS") } - sh.cleanupJob(providerPlatformId, jobId, true) + sh.cleanupJob(contxt, providerPlatformId, jobId, true) +} + +/** +* GET: /api/libraries +* This handler will be responsible for importing libraries from Open Content Providers +* to the UnlockEd platform with the proper fields for Library objects +**/ +func (sh *ServiceHandler) handleScrapeLibraries(ctx context.Context, msg *nats.Msg) { + contxt, cancel := context.WithTimeout(ctx, CANCEL_TIMEOUT) + defer cancel() + service, err := sh.initContentProviderService(msg) + if err != nil { + log.Errorf("error fetching provider service from msg parameters %v", err) + return + } + params := *service.GetJobParams() + provId := int(params["open_content_provider_id"].(float64)) + jobId := params["job_id"].(string) + err = service.ImportLibraries(contxt, sh.db) + if err != nil { + log.Errorf("error importing libraries from msg %v", err) + sh.cleanupJob(contxt, provId, jobId, false) + return + } + + sh.cleanupJob(contxt, provId, jobId, true) } /** @@ -78,7 +108,7 @@ func (sh *ServiceHandler) handleCourses(msg *nats.Msg) { **/ func (sh *ServiceHandler) handleUsers(w http.ResponseWriter, r *http.Request) { fields := log.Fields{"handler": "handleUsers"} - service, err := sh.initServiceFromRequest(r) + service, err := sh.initServiceFromRequest(sh.ctx, r) if err != nil { fields["error"] = err.Error() log.WithFields(fields).Error("Failed to initialize service") @@ -103,8 +133,10 @@ func (sh *ServiceHandler) handleUsers(w http.ResponseWriter, r *http.Request) { } } -func (sh *ServiceHandler) handleMilestonesForCourseUser(msg *nats.Msg) { - service, err := sh.initService(msg) +func (sh *ServiceHandler) handleMilestonesForCourseUser(ctx context.Context, msg *nats.Msg) { + contxt, cancel := context.WithTimeout(ctx, CANCEL_TIMEOUT) + defer cancel() + service, err := sh.initProviderPlatformService(contxt, msg) if err != nil { log.WithFields(log.Fields{"error": err.Error()}).Error("Failed to initialize service") return @@ -119,22 +151,30 @@ func (sh *ServiceHandler) handleMilestonesForCourseUser(msg *nats.Msg) { providerPlatformId := int(params["provider_platform_id"].(float64)) lastRun, err := time.Parse(time.RFC3339, lastRunStr) if err != nil { - sh.cleanupJob(providerPlatformId, jobId, false) + sh.cleanupJob(contxt, providerPlatformId, jobId, false) return } for _, course := range courses { - err = service.ImportMilestones(course, users, sh.db, lastRun) - time.Sleep(1 * time.Second) // to avoid rate limiting with the provider - if err != nil { - log.Errorf("Failed to retrieve milestones: %v", err) - continue + select { + case <-contxt.Done(): + log.Println("context cancelled for getMilestones") + return + default: + err = service.ImportMilestones(course, users, sh.db, lastRun) + time.Sleep(TIMEOUT_WAIT * time.Second) // to avoid rate limiting with the provider + if err != nil { + log.Errorf("Failed to retrieve milestones: %v", err) + continue + } } } - sh.cleanupJob(providerPlatformId, jobId, true) + sh.cleanupJob(contxt, providerPlatformId, jobId, true) } -func (sh *ServiceHandler) handleAcitivityForCourse(msg *nats.Msg) { - service, err := sh.initService(msg) +func (sh *ServiceHandler) handleAcitivityForCourse(ctx context.Context, msg *nats.Msg) { + contxt, cancel := context.WithTimeout(ctx, CANCEL_TIMEOUT) + defer cancel() + service, err := sh.initProviderPlatformService(contxt, msg) if err != nil { log.WithFields(log.Fields{"error": err.Error()}).Error("Failed to initialize service") return @@ -145,14 +185,20 @@ func (sh *ServiceHandler) handleAcitivityForCourse(msg *nats.Msg) { jobId := params["job_id"].(string) providerPlatformId := int(params["provider_platform_id"].(float64)) for _, course := range courses { - err = service.ImportActivityForCourse(course, sh.db) - if err != nil { - sh.cleanupJob(providerPlatformId, jobId, false) - log.Errorf("failed to get course activity: %v", err) - continue + select { + case <-contxt.Done(): + log.Println("context cancelled for getActivity") + return + default: + err = service.ImportActivityForCourse(course, sh.db) + if err != nil { + sh.cleanupJob(contxt, providerPlatformId, jobId, false) + log.Errorf("failed to get course activity: %v", err) + continue + } } } - sh.cleanupJob(providerPlatformId, jobId, true) + sh.cleanupJob(contxt, providerPlatformId, jobId, true) } func extractArrayMap(params map[string]interface{}, mapType string) []map[string]interface{} { diff --git a/provider-middleware/kiwix.go b/provider-middleware/kiwix.go new file mode 100644 index 00000000..7a2e22c2 --- /dev/null +++ b/provider-middleware/kiwix.go @@ -0,0 +1,112 @@ +package main + +import ( + "UnlockEdv2/src/models" + "context" + "encoding/xml" + "fmt" + "io" + "net/http" + + "github.com/sirupsen/logrus" + log "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +const ( + KiwixCatalogUrl = "/catalog/v2/entries?lang=eng&start=1&count=" + MaxLibraries = 1000 +) + +type KiwixService struct { + OpenContentProviderId uint + Url string + params *map[string]interface{} +} + +func NewKiwixService(openContentProvider *models.OpenContentProvider, params *map[string]interface{}) *KiwixService { + url := fmt.Sprintf("%s%s%d", openContentProvider.BaseUrl, KiwixCatalogUrl, MaxLibraries) + return &KiwixService{ + OpenContentProviderId: openContentProvider.ID, + Url: url, + params: params, + } +} + +func (ks *KiwixService) GetJobParams() *map[string]interface{} { + return ks.params +} + +func (ks *KiwixService) ImportLibraries(ctx context.Context, db *gorm.DB) error { + log.Infoln("Importing libraries from Kiwix") + resp, err := http.Get(ks.Url) + if err != nil { + log.Errorf("error fetching data from url: %v", err) + return err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Errorf("error reading data: %v", err) + return err + } + var feed Feed + err = xml.Unmarshal(body, &feed) + if err != nil { + log.Errorf("error parsing data: %v", err) + return err + } + log.Infof("Found %v libraries from Kiwix", len(feed.Entries)) + + var externalIds []string + for _, entry := range feed.Entries { + select { + case <-ctx.Done(): + log.Infoln("Context cancelled, stopping import") + return nil + default: + externalIds = append(externalIds, entry.ID) + err = UpdateOrInsertLibrary(ctx, db, entry, ks.OpenContentProviderId) + if err != nil { + log.Errorf("error updating or inserting library: %v", err) + return err + } + } + } + removed, err := RemoveDeletedEntries(ctx, db, externalIds, ks.OpenContentProviderId) + if err != nil { + log.Errorf("error removing deleted entries: %v", err) + return err + } + log.Infof("Removed %v deleted libraries", removed) + return nil +} + +func UpdateOrInsertLibrary(ctx context.Context, db *gorm.DB, entry Entry, providerId uint) error { + log.Infoln("Attempting to update existing Kiwix Libraries.") + library := IntoLibrary(entry, providerId) + if err := db.WithContext(ctx). + Where(&models.Library{ExternalID: models.StringPtr(entry.ID)}). + Assign(models.Library{ + Path: library.Path, + Name: library.Name, + Description: library.Description, + Language: library.Language}). + FirstOrCreate(&library).Error; err != nil { + logrus.Errorln("Error updating or inserting library: ", err) + return err + } + return nil +} + +func RemoveDeletedEntries(ctx context.Context, db *gorm.DB, externalIds []string, providerId uint) (int64, error) { + log.Infoln("Removing any deleted Kiwix libraries") + + tx := db.WithContext(ctx).Where("open_content_provider_id = ?", providerId).Delete(&models.Library{}, "external_id NOT IN (?)", externalIds) + if tx.Error != nil { + return 0, tx.Error + } + + return tx.RowsAffected, nil +} diff --git a/provider-middleware/kiwix_data.go b/provider-middleware/kiwix_data.go new file mode 100644 index 00000000..9a962817 --- /dev/null +++ b/provider-middleware/kiwix_data.go @@ -0,0 +1,71 @@ +package main + +import ( + "UnlockEdv2/src/models" + "encoding/xml" + "strings" +) + +type Feed struct { + XMLName xml.Name `xml:"feed"` + Entries []Entry `xml:"entry"` +} + +type Entry struct { + ID string `xml:"id"` + Title string `xml:"title"` + Updated string `xml:"updated"` + Summary string `xml:"summary"` + Language string `xml:"language"` + Name string `xml:"name"` + Flavour string `xml:"flavour"` + Category string `xml:"category"` + Tags string `xml:"tags"` + ArticleCount int `xml:"articleCount"` + MediaCount int `xml:"mediaCount"` + Author Author `xml:"author"` + Publisher Publisher `xml:"publisher"` + Links []Link `xml:"link"` +} + +type Author struct { + Name string `xml:"name"` +} + +type Publisher struct { + Name string `xml:"name"` +} + +type Link struct { + Rel string `xml:"rel,attr"` + Href string `xml:"href,attr"` + Type string `xml:"type,attr"` +} + +func IntoLibrary(entry Entry, providerId uint) *models.Library { + url, thumbnailURL := ParseUrls(entry.Links) + return &models.Library{ + OpenContentProviderID: providerId, + ExternalID: models.StringPtr(entry.ID), + Name: entry.Title, + Language: models.StringPtr(entry.Language), + Description: models.StringPtr(entry.Summary), + Path: url, + ImageUrl: models.StringPtr(thumbnailURL), + VisibilityStatus: false, + } +} + +func ParseUrls(links []Link) (string, string) { + var url string + var thumbnailURL string + for _, link := range links { + if link.Type == "text/html" { + url = link.Href + } + if strings.Split(link.Type, "/")[0] == "image" { + thumbnailURL = link.Href + } + } + return url, thumbnailURL +} diff --git a/provider-middleware/kolibri.go b/provider-middleware/kolibri.go index 7d20b1e0..4db0c5b7 100644 --- a/provider-middleware/kolibri.go +++ b/provider-middleware/kolibri.go @@ -47,6 +47,7 @@ func NewKolibriService(provider *models.ProviderPlatform, params *map[string]int ProviderPlatformID: provider.ID, AccountID: provider.AccountID, db: conn, + BaseURL: provider.BaseUrl, JobParams: params, } } diff --git a/provider-middleware/kolibri_data.go b/provider-middleware/kolibri_data.go index 652765dc..70fcafc6 100644 --- a/provider-middleware/kolibri_data.go +++ b/provider-middleware/kolibri_data.go @@ -3,6 +3,7 @@ package main import ( "UnlockEdv2/src/models" "bytes" + "context" "encoding/base64" "encoding/json" "errors" @@ -21,9 +22,9 @@ type KolibriResponse[T any] struct { TotalPages int `json:"total_pages"` } -func (srv *ServiceHandler) LookupProvider(id int) (*models.ProviderPlatform, error) { +func (srv *ServiceHandler) LookupProvider(ctx context.Context, id int) (*models.ProviderPlatform, error) { var provider models.ProviderPlatform - err := srv.db.Where("id = ?", id).First(&provider).Error + err := srv.db.WithContext(ctx).Where("id = ?", id).First(&provider).Error if err != nil { log.Println("Failed to find provider") return nil, err diff --git a/provider-middleware/main.go b/provider-middleware/main.go index f1386c49..6f5e2630 100644 --- a/provider-middleware/main.go +++ b/provider-middleware/main.go @@ -2,6 +2,7 @@ package main import ( "UnlockEdv2/src/models" + "context" "fmt" "net/http" "os" @@ -28,16 +29,23 @@ type ProviderServiceInterface interface { GetJobParams() *map[string]interface{} } +type OpenContentProviderServiceInterface interface { + ImportLibraries(ctx context.Context, db *gorm.DB) error + GetJobParams() *map[string]interface{} +} + /** * Handler struct that will be passed to our HTTP server handlers * to handle the different routes. * It will have a refernce to the KolibriService struct **/ type ServiceHandler struct { - nats *nats.Conn - Mux *http.ServeMux - token string - db *gorm.DB + nats *nats.Conn + Mux *http.ServeMux + token string + db *gorm.DB + cancel context.CancelFunc + ctx context.Context } func newServiceHandler(token string, db *gorm.DB) *ServiceHandler { @@ -53,11 +61,14 @@ func newServiceHandler(token string, db *gorm.DB) *ServiceHandler { log.Fatalf("Failed to connect to NATS: %v", err) } log.Println("Connected to NATS at ", options.Url) + ctx, cancel := context.WithCancel(context.Background()) return &ServiceHandler{ - token: token, - db: db, - nats: conn, - Mux: http.NewServeMux(), + token: token, + db: db, + nats: conn, + Mux: http.NewServeMux(), + cancel: cancel, + ctx: ctx, } } @@ -76,7 +87,6 @@ func main() { if err != nil { log.Fatalf("Failed to connect to PostgreSQL database: %v", err) } - log.Println("Connected to the PostgreSQL database") token := os.Getenv("PROVIDER_SERVICE_KEY") handler := newServiceHandler(token, db) @@ -84,12 +94,13 @@ func main() { handler.registerRoutes() err = handler.initSubscription() if err != nil { + handler.cancel() log.Fatalf("Failed to subscribe to NATS: %v", err) } - log.Println("Routes registered") initLogging() err = http.ListenAndServe(":8081", handler.Mux) if err != nil { + handler.cancel() log.Fatalf("Failed to start server: %v", err) } } diff --git a/provider-middleware/middleware.go b/provider-middleware/middleware.go index 6da74843..93156771 100644 --- a/provider-middleware/middleware.go +++ b/provider-middleware/middleware.go @@ -2,6 +2,7 @@ package main import ( "UnlockEdv2/src/models" + "context" "encoding/json" "fmt" "net/http" @@ -12,13 +13,13 @@ import ( log "github.com/sirupsen/logrus" ) -func (sh *ServiceHandler) initServiceFromRequest(r *http.Request) (ProviderServiceInterface, error) { +func (sh *ServiceHandler) initServiceFromRequest(ctx context.Context, r *http.Request) (ProviderServiceInterface, error) { id, err := strconv.Atoi(r.URL.Query().Get("id")) if err != nil { log.Printf("Error: %v", err) return nil, fmt.Errorf("failed to find provider: %v", err) } - provider, err := sh.LookupProvider(id) + provider, err := sh.LookupProvider(ctx, id) if err != nil { log.Printf("Error: %v", err) return nil, fmt.Errorf("failed to find provider: %v", err) @@ -32,7 +33,7 @@ func (sh *ServiceHandler) initServiceFromRequest(r *http.Request) (ProviderServi return nil, fmt.Errorf("unsupported provider type: %s", provider.Type) } -func (sh *ServiceHandler) initService(msg *nats.Msg) (ProviderServiceInterface, error) { +func (sh *ServiceHandler) initProviderPlatformService(ctx context.Context, msg *nats.Msg) (ProviderServiceInterface, error) { var body map[string]interface{} if err := json.Unmarshal(msg.Data, &body); err != nil { log.Errorf("failed to unmarshal message: %v", err) @@ -47,11 +48,11 @@ func (sh *ServiceHandler) initService(msg *nats.Msg) (ProviderServiceInterface, return nil, fmt.Errorf("failed to parse job_id: %v", body["job_id"]) } // prior to here, we are unable to cleanup the job - provider, err := sh.LookupProvider(int(providerId)) - log.Println("InitService called") + provider, err := sh.LookupProvider(ctx, int(providerId)) + log.Infoln("InitService called") if err != nil { - log.Printf("Error: %v", err) - sh.cleanupJob(int(providerId), jobId, false) + log.Errorf("error looking up provider platform: %v", err) + sh.cleanupJob(ctx, int(providerId), jobId, false) return nil, fmt.Errorf("failed to find provider: %v", err) } switch provider.Type { @@ -63,31 +64,41 @@ func (sh *ServiceHandler) initService(msg *nats.Msg) (ProviderServiceInterface, return nil, fmt.Errorf("unsupported provider type: %s", provider.Type) } -func (sh *ServiceHandler) authMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - checkHeader := r.Header.Get("Authorization") - if checkHeader == "" || checkHeader != sh.token { - log.Println("Authorization failure") - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - next.ServeHTTP(w, r.WithContext(r.Context())) - }) +func (sh *ServiceHandler) initContentProviderService(msg *nats.Msg) (OpenContentProviderServiceInterface, error) { + var body map[string]interface{} + if err := json.Unmarshal(msg.Data, &body); err != nil { + log.Errorf("failed to unmarshal message: %v", err) + return nil, fmt.Errorf("failed to unmarshal message: %v", err) + } + providerId, ok := body["open_content_provider_id"].(float64) + if !ok { + return nil, fmt.Errorf("failed to parse open_content_provider_id: %v", body["open_content_provider_id"].(float64)) + } + openContentProvider, err := sh.LookupOpenContentProvider(int(providerId)) + if err != nil { + log.Printf("Error looking up content provider: %v", err) + return nil, fmt.Errorf("failed to find open content provider: %v", err) + } + if openContentProvider.Name == models.Kiwix { + return NewKiwixService(openContentProvider, &body), nil + } + return nil, fmt.Errorf("unsupported open content provider type: %s", openContentProvider.Name) } -func (sh *ServiceHandler) cleanupJob(provId int, jobId string, success bool) { - log.Println(fmt.Sprintf("job %s succeeded: %v \n cleaning up task", jobId, success)) +func (sh *ServiceHandler) cleanupJob(ctx context.Context, provId int, jobId string, success bool) { + log.Infof("job %s succeeded?: %v \n cleaning up task", jobId, success) var task models.RunnableTask - if err := sh.db.Model(models.RunnableTask{}).Find(&task, "provider_platform_id = ? AND job_id = ?", provId, jobId).Error; err != nil { + if err := sh.db.WithContext(ctx).Model(models.RunnableTask{}). + Find(&task, "(provider_platform_id = ? AND job_id = ?) OR (open_content_provider_id = ? AND job_id = ?)", provId, jobId, provId, jobId). + Error; err != nil { log.Errorf("failed to fetch task: %v", err) return } - task.Status = models.StatusPending if success { task.LastRun = time.Now() } - if err := sh.db.Save(&task).Error; err != nil { + if err := sh.db.WithContext(ctx).Save(&task).Error; err != nil { log.Errorf("failed to update task: %v", err) return } diff --git a/provider-middleware/middleware_helpers.go b/provider-middleware/middleware_helpers.go new file mode 100644 index 00000000..57204329 --- /dev/null +++ b/provider-middleware/middleware_helpers.go @@ -0,0 +1,18 @@ +package main + +import ( + "UnlockEdv2/src/models" + + log "github.com/sirupsen/logrus" +) + +const TIMEOUT_WAIT = 5 + +func (sh *ServiceHandler) LookupOpenContentProvider(id int) (*models.OpenContentProvider, error) { + var provider models.OpenContentProvider + if err := sh.db.Model(models.OpenContentProvider{}).Find(&provider, id).Error; err != nil { + log.Errorf("Failed to fetch open content provider %v", err) + return nil, err + } + return &provider, nil +}