From af52085ea3c9fc28e896ebcd192cd36fb49a449d Mon Sep 17 00:00:00 2001 From: Elshimy Ziad Magdy Taha Date: Mon, 26 Jan 2026 17:33:48 +0500 Subject: [PATCH] fix errors and restructure project --- Makefile | 13 + apps/cron-analyzer/cmd/cli/main.go | 2 +- apps/cron-analyzer/go.mod | 12 +- apps/cron-analyzer/internal/analyze-jobs.go | 8 +- go.work | 12 +- internal/{pkg => }/browser/browser.go | 174 ++- internal/{pkg => }/browser/launch-chrome.go | 0 internal/domain/go.mod | 5 + internal/{pkg => }/domain/google-link.go | 0 internal/{pkg => }/domain/job-description.go | 0 .../{pkg => }/domain/job-with-description.go | 0 internal/{pkg => }/domain/job.go | 0 internal/dto/jobs.go | 32 +- internal/{pkg => }/gemini/gemini.go | 2 +- internal/{pkg => }/gemini/go.mod | 2 +- internal/{pkg => }/gemini/go.sum | 0 internal/{pkg => }/infrastructure/db.go | 0 internal/{pkg => }/infrastructure/go.mod | 10 +- internal/{pkg => }/infrastructure/go.sum | 0 internal/{pkg => }/infrastructure/http/job.go | 2 +- .../infrastructure/http/middlewares.go | 0 .../infrastructure/rabbitmq/messages.go | 0 .../infrastructure/rabbitmq/rabbitmq.go | 16 +- internal/{pkg => }/openai/README.md | 4 +- internal/openai/go.mod | 10 + internal/{pkg => }/openai/go.sum | 0 internal/{pkg => }/openai/interface.go | 2 +- internal/{pkg => }/openai/openai.go | 2 +- internal/openrouter/go.mod | 12 + internal/{pkg => }/openrouter/go.sum | 0 internal/{pkg => }/openrouter/openrouter.go | 163 ++- internal/pkg/domain/go.mod | 5 - internal/pkg/openai/go.mod | 10 - internal/pkg/openrouter/go.mod | 12 - internal/pkg/utils/go.mod | 5 - internal/utils/go.mod | 5 + internal/{pkg => }/utils/http.go | 0 internal/{pkg => }/utils/pagination.go | 0 .../{pkg => }/utils/retryable-http-request.go | 0 libs/browser-automation/package-lock.json | 1146 +++++++++++++++++ libs/browser-automation/package.json | 25 + libs/browser-automation/src/browser.ts | 389 ++++++ libs/browser-automation/src/index.ts | 18 + .../src/openrouter/index.ts | 12 + .../src/openrouter/mock-data.ts | 102 ++ .../src/openrouter/openrouter.ts | 256 ++++ .../src/openrouter/types.ts | 104 ++ .../src/utils/chrome-launcher.ts | 144 +++ libs/browser-automation/src/utils/helpers.ts | 14 + libs/browser-automation/tsconfig.json | 34 + libs/ports/go.mod | 4 +- libs/ports/job-commands.go | 9 +- libs/ports/job-queries.go | 2 +- libs/rabbitmq-ts/package-lock.json | 102 ++ libs/rabbitmq-ts/package.json | 21 + libs/rabbitmq-ts/src/index.ts | 173 +++ libs/rabbitmq-ts/tsconfig.json | 29 + libs/repo/go.mod | 4 +- libs/repo/job-analysis-result.go | 2 +- libs/repo/job-description.go | 2 +- libs/repo/job.go | 53 +- services/api/cmd/server/main.go | 10 +- services/api/go.mod | 12 +- services/api/internal/app/app.go | 5 +- .../internal/commands/job/apply-for-job.go | 44 + .../api/internal/commands/job/create-job.go | 8 +- services/api/pkg/http/job.go | 41 +- services/api/pkg/http/middlewares.go | 12 +- services/api/pkg/swagger/docs.go | 66 +- services/api/pkg/swagger/swagger.json | 66 +- services/api/pkg/swagger/swagger.yaml | 45 +- services/job-applier-ts/.env | 17 + services/job-applier-ts/CV.pdf | Bin 0 -> 114464 bytes services/job-applier-ts/cv.txt | 65 + services/job-applier-ts/package-lock.json | 287 +++++ services/job-applier-ts/package.json | 25 + services/job-applier-ts/src/applier/work.ts | 347 +++++ services/job-applier-ts/src/index.ts | 91 ++ services/job-applier-ts/tsconfig.json | 34 + .../analyzer/get-job-description.go | 8 +- services/scraper-google/go.mod | 4 +- services/scraper-google/main.go | 5 +- .../setup-infrastructure.go | 2 +- services/scraper-google/utils/build-url.go | 27 +- services/scraper-google/utils/get-html-raw.go | 4 +- services/scraper-google/work.go | 15 +- services/scraper-hiringcafe/index.ts | 5 +- services/scraper-hiringcafe/utils/browser.ts | 4 +- services/scraper-linkedin/go.mod | 12 +- services/scraper-linkedin/main.go | 6 +- .../scraper-linkedin/pipeline/job_pipeline.go | 2 +- .../pipeline/job_pipeline_workers.go | 2 +- services/scraper-linkedin/pipeline/scraper.go | 4 +- 93 files changed, 4250 insertions(+), 190 deletions(-) rename internal/{pkg => }/browser/browser.go (58%) rename internal/{pkg => }/browser/launch-chrome.go (100%) create mode 100644 internal/domain/go.mod rename internal/{pkg => }/domain/google-link.go (100%) rename internal/{pkg => }/domain/job-description.go (100%) rename internal/{pkg => }/domain/job-with-description.go (100%) rename internal/{pkg => }/domain/job.go (100%) rename internal/{pkg => }/gemini/gemini.go (98%) rename internal/{pkg => }/gemini/go.mod (94%) rename internal/{pkg => }/gemini/go.sum (100%) rename internal/{pkg => }/infrastructure/db.go (100%) rename internal/{pkg => }/infrastructure/go.mod (58%) rename internal/{pkg => }/infrastructure/go.sum (100%) rename internal/{pkg => }/infrastructure/http/job.go (99%) rename internal/{pkg => }/infrastructure/http/middlewares.go (100%) rename internal/{pkg => }/infrastructure/rabbitmq/messages.go (100%) rename internal/{pkg => }/infrastructure/rabbitmq/rabbitmq.go (94%) rename internal/{pkg => }/openai/README.md (97%) create mode 100644 internal/openai/go.mod rename internal/{pkg => }/openai/go.sum (100%) rename internal/{pkg => }/openai/interface.go (94%) rename internal/{pkg => }/openai/openai.go (99%) create mode 100644 internal/openrouter/go.mod rename internal/{pkg => }/openrouter/go.sum (100%) rename internal/{pkg => }/openrouter/openrouter.go (50%) delete mode 100644 internal/pkg/domain/go.mod delete mode 100644 internal/pkg/openai/go.mod delete mode 100644 internal/pkg/openrouter/go.mod delete mode 100644 internal/pkg/utils/go.mod create mode 100644 internal/utils/go.mod rename internal/{pkg => }/utils/http.go (100%) rename internal/{pkg => }/utils/pagination.go (100%) rename internal/{pkg => }/utils/retryable-http-request.go (100%) create mode 100644 libs/browser-automation/package-lock.json create mode 100644 libs/browser-automation/package.json create mode 100644 libs/browser-automation/src/browser.ts create mode 100644 libs/browser-automation/src/index.ts create mode 100644 libs/browser-automation/src/openrouter/index.ts create mode 100644 libs/browser-automation/src/openrouter/mock-data.ts create mode 100644 libs/browser-automation/src/openrouter/openrouter.ts create mode 100644 libs/browser-automation/src/openrouter/types.ts create mode 100644 libs/browser-automation/src/utils/chrome-launcher.ts create mode 100644 libs/browser-automation/src/utils/helpers.ts create mode 100644 libs/browser-automation/tsconfig.json create mode 100644 libs/rabbitmq-ts/package-lock.json create mode 100644 libs/rabbitmq-ts/package.json create mode 100644 libs/rabbitmq-ts/src/index.ts create mode 100644 libs/rabbitmq-ts/tsconfig.json create mode 100644 services/api/internal/commands/job/apply-for-job.go create mode 100644 services/job-applier-ts/.env create mode 100644 services/job-applier-ts/CV.pdf create mode 100644 services/job-applier-ts/cv.txt create mode 100644 services/job-applier-ts/package-lock.json create mode 100644 services/job-applier-ts/package.json create mode 100644 services/job-applier-ts/src/applier/work.ts create mode 100644 services/job-applier-ts/src/index.ts create mode 100644 services/job-applier-ts/tsconfig.json diff --git a/Makefile b/Makefile index ccf6782..1b462c0 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ API_DIR=./services/api SCRAPER_LINKEDIN_DIR=./services/scraper-linkedin SCRAPER_GLASSDOOR_DIR=./services/scraper-glassdoor SCRAPER_PLAYWRIGHT_DIR=./services/scraper-playwright +JOB_APPLIER_BROWSER_USE_DIR=./services/job-applier-browser-use CRON_ANALYZER_DIR=./apps/cron-analyzer CV_ANALYZER_DIR=./apps/cv-analyzer DOCS_DIR=./services/api/pkg/swagger @@ -71,6 +72,18 @@ docker-scraper-playwright: ## Build and run Playwright scraper in Docker @echo "Running Playwright scraper in Docker..." @docker run --rm --name playwright-scraper playwright-scraper +.PHONY: setup-job-applier-browser-use +setup-job-applier-browser-use: ## Setup the browser-use job applier service + @echo "Setting up browser-use job applier service..." + @cd $(JOB_APPLIER_BROWSER_USE_DIR) && uv venv && uv pip install -e . + @echo "Installing Chromium browser..." + @cd $(JOB_APPLIER_BROWSER_USE_DIR) && uvx browser-use install + +.PHONY: run-job-applier-browser-use +run-job-applier-browser-use: ## Run the browser-use job applier service + @echo "Running browser-use job applier service..." + @cd $(JOB_APPLIER_BROWSER_USE_DIR) && uv run python -m src.main + # Apps .PHONY: run-cron-analyzer run-cron-analyzer: ## Run the job analysis cron service diff --git a/apps/cron-analyzer/cmd/cli/main.go b/apps/cron-analyzer/cmd/cli/main.go index 41124c8..1b9b2c4 100644 --- a/apps/cron-analyzer/cmd/cli/main.go +++ b/apps/cron-analyzer/cmd/cli/main.go @@ -4,7 +4,7 @@ import ( "log" "github.com/jobs-scraper/apps/cron-analyzer/internal" - "github.com/jobs-scraper/internal/pkg/infrastructure" + "github.com/jobs-scraper/internal/infrastructure" "github.com/joho/godotenv" ) diff --git a/apps/cron-analyzer/go.mod b/apps/cron-analyzer/go.mod index ec40919..7cff5f2 100644 --- a/apps/cron-analyzer/go.mod +++ b/apps/cron-analyzer/go.mod @@ -3,9 +3,9 @@ module github.com/jobs-scraper/apps/cron-analyzer go 1.24.0 require ( - github.com/jobs-scraper/internal/pkg/domain v0.0.0 - github.com/jobs-scraper/internal/pkg/infrastructure v0.0.0-00010101000000-000000000000 - github.com/jobs-scraper/internal/pkg/openrouter v0.0.0-00010101000000-000000000000 + github.com/jobs-scraper/internal/domain v0.0.0 + github.com/jobs-scraper/internal/infrastructure v0.0.0-00010101000000-000000000000 + github.com/jobs-scraper/internal/openrouter v0.0.0-00010101000000-000000000000 github.com/jobs-scraper/libs/repo v0.0.0 github.com/joho/godotenv v1.5.1 ) @@ -20,10 +20,10 @@ require ( github.com/rabbitmq/amqp091-go v1.10.0 // indirect ) -replace github.com/jobs-scraper/internal/pkg/domain => ../../internal/pkg/domain +replace github.com/jobs-scraper/internal/domain => ../../internal/domain -replace github.com/jobs-scraper/internal/pkg/infrastructure => ../../internal/pkg/infrastructure +replace github.com/jobs-scraper/internal/infrastructure => ../../internal/infrastructure -replace github.com/jobs-scraper/internal/pkg/openrouter => ../../internal/pkg/openrouter +replace github.com/jobs-scraper/internal/openrouter => ../../internal/openrouter replace github.com/jobs-scraper/libs/repo => ../../libs/repo diff --git a/apps/cron-analyzer/internal/analyze-jobs.go b/apps/cron-analyzer/internal/analyze-jobs.go index d04d3fe..aca8d45 100644 --- a/apps/cron-analyzer/internal/analyze-jobs.go +++ b/apps/cron-analyzer/internal/analyze-jobs.go @@ -8,10 +8,10 @@ import ( // "path/filepath" - "github.com/jobs-scraper/internal/pkg/domain" - "github.com/jobs-scraper/internal/pkg/infrastructure/rabbitmq" - "github.com/jobs-scraper/internal/pkg/openai" - "github.com/jobs-scraper/internal/pkg/openrouter" + "github.com/jobs-scraper/internal/domain" + "github.com/jobs-scraper/internal/infrastructure/rabbitmq" + "github.com/jobs-scraper/internal/openai" + "github.com/jobs-scraper/internal/openrouter" "github.com/jobs-scraper/libs/repo" ) diff --git a/go.work b/go.work index 998a0ea..782eee9 100644 --- a/go.work +++ b/go.work @@ -3,12 +3,12 @@ go 1.24.0 use ( . ./apps/cron-analyzer - ./internal/pkg/domain - ./internal/pkg/gemini - ./internal/pkg/infrastructure - ./internal/pkg/openai - ./internal/pkg/openrouter - ./internal/pkg/utils + ./internal/domain + ./internal/gemini + ./internal/infrastructure + ./internal/openai + ./internal/openrouter + ./internal/utils ./libs/ports ./libs/repo ./libs/server diff --git a/internal/pkg/browser/browser.go b/internal/browser/browser.go similarity index 58% rename from internal/pkg/browser/browser.go rename to internal/browser/browser.go index d8f4a62..47fe16d 100644 --- a/internal/pkg/browser/browser.go +++ b/internal/browser/browser.go @@ -5,9 +5,11 @@ import ( "log" "math/rand" "os/exec" + // "strings" "time" "github.com/chromedp/cdproto/cdp" + "github.com/chromedp/cdproto/dom" "github.com/chromedp/cdproto/page" "github.com/chromedp/chromedp" ) @@ -116,7 +118,8 @@ func (b *Browser) GetNodes(selector string) ([]*cdp.Node, error) { var nodes []*cdp.Node err := chromedp.Run(b.ctx, - chromedp.Nodes("[data-snc]", &nodes, chromedp.ByQueryAll), + chromedp.WaitVisible(selector, chromedp.ByQueryAll), + chromedp.Nodes(selector, &nodes, chromedp.ByQueryAll), ) if err != nil { @@ -126,6 +129,22 @@ func (b *Browser) GetNodes(selector string) ([]*cdp.Node, error) { return nodes, nil } +// GetOuterHTML gets the outer HTML of the first element matching the selector +func (b *Browser) GetOuterHTML(selector string) (string, error) { + var html string + + err := chromedp.Run(b.ctx, + chromedp.WaitVisible(selector, chromedp.ByQuery), + chromedp.OuterHTML(selector, &html, chromedp.ByQuery), + ) + + if err != nil { + return "", err + } + + return html, nil +} + func (b *Browser) GetTextFromNode(node *cdp.Node, selector string) (string, error) { var text string @@ -151,6 +170,8 @@ func (b *Browser) GetAttributeFromNode(node *cdp.Node, selector string, attr str return "", err } + // value = strings.ReplaceAll(value, "/apply", "") + return value, nil } @@ -229,13 +250,154 @@ func (b *Browser) WaitForNetworkIdle(timeout time.Duration) error { } } +func (b *Browser) NodeToString(node *cdp.Node) (string, error) { + var outerHTML string + + err := chromedp.Run(b.ctx, + chromedp.ActionFunc(func(ctx context.Context) error { + h, err := dom.GetOuterHTML().WithNodeID(node.NodeID).Do(ctx) + if err != nil { + return err + } + outerHTML = h + return nil + }), + ) + + if err != nil { + return "", err + } + + return outerHTML, err +} + +func (b *Browser) Type(selector string, value string) error { + err := chromedp.Run(b.ctx, + chromedp.WaitVisible(selector), + chromedp.SendKeys(selector, value), + ) + if err != nil { + return err + } + + return nil +} + +func (b *Browser) Click(selector string) error { + err := chromedp.Run(b.ctx, + chromedp.WaitVisible(selector), + chromedp.Click(selector), + ) + if err != nil { + return err + } + + return nil +} + +// Select chooses an option from a element by value + */ + async select(selector: string, value: string): Promise { + const page = this.getPage(); + await this.waitVisible(selector); + await page.select(selector, value); + } + + /** + * Set a checkbox to checked or unchecked state + */ + async setCheckbox(selector: string, checked: boolean): Promise { + const page = this.getPage(); + await this.waitVisible(selector); + + const isChecked = await page.$eval( + selector, + (el) => (el as HTMLInputElement).checked + ); + + if (isChecked !== checked) { + await page.click(selector); + } + } + + /** + * Click a radio button to select it + */ + async setRadio(selector: string): Promise { + const page = this.getPage(); + await this.waitVisible(selector); + await page.click(selector); + } + + /** + * Upload a file to a file input + */ + async uploadFile(selector: string, filePath: string): Promise { + const page = this.getPage(); + await this.waitReady(selector); + + const input = (await page.$(selector)) as ElementHandle | null; + if (!input) { + throw new Error(`File input not found: ${selector}`); + } + + await input.uploadFile(filePath); + } + + /** + * Set the value of an input field directly (useful for hidden, date, color, etc.) + */ + async setValue(selector: string, value: string): Promise { + const page = this.getPage(); + await this.waitReady(selector); + + await page.$eval( + selector, + (el, val) => { + (el as HTMLInputElement).value = val; + el.dispatchEvent(new Event("input", { bubbles: true })); + el.dispatchEvent(new Event("change", { bubbles: true })); + }, + value + ); + } + + /** + * Clear the value of an input field + */ + async clear(selector: string): Promise { + const page = this.getPage(); + await this.waitVisible(selector); + + // Triple-click to select all text, then delete + await page.click(selector, { clickCount: 3 }); + await page.keyboard.press("Backspace"); + } + + /** + * Focus on an element + */ + async focus(selector: string): Promise { + const page = this.getPage(); + await this.waitVisible(selector); + await page.focus(selector); + } + + /** + * Take a screenshot + */ + async screenshot(path?: string): Promise { + const page = this.getPage(); + const buffer = await page.screenshot({ path }); + return buffer as Buffer; + } + + /** + * Random delay between min and max milliseconds + */ + async randomDelay(minMs: number, maxMs: number): Promise { + await randomDelay(minMs, maxMs); + } + + /** + * Scroll to an element + */ + async scrollToElement(selector: string): Promise { + const page = this.getPage(); + await this.waitVisible(selector); + await page.$eval(selector, (el) => { + el.scrollIntoView({ behavior: "smooth", block: "center" }); + }); + } + + /** + * Check if an element exists + */ + async exists(selector: string): Promise { + const page = this.getPage(); + const element = await page.$(selector); + return element !== null; + } + + /** + * Get the count of elements matching a selector + */ + async count(selector: string): Promise { + const page = this.getPage(); + const elements = await page.$$(selector); + return elements.length; + } + + /** + * Press a keyboard key + */ + async pressKey(key: string): Promise { + const page = this.getPage(); + await page.keyboard.press(key as any); + } + + /** + * Triple-click to select all text in an input + */ + async selectAllText(selector: string): Promise { + const page = this.getPage(); + await this.waitVisible(selector); + await page.click(selector, { clickCount: 3 }); + } + + /** + * Clear and type new value (common pattern) + */ + async clearAndType(selector: string, value: string): Promise { + await this.clear(selector); + await this.type(selector, value); + } +} diff --git a/libs/browser-automation/src/index.ts b/libs/browser-automation/src/index.ts new file mode 100644 index 0000000..b8f71b2 --- /dev/null +++ b/libs/browser-automation/src/index.ts @@ -0,0 +1,18 @@ +export { Browser, BrowserOptions } from "./browser"; +export { + launchChromeWithDebugging, + waitForChromeReady, + getChromeWebSocketUrl, + ChromeVersionResponse, +} from "./utils/chrome-launcher"; +export { randomDelay, sleep } from "./utils/helpers"; +export { + OpenRouterService, + OpenRouterServiceOptions, + FieldType, + FormField, + FormExtractionResult, + JobAnalysisResult, + JobDescription, + getMockFormData, +} from "./openrouter"; diff --git a/libs/browser-automation/src/openrouter/index.ts b/libs/browser-automation/src/openrouter/index.ts new file mode 100644 index 0000000..ae7ffaf --- /dev/null +++ b/libs/browser-automation/src/openrouter/index.ts @@ -0,0 +1,12 @@ +export { OpenRouterService, OpenRouterServiceOptions } from "./openrouter"; +export { + FieldType, + FormField, + FormExtractionResult, + JobAnalysisResult, + JobDescription, + OpenRouterMessage, + OpenRouterRequest, + OpenRouterResponse, +} from "./types"; +export { getMockFormData } from "./mock-data"; diff --git a/libs/browser-automation/src/openrouter/mock-data.ts b/libs/browser-automation/src/openrouter/mock-data.ts new file mode 100644 index 0000000..d1492d6 --- /dev/null +++ b/libs/browser-automation/src/openrouter/mock-data.ts @@ -0,0 +1,102 @@ +import { FormExtractionResult } from "./types"; + +/** + * Returns mock form extraction result for testing + */ +export function getMockFormData(): FormExtractionResult { + return { + fields: [ + { + label: "Resume", + field_name: "resume", + field_type: "file", + value: "", + placeholder: "", + required: true, + selector: "input[data-ui='resume']", + }, + { + label: "Cover letter", + field_name: "cover_letter", + field_type: "textarea", + value: + "Dear KOMOJU Hiring Team,\n\nI am excited to apply for the Fullstack Developer position at KOMOJU. With over 3 years of experience building web applications and leading development projects, I am confident I can contribute to your Fraud Prevention Team's mission of protecting merchants and customers across borders.\n\nMy experience includes leading the development of online shopping platforms, mentoring junior developers, and coordinating complex projects. I have hands-on experience with modern web technologies including React, Node.js, and microservices architecture. I am a motivated self-starter who can work independently while collaborating effectively with cross-functional teams.\n\nI am eager to bring my technical skills and passion for product-focused development to KOMOJU's innovative payment gateway solutions.\n\nThank you for your consideration.\n\nBest regards,\nZiad Elshimy", + placeholder: "", + required: false, + selector: "#cover_letter", + }, + { + label: + "Do you have at least 2 years of personal or professional experience with Ruby?", + field_name: "QA_10609776", + field_type: "radio", + value: "false", + placeholder: "", + required: true, + selector: "input[name='QA_10609776']", + }, + { + label: "Could you tell us more about your Ruby experience?", + field_name: "QA_10609777", + field_type: "textarea", + value: + "I do not have professional Ruby experience, but I have extensive experience with JavaScript (React, Node.js), TypeScript, and Go. I am a fast learner with strong fundamentals in software development best practices, data structures, and algorithms. I am excited about the opportunity to learn Ruby and contribute to KOMOJU's fraud prevention systems.", + placeholder: "", + required: true, + selector: "#QA_10609777", + }, + { + label: + "Have you ever investigated and resolved a production issue that wasn't reproducible in a development or staging environment?", + field_name: "QA_10609885", + field_type: "radio", + value: "true", + placeholder: "", + required: true, + selector: "input[name='QA_10609885']", + }, + { + label: + "If the answer for above question is yes, can you tell us how did you approach it?", + field_name: "QA_10609886", + field_type: "textarea", + value: + "Yes, I have experience debugging production issues. My approach includes: 1) Analyzing production logs and error reports to identify patterns, 2) Using monitoring tools to track system behavior and performance metrics, 3) Creating targeted tests to reproduce the issue in isolated environments, 4) Collaborating with team members to validate hypotheses, and 5) Implementing and deploying fixes with proper testing and monitoring. I understand the importance of being systematic and thorough when diagnosing complex production issues.", + placeholder: "", + required: true, + selector: "#QA_10609886", + }, + { + label: "Do you currently reside in Japan?", + field_name: "QA_10609774", + field_type: "radio", + value: "false", + placeholder: "", + required: true, + selector: "input[name='QA_10609774']", + }, + { + label: + "This role requires at least 5 hours of overlap with Japan Standard Time (JST) business hours. Are you able to accommodate this?", + field_name: "QA_10609887", + field_type: "radio", + value: "true", + placeholder: "", + required: true, + selector: "input[name='QA_10609887']", + }, + { + label: + "Are you willing to relocate to Japan? If so, what are your motivations for doing so?", + field_name: "QA_10609888", + field_type: "textarea", + value: + "Yes, I am willing to relocate to Japan. My motivations include: 1) Joining KOMOJU's innovative team working on cutting-edge payment technology, 2) Experiencing Japan's world-class technology culture and work ethic, 3) Contributing to a product that powers payments for major platforms like Steam and TikTok, 4) Growing my career in an international environment with diverse perspectives, and 5) Embracing the opportunity to learn Japanese culture and language while working on globally impactful projects.", + placeholder: "", + required: true, + selector: "#QA_10609888", + }, + ], + apply_button: `button[data-ui="apply-button"]`, + }; +} diff --git a/libs/browser-automation/src/openrouter/openrouter.ts b/libs/browser-automation/src/openrouter/openrouter.ts new file mode 100644 index 0000000..a797380 --- /dev/null +++ b/libs/browser-automation/src/openrouter/openrouter.ts @@ -0,0 +1,256 @@ +import { + FormExtractionResult, + JobAnalysisResult, + JobDescription, + OpenRouterRequest, + OpenRouterResponse, +} from "./types"; + +export interface OpenRouterServiceOptions { + model: string; + apiKey: string; +} + +export class OpenRouterService { + private model: string; + private apiKey: string; + private baseUrl = "https://openrouter.ai/api/v1/chat/completions"; + + constructor(options: OpenRouterServiceOptions) { + this.model = options.model; + this.apiKey = options.apiKey; + } + + /** + * Clean markdown code blocks from response + */ + private cleanMarkdownCodeBlocks(content: string): string { + content = content.trim(); + + if (content.startsWith("```json")) { + content = content.slice(7); + } else if (content.startsWith("```")) { + content = content.slice(3); + } + + if (content.endsWith("```")) { + content = content.slice(0, -3); + } + + return content.trim(); + } + + /** + * Make a request to OpenRouter API + */ + private async makeRequest( + systemMessage: string, + userMessage: string + ): Promise { + const request: OpenRouterRequest = { + model: this.model, + messages: [ + { role: "system", content: systemMessage }, + { role: "user", content: userMessage }, + ], + }; + + const response = await fetch(this.baseUrl, { + method: "POST", + headers: { + Authorization: `Bearer ${this.apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `OpenRouter API error: ${response.status} - ${errorText}` + ); + } + + const data = (await response.json()) as OpenRouterResponse; + + if (!data.choices || data.choices.length === 0) { + throw new Error("No response choices received from API"); + } + + const content = data.choices[0].message.content; + + if (content.startsWith("<")) { + throw new Error( + `API returned HTML/XML instead of JSON: ${content.slice(0, 200)}` + ); + } + + return this.cleanMarkdownCodeBlocks(content); + } + + /** + * Analyze a job description against a CV + */ + async analyzeJobDescription( + cv: string, + jobDesc: JobDescription + ): Promise { + const userMessage = `Analyze the following CV against the job description and criteria, then provide a recommendation following the schema below. +1) If there are missing skills, try to guess if they still match based on similar skills or experience in the cv. +for example: Javascript is mentioned in the cv, but the job requires Vanilla js, since they are the same thing, it should be included in the matching skills. + +2) The job shouldn't require any language skills, preferbly only english. + +3) The job should be remote, or provide relocation to the country. + +CV: +${cv} + +Job Description: +${jobDesc.description} + +Job Criteria (key-value): +${JSON.stringify(jobDesc.criteria, null, 2)} + +CRITICAL OUTPUT REQUIREMENTS: +- Return ONLY raw JSON - NO markdown formatting whatsoever +- NO backticks, NO code blocks, NO json prefix +- NO additional text before or after the JSON +- Start your response directly with { and end with } +- Use this exact schema and key names: +{ + "recommendation": "apply" | "do_not_apply", + "confidence_score": number, // integer 0-100 + "matching_skills": [string], + "missing_skills": [string], + "experience_match": "excellent" | "good" | "fair" | "poor", + "summary": string, + "improvement_suggestions": [string] +}`; + + const systemMessage = + "You are an expert HR assistant specializing in job application analysis. You help candidates determine if they should apply for specific positions based on their CV and the job requirements. CRITICAL: You must respond with ONLY raw JSON - no markdown, no code blocks, no backticks, no additional text. Start directly with { and end with }."; + + const jsonContent = await this.makeRequest(systemMessage, userMessage); + + console.log("Raw API response:", jsonContent); + + const result = JSON.parse(jsonContent) as JobAnalysisResult; + return result; + } + + /** + * Create a job description from HTML content + */ + async createJobDescription(htmlContent: string): Promise { + const userMessage = `You are a job posting data extractor. Analyze the HTML content and extract data according to this JSON structure: +{"description":"job description text","criteria":{"title":"Job Title","company":"Company","location":"Location","salary":"Salary","skills":"Skills","experience":"Experience","job_type":"Job Type","remote":"Remote"}} + +CRITICAL RULES: +1. Return ONLY valid JSON - no markdown, no code blocks, no backticks, no explanations +2. Use SINGLE-LINE JSON (no pretty printing, no newlines inside the JSON) +3. All criteria values MUST be strings (never use booleans, numbers, or arrays) +4. Properly escape all quotes inside strings using \\" +5. Use only standard ASCII quotes ("), never smart quotes (" " ' ') +7. If no job description is found, use empty string for "description": "" +8. All criteria fields must be present with empty string "" if not found + +Example output: +{"description":"Develop software applications...","criteria":{"title":"Backend Engineer","company":"Tech Corp","location":"Remote","salary":"$120k","skills":"Go, Docker","experience":"3+ years","job_type":"Full-time","remote":"Yes"}} + +Extract from this HTML Content: +${htmlContent}`; + + const systemMessage = + "You are an expert job description extractor. You convert unstructured job description text into a structured JSON format. CRITICAL: You must respond with ONLY raw JSON - no markdown, no code blocks, no backticks, no additional text. Start directly with { and end with }."; + + const jsonContent = await this.makeRequest(systemMessage, userMessage); + + console.log("Raw API response for job description:", jsonContent); + + const result = JSON.parse(jsonContent) as JobDescription; + return result; + } + + /** + * Extract form fields from HTML and populate them based on CV data. + * Also adjusts the CV based on job type (frontend/backend/fullstack). + */ + async applyForJob( + htmlForm: string, + cvMarkdown: string, + jobDescription: string + ): Promise { + const userMessage = `You are a form field extractor, auto-filler, and CV adapter. Analyze the HTML form and job description, then: +1. Extract ALL form fields and populate them with appropriate values from the CV + +HTML FORM: +${htmlForm} + +JOB DESCRIPTION: +${jobDescription} + +ORIGINAL CV (Markdown): +${cvMarkdown} + +CV ADJUSTMENT RULES: +- Detect if the job is: FRONTEND, BACKEND, or FULLSTACK based on the job description +- If FRONTEND: Keep only frontend-related skills, projects, and experience (React, Vue, Angular, CSS, HTML, UI/UX, etc.). Remove backend-specific content. +- If BACKEND: Keep only backend-related skills, projects, and experience (APIs, databases, servers, Go, Node.js, Python, etc.). Remove frontend-specific content. +- If FULLSTACK: Keep both frontend and backend content. +- For any OTHER job type (not frontend/backend/fullstack): Treat as FRONTEND by default. +- Maintain the same markdown structure and formatting as the original CV. +- Do NOT invent new skills or experience - only filter existing content. + +CRITICAL RULES: +1. Return ONLY valid JSON - no markdown, no code blocks, no backticks, no explanations +2. Extract ALL input fields, textareas, selects, and buttons from the form +3. For each field, determine the appropriate value from the ADJUSTED CV +4. If a field cannot be populated from the CV (e.g., password, captcha), leave value as empty string +5. Identify the apply/submit button text +6. Use the exact JSON schema below +7. If a field is already populated, ignore it (don't send it in the results) + +JSON Schema: +{ + "fields": [ + { + "label": "Field label or name attribute", + "field_name": "name or id attribute of the input", + "field_type": "text|email|tel|password|number|date|datetime-local|time|month|week|url|search|color|range|file|hidden|checkbox|radio|select|textarea|button|submit|reset", + "value": "Value to populate based on CV data", + "placeholder": "Placeholder text if any", + "required": true|false, + "selector": "Unique CSS selector for the input (e.g., #email, input[name='email'], .form-field-email). Use id selector if available, otherwise name attribute, otherwise class. Empty string if no unique selector can be determined." + } + ], + "apply_button": "Unique CSS selector for the form's submit button", +} + +FIELD MAPPING GUIDELINES: +- Name fields: Extract full name, first name, last name from CV +- Email: Use email from CV contact info +- Phone: Use phone number from CV +- LinkedIn/Portfolio/Website: Use URLs from CV +- Experience/Years: Calculate from CV work history +- Current company/title: Use most recent from CV +- Skills: List relevant skills from ADJUSTED CV +- Education: Use education details from CV +- Cover letter/Message: Generate a brief professional message based on ADJUSTED CV +- Salary expectations: Leave empty unless specified in CV +- Location/Address: Use from CV contact info +- Resume/CV upload: Leave value empty (file upload) + +Start your response with { and end with }`; + + const systemMessage = + "You are an expert form analyzer and auto-filler. You extract form fields from HTML and intelligently populate them with data from a CV/resume. CRITICAL: You must respond with ONLY raw JSON - no markdown, no code blocks, no backticks, no additional text. Start directly with { and end with }."; + + const jsonContent = await this.makeRequest(systemMessage, userMessage); + + console.log("Raw API response for form extraction:", jsonContent); + + const result = JSON.parse(jsonContent) as FormExtractionResult; + return result; + } +} diff --git a/libs/browser-automation/src/openrouter/types.ts b/libs/browser-automation/src/openrouter/types.ts new file mode 100644 index 0000000..be432fc --- /dev/null +++ b/libs/browser-automation/src/openrouter/types.ts @@ -0,0 +1,104 @@ +/** + * FieldType represents HTML input field types + */ +export type FieldType = + | "text" + | "email" + | "tel" + | "password" + | "number" + | "date" + | "datetime-local" + | "time" + | "month" + | "week" + | "url" + | "search" + | "color" + | "range" + | "file" + | "hidden" + | "checkbox" + | "radio" + | "select" + | "textarea" + | "button" + | "submit" + | "reset"; + +/** + * FormField represents a single form field extracted from HTML + */ +export interface FormField { + label: string; + field_name: string; + field_type: FieldType; + value: string; + placeholder?: string; + required: boolean; + selector: string; +} + +/** + * FormExtractionResult represents the structured response from form extraction + */ +export interface FormExtractionResult { + fields: FormField[]; + apply_button: string; +} + +/** + * JobAnalysisResult represents the structured response from job analysis + */ +export interface JobAnalysisResult { + recommendation: "apply" | "do_not_apply"; + confidence_score: number; + matching_skills: string[]; + missing_skills: string[]; + experience_match: "excellent" | "good" | "fair" | "poor"; + summary: string; + improvement_suggestions: string[]; +} + +/** + * JobDescription represents a parsed job description + */ +export interface JobDescription { + description: string; + criteria: Record; +} + +/** + * OpenRouter API message format + */ +export interface OpenRouterMessage { + role: "system" | "user" | "assistant"; + content: string; +} + +/** + * OpenRouter API request body + */ +export interface OpenRouterRequest { + model: string; + messages: OpenRouterMessage[]; +} + +/** + * OpenRouter API response + */ +export interface OpenRouterResponse { + id: string; + choices: { + message: { + role: string; + content: string; + }; + finish_reason: string; + }[]; + usage?: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }; +} diff --git a/libs/browser-automation/src/utils/chrome-launcher.ts b/libs/browser-automation/src/utils/chrome-launcher.ts new file mode 100644 index 0000000..dbc4d8c --- /dev/null +++ b/libs/browser-automation/src/utils/chrome-launcher.ts @@ -0,0 +1,144 @@ +import fs from "fs"; +import path from "path"; +import os from "os"; +import { spawn, ChildProcess } from "child_process"; +import net from "net"; + +export interface ChromeVersionResponse { + webSocketDebuggerUrl: string; +} + +const userDataDir = path.join(os.tmpdir(), "chrome-debug-profile"); + +function getChromePath(): string { + const platform = os.platform(); + + const paths: Record = { + darwin: [ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", + "/Applications/Chromium.app/Contents/MacOS/Chromium", + ], + win32: [ + process.env.LOCALAPPDATA + "\\Google\\Chrome\\Application\\chrome.exe", + process.env.PROGRAMFILES + "\\Google\\Chrome\\Application\\chrome.exe", + process.env["PROGRAMFILES(X86)"] + + "\\Google\\Chrome\\Application\\chrome.exe", + ], + linux: [ + "/usr/bin/google-chrome", + "/usr/bin/google-chrome-stable", + "/usr/bin/chromium", + "/usr/bin/chromium-browser", + ], + aix: [], + freebsd: [], + openbsd: [], + sunos: [], + android: [], + haiku: [], + cygwin: [], + netbsd: [], + }; + + const platformPaths = paths[platform] || []; + + for (const chromePath of platformPaths) { + try { + if (fs.existsSync(chromePath)) { + return chromePath; + } + } catch { + continue; + } + } + + throw new Error("Could not find Chrome/Chromium installation"); +} + +export function launchChromeWithDebugging( + port: number = 9222, + headless: boolean = false +): ChildProcess { + try { + const chromePath = getChromePath(); + const args = [ + `--remote-debugging-port=${port}`, + `--user-data-dir=${userDataDir}`, + "--remote-allow-origins=*", + "--incognito", + ]; + + if (headless) { + args.push("--headless=new"); + } + + console.log(`Launching Chrome at: ${chromePath}`); + + const chromeProcess = spawn(chromePath, args, { + detached: true, + stdio: "ignore", + }); + + chromeProcess.unref(); + + chromeProcess.on("error", (err) => { + console.error("Failed to start Chrome:", err); + }); + + chromeProcess.on("exit", (code) => { + if (code !== 0) { + console.error(`Chrome process exited with code ${code}`); + } + }); + + console.log(`Chrome launched successfully with debugging port ${port}`); + return chromeProcess; + } catch (error) { + console.error("Failed to launch Chrome:", error); + throw error; + } +} + +export async function waitForChromeReady( + port: number = 9222, + timeout: number = 30000 +): Promise { + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + function check() { + const client = net.createConnection({ port }, () => { + client.end(); + resolve(); + }); + + client.on("error", () => { + if (Date.now() - startTime > timeout) { + reject(new Error("Timeout waiting for Chrome to start")); + } else { + setTimeout(check, 500); + } + }); + } + + check(); + }); +} + +export async function getChromeWebSocketUrl( + port: number = 9222 +): Promise { + const response = await fetch(`http://localhost:${port}/json/version`, { + headers: { + Origin: "", + }, + }); + + if (!response.ok) { + throw new Error("Failed to connect to Chrome debugging port"); + } + + const chromeInstance = (await response.json()) as ChromeVersionResponse; + return chromeInstance.webSocketDebuggerUrl; +} diff --git a/libs/browser-automation/src/utils/helpers.ts b/libs/browser-automation/src/utils/helpers.ts new file mode 100644 index 0000000..8a31c84 --- /dev/null +++ b/libs/browser-automation/src/utils/helpers.ts @@ -0,0 +1,14 @@ +/** + * Random delay between min and max milliseconds + */ +export function randomDelay(minMs: number, maxMs: number): Promise { + const delay = Math.floor(Math.random() * (maxMs - minMs) + minMs); + return new Promise((resolve) => setTimeout(resolve, delay)); +} + +/** + * Sleep for a specified number of milliseconds + */ +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/libs/browser-automation/tsconfig.json b/libs/browser-automation/tsconfig.json new file mode 100644 index 0000000..2c37fd3 --- /dev/null +++ b/libs/browser-automation/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "exactOptionalPropertyTypes": false, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedIndexedAccess": false + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/libs/ports/go.mod b/libs/ports/go.mod index 1bb16ea..e1bee67 100644 --- a/libs/ports/go.mod +++ b/libs/ports/go.mod @@ -4,6 +4,6 @@ go 1.24.0 toolchain go1.24.7 -require github.com/jobs-scraper/internal/pkg/domain v0.0.0 +require github.com/jobs-scraper/internal/domain v0.0.0 -replace github.com/jobs-scraper/internal/pkg/domain => ../../internal/pkg/domain +replace github.com/jobs-scraper/internal/domain => ../../internal/domain diff --git a/libs/ports/job-commands.go b/libs/ports/job-commands.go index 9a834c0..32988b9 100644 --- a/libs/ports/job-commands.go +++ b/libs/ports/job-commands.go @@ -1,9 +1,10 @@ package ports -import "github.com/jobs-scraper/internal/pkg/domain" +import "github.com/jobs-scraper/internal/domain" type JobCommands struct { - CreateJob JobCommandHandler + CreateJob JobCommandHandler + ApplyForJob ApplyForJobCommandHandler } // CreateJobCommand represents the command to create a job @@ -19,3 +20,7 @@ type CreateJobCommand struct { type JobCommandHandler interface { Handle(cmd CreateJobCommand) error } + +type ApplyForJobCommandHandler interface { + Handle(jobId int) error +} diff --git a/libs/ports/job-queries.go b/libs/ports/job-queries.go index 6a37de0..9f6886c 100644 --- a/libs/ports/job-queries.go +++ b/libs/ports/job-queries.go @@ -2,7 +2,7 @@ package ports import ( "github.com/jobs-scraper/internal/dto" - "github.com/jobs-scraper/internal/pkg/domain" + "github.com/jobs-scraper/internal/domain" ) type JobQueries struct { diff --git a/libs/rabbitmq-ts/package-lock.json b/libs/rabbitmq-ts/package-lock.json new file mode 100644 index 0000000..8c962f7 --- /dev/null +++ b/libs/rabbitmq-ts/package-lock.json @@ -0,0 +1,102 @@ +{ + "name": "@jobs-scraper/rabbitmq-ts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@jobs-scraper/rabbitmq-ts", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "amqplib": "^0.10.9" + }, + "devDependencies": { + "@types/amqplib": "^0.10.8", + "typescript": "^5.9.3" + } + }, + "node_modules/@types/amqplib": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.8.tgz", + "integrity": "sha512-vtDp8Pk1wsE/AuQ8/Rgtm6KUZYqcnTgNvEHwzCkX8rL7AGsC6zqAfKAAJhUZXFhM/Pp++tbnUHiam/8vVpPztA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/amqplib": { + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.9.tgz", + "integrity": "sha512-jwSftI4QjS3mizvnSnOrPGYiUnm1vI2OP1iXeOUz5pb74Ua0nbf6nPyyTzuiCLEE3fMpaJORXh2K/TQ08H5xGA==", + "license": "MIT", + "dependencies": { + "buffer-more-ints": "~1.0.0", + "url-parse": "~1.5.10" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", + "license": "MIT" + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + } + } +} diff --git a/libs/rabbitmq-ts/package.json b/libs/rabbitmq-ts/package.json new file mode 100644 index 0000000..892b739 --- /dev/null +++ b/libs/rabbitmq-ts/package.json @@ -0,0 +1,21 @@ +{ + "name": "@jobs-scraper/rabbitmq-ts", + "version": "1.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "amqplib": "^0.10.9" + }, + "devDependencies": { + "@types/amqplib": "^0.10.8", + "typescript": "^5.9.3" + } +} diff --git a/libs/rabbitmq-ts/src/index.ts b/libs/rabbitmq-ts/src/index.ts new file mode 100644 index 0000000..c17d39b --- /dev/null +++ b/libs/rabbitmq-ts/src/index.ts @@ -0,0 +1,173 @@ +import * as amqp from "amqplib"; + +export interface RabbitMQConfig { + url: string; + queueName: string; + exchangeName: string; + exchangeType: "direct" | "topic" | "fanout" | "headers"; + durable: boolean; +} + +export interface SearchQuery { + keywords: string; + location: string; + fwt?: string; + numPages: number; +} + +export class RabbitMQClient { + private connection: amqp.ChannelModel | null = null; + private channel: amqp.Channel | null = null; + private config: RabbitMQConfig; + + constructor(config: RabbitMQConfig) { + this.config = config; + } + + async connect(): Promise { + try { + console.log(`Connecting to RabbitMQ at: ${this.config.url}`); + this.connection = await amqp.connect(this.config.url); + console.log("Connected to RabbitMQ successfully"); + + if (!this.connection) { + throw new Error("Failed to establish RabbitMQ connection"); + } + + this.channel = await this.connection.createChannel(); + + if (!this.channel) { + throw new Error("Failed to create RabbitMQ channel"); + } + + // Ensure exchange and queue exist + await this.channel.assertExchange( + this.config.exchangeName, + this.config.exchangeType, + { durable: this.config.durable } + ); + + await this.channel.assertQueue(this.config.queueName, { + durable: this.config.durable, + arguments: { + "x-dead-letter-exchange": "scraper_dlx", + "x-dead-letter-routing-key": this.config.queueName, + "x-message-ttl": 24 * 60 * 60 * 1000, // 24 hours in milliseconds + "x-max-retries": 3, + }, + }); + + await this.channel.bindQueue( + this.config.queueName, + this.config.exchangeName, + this.config.queueName + ); + + // Set prefetch to process one message at a time + await this.channel.prefetch(1); + + // Handle connection events + this.connection.on("close", () => { + console.log("RabbitMQ connection closed"); + this.connection = null; + this.channel = null; + }); + + this.connection.on("error", (error: Error) => { + console.error("RabbitMQ connection error:", error); + this.connection = null; + this.channel = null; + }); + } catch (error) { + console.error("Failed to connect to RabbitMQ:", error); + throw error; + } + } + + async subscribe( + messageHandler: (message: T) => Promise + ): Promise { + if (!this.channel) { + throw new Error( + "RabbitMQ channel not initialized. Call connect() first." + ); + } + + console.log( + `Waiting for messages from ${this.config.queueName}. To exit press CTRL+C` + ); + + await this.channel.consume( + this.config.queueName, + async (msg: amqp.ConsumeMessage | null) => { + if (msg && this.channel) { + try { + const messageContent = msg.content.toString(); + console.log(`Received message: ${messageContent}`); + + const message: T = JSON.parse(messageContent); + console.log(`Processing message:`, message); + + // Process the message using the provided handler + await messageHandler(message); + + // Acknowledge the message on success + this.channel.ack(msg); + console.log("Message processed successfully"); + } catch (error) { + console.error("Error processing message:", error); + // Reject the message and don't requeue it + if (this.channel) { + this.channel.nack(msg, false, false); + } + } + } + } + ); + } + + async close(): Promise { + try { + if (this.channel) { + try { + await this.channel.close(); + } catch { + // Channel may already be closing + } + this.channel = null; + } + if (this.connection) { + try { + await this.connection.close(); + } catch { + // Connection may already be closing + } + this.connection = null; + } + console.log("RabbitMQ connection closed gracefully"); + } catch (error) { + console.error("Error closing RabbitMQ connection:", error); + } + } + + isConnected(): boolean { + return this.connection !== null && this.channel !== null; + } +} + +export function createRabbitMQConfig(): RabbitMQConfig { + return { + url: process.env.RABBITMQ_URL || "amqp://guest:guest@localhost:5672/", + queueName: process.env.GLASSDOOR_QUEUE_NAME || "scraper.glassdoor", + exchangeName: process.env.SCRAPER_EXCHANGE_NAME || "scraper_exchange", + exchangeType: "topic", + durable: true, + }; +} + +export async function createRabbitMQClient(): Promise { + const config = createRabbitMQConfig(); + const client = new RabbitMQClient(config); + await client.connect(); + return client; +} diff --git a/libs/rabbitmq-ts/tsconfig.json b/libs/rabbitmq-ts/tsconfig.json new file mode 100644 index 0000000..86d80d6 --- /dev/null +++ b/libs/rabbitmq-ts/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "exactOptionalPropertyTypes": false, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedIndexedAccess": false + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/libs/repo/go.mod b/libs/repo/go.mod index 18e388c..c134728 100644 --- a/libs/repo/go.mod +++ b/libs/repo/go.mod @@ -5,8 +5,8 @@ go 1.24.0 toolchain go1.24.7 require ( - github.com/jobs-scraper/internal/pkg/domain v0.0.0 + github.com/jobs-scraper/internal/domain v0.0.0 github.com/lib/pq v1.10.9 ) -replace github.com/jobs-scraper/internal/pkg/domain => ../../internal/pkg/domain +replace github.com/jobs-scraper/internal/domain => ../../internal/domain diff --git a/libs/repo/job-analysis-result.go b/libs/repo/job-analysis-result.go index 8ba0f28..8f46e37 100644 --- a/libs/repo/job-analysis-result.go +++ b/libs/repo/job-analysis-result.go @@ -4,7 +4,7 @@ import ( "database/sql" "fmt" - "github.com/jobs-scraper/internal/pkg/domain" + "github.com/jobs-scraper/internal/domain" "github.com/lib/pq" ) diff --git a/libs/repo/job-description.go b/libs/repo/job-description.go index f1cbc68..24c0b30 100644 --- a/libs/repo/job-description.go +++ b/libs/repo/job-description.go @@ -6,7 +6,7 @@ import ( "fmt" "strings" - "github.com/jobs-scraper/internal/pkg/domain" + "github.com/jobs-scraper/internal/domain" ) type JobDescriptionRepository struct { diff --git a/libs/repo/job.go b/libs/repo/job.go index 650f4d7..1d923ea 100644 --- a/libs/repo/job.go +++ b/libs/repo/job.go @@ -3,11 +3,12 @@ package repo import ( "bytes" "database/sql" + "encoding/json" "fmt" "strconv" "strings" - "github.com/jobs-scraper/internal/pkg/domain" + "github.com/jobs-scraper/internal/domain" ) type JobRepository struct { @@ -104,7 +105,7 @@ type JobFilter struct { } func (r *JobRepository) GetAllJobs(filter JobFilter) ([]domain.Job, int, error) { - query := "SELECT id, title, company, company_link, location, job_link, job_timestamp FROM jobs" + query := "SELECT id, title, company, company_link, location, job_link, job_timestamp, provider FROM jobs" countQuery := "SELECT COUNT(*) FROM jobs" var conditions []string var args []interface{} @@ -164,7 +165,7 @@ func (r *JobRepository) GetAllJobs(filter JobFilter) ([]domain.Job, int, error) var jobs []domain.Job for rows.Next() { var job domain.Job - if err := rows.Scan(&job.ID, &job.Title, &job.Company, &job.CompanyLink, &job.Location, &job.JobLink, &job.JobPostTime); err != nil { + if err := rows.Scan(&job.ID, &job.Title, &job.Company, &job.CompanyLink, &job.Location, &job.JobLink, &job.JobPostTime, &job.Provider); err != nil { return nil, 0, fmt.Errorf("error scanning job row: %v", err) } jobs = append(jobs, job) @@ -180,7 +181,7 @@ func (r *JobRepository) GetJobByID(id int) (*domain.Job, error) { var job domain.Job sqlStatement := ` - SELECT id, title, company, company_link, location, job_link, job_timestamp + SELECT id, title, company, company_link, location, job_link, job_timestamp, provider FROM jobs WHERE id = $1 ` @@ -193,6 +194,7 @@ func (r *JobRepository) GetJobByID(id int) (*domain.Job, error) { &job.Location, &job.JobLink, &job.JobPostTime, + &job.Provider, ) if err == sql.ErrNoRows { @@ -206,6 +208,49 @@ func (r *JobRepository) GetJobByID(id int) (*domain.Job, error) { return &job, nil } +func (r *JobRepository) GetJobWithDescription(id int) (*domain.JobWithDescription, error) { + var job domain.JobWithDescription + var criteriaJSON []byte + + sqlStatement := ` + SELECT j.id, j.title, j.company, j.company_link, j.location, j.job_link, j.job_timestamp, j.provider, + COALESCE(jd.description, ''), COALESCE(jd.job_criteria, '{}'::jsonb) + FROM jobs j + LEFT JOIN job_descriptions jd ON j.id = jd.job_id + WHERE j.id = $1 + ` + + err := r.db.QueryRow(sqlStatement, id).Scan( + &job.Job.ID, + &job.Job.Title, + &job.Job.Company, + &job.Job.CompanyLink, + &job.Job.Location, + &job.Job.JobLink, + &job.Job.JobPostTime, + &job.Job.Provider, + &job.JobDescription.Description, + &criteriaJSON, + ) + + if err == sql.ErrNoRows { + return nil, fmt.Errorf("job with ID %d not found", id) + } + + if err != nil { + return nil, fmt.Errorf("error querying job: %v", err) + } + + // Parse criteria JSON + if err := json.Unmarshal(criteriaJSON, &job.JobDescription.Criteria); err != nil { + job.JobDescription.Criteria = make(map[string]string) + } + + job.JobDescription.JobID = job.Job.ID + + return &job, nil +} + func prepareQueryCreateBulk(s string, models []*domain.Job) (string, []interface{}) { bf := bytes.Buffer{} values := make([]interface{}, 0, len(models)*7) diff --git a/services/api/cmd/server/main.go b/services/api/cmd/server/main.go index 8cbc436..9c11df6 100644 --- a/services/api/cmd/server/main.go +++ b/services/api/cmd/server/main.go @@ -10,8 +10,8 @@ import ( "time" "github.com/gorilla/mux" - "github.com/jobs-scraper/internal/pkg/infrastructure" - "github.com/jobs-scraper/internal/pkg/infrastructure/rabbitmq" + "github.com/jobs-scraper/internal/infrastructure" + "github.com/jobs-scraper/internal/infrastructure/rabbitmq" "github.com/jobs-scraper/libs/repo" "github.com/jobs-scraper/services/api/internal/app" httpHandler "github.com/jobs-scraper/services/api/pkg/http" @@ -64,12 +64,12 @@ func main() { router := mux.NewRouter() - // Swagger endpoint - router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler) - router.Use(httpHandler.CORSMiddleware) router.Use(httpHandler.LogsMiddleware) + // Swagger endpoint + router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler) + app := app.NewApplication(router, db, rmq) // Create job analysis result repository diff --git a/services/api/go.mod b/services/api/go.mod index b586643..020b5b2 100644 --- a/services/api/go.mod +++ b/services/api/go.mod @@ -5,9 +5,9 @@ go 1.24.0 require ( github.com/gorilla/mux v1.8.1 github.com/gorilla/schema v1.4.1 - github.com/jobs-scraper/internal/pkg/domain v0.0.0 - github.com/jobs-scraper/internal/pkg/infrastructure v0.0.0-00010101000000-000000000000 - github.com/jobs-scraper/internal/pkg/utils v0.0.0 + github.com/jobs-scraper/internal/domain v0.0.0 + github.com/jobs-scraper/internal/infrastructure v0.0.0-00010101000000-000000000000 + github.com/jobs-scraper/internal/utils v0.0.0 github.com/jobs-scraper/libs/ports v0.0.0 github.com/jobs-scraper/libs/repo v0.0.0 github.com/jobs-scraper/libs/server v0.0.0-00010101000000-000000000000 @@ -39,11 +39,11 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect ) -replace github.com/jobs-scraper/internal/pkg/domain => ../../internal/pkg/domain +replace github.com/jobs-scraper/internal/domain => ../../internal/domain -replace github.com/jobs-scraper/internal/pkg/infrastructure => ../../internal/pkg/infrastructure +replace github.com/jobs-scraper/internal/infrastructure => ../../internal/infrastructure -replace github.com/jobs-scraper/internal/pkg/utils => ../../internal/pkg/utils +replace github.com/jobs-scraper/internal/utils => ../../internal/utils replace github.com/jobs-scraper/libs/ports => ../../libs/ports diff --git a/services/api/internal/app/app.go b/services/api/internal/app/app.go index 95c7bbc..84699b0 100644 --- a/services/api/internal/app/app.go +++ b/services/api/internal/app/app.go @@ -4,7 +4,7 @@ import ( "database/sql" "github.com/gorilla/mux" - "github.com/jobs-scraper/internal/pkg/infrastructure/rabbitmq" + "github.com/jobs-scraper/internal/infrastructure/rabbitmq" "github.com/jobs-scraper/libs/ports" "github.com/jobs-scraper/libs/repo" "github.com/jobs-scraper/libs/server" @@ -30,7 +30,8 @@ func NewApplication(router *mux.Router, db *sql.DB, rmq *rabbitmq.RabbitMQClient Router: router, DB: db, JobCommands: &ports.JobCommands{ - CreateJob: jobCommands.NewCreateJobHandler(jobRepo, rmq), + CreateJob: jobCommands.NewCreateJobHandler(jobRepo, rmq), + ApplyForJob: jobCommands.NewApplyForJobHandler(jobRepo, rmq), }, JobQueries: &ports.JobQueries{ GetJobs: jobQueries.NewGetJobsHandler(jobRepo), diff --git a/services/api/internal/commands/job/apply-for-job.go b/services/api/internal/commands/job/apply-for-job.go new file mode 100644 index 0000000..668a2ce --- /dev/null +++ b/services/api/internal/commands/job/apply-for-job.go @@ -0,0 +1,44 @@ +package job + +import ( + "encoding/json" + + "github.com/jobs-scraper/internal/dto" + "github.com/jobs-scraper/internal/infrastructure/rabbitmq" + "github.com/jobs-scraper/libs/repo" +) + +type ApplyForJob struct { + jobRepo *repo.JobRepository + rmq *rabbitmq.RabbitMQClient +} + +func NewApplyForJobHandler(jobRepo *repo.JobRepository, rmq *rabbitmq.RabbitMQClient) *ApplyForJob { + return &ApplyForJob{ + jobRepo: jobRepo, + rmq: rmq, + } +} + +func (aj *ApplyForJob) Handle(jobId int) error { + job, err := aj.jobRepo.GetJobWithDescription(jobId) + + if err != nil { + return err + } + + jobDTO := dto.JobWithDescriptionFromDomain(*job) + jsonData, err := json.Marshal(jobDTO) + + if err != nil { + return err + } + + err = aj.rmq.Publish(rabbitmq.JobApplierQueue, rabbitmq.ScraperExchange, jsonData) + + if err != nil { + return err + } + + return nil +} diff --git a/services/api/internal/commands/job/create-job.go b/services/api/internal/commands/job/create-job.go index d0a1bb7..c730fe4 100644 --- a/services/api/internal/commands/job/create-job.go +++ b/services/api/internal/commands/job/create-job.go @@ -5,8 +5,8 @@ import ( "errors" "log" - "github.com/jobs-scraper/internal/pkg/domain" - "github.com/jobs-scraper/internal/pkg/infrastructure/rabbitmq" + "github.com/jobs-scraper/internal/domain" + "github.com/jobs-scraper/internal/infrastructure/rabbitmq" "github.com/jobs-scraper/libs/ports" "github.com/jobs-scraper/libs/repo" ) @@ -33,25 +33,21 @@ func (cj *CreateJob) Handle(cmd ports.CreateJobCommand) error { jsonData, err := json.Marshal(jobRequest) if err != nil { - log.Printf("Error marshaling JSON: %v", err) return err } switch cmd.Provider { case domain.LinkedIn: if err := cj.rmq.Publish(rabbitmq.LinkedInQueue, rabbitmq.ScraperExchange, jsonData); err != nil { - log.Printf("Error publishing LinkedIn message: %v", err) return err } log.Printf("Published LinkedIn job request: %s", string(jsonData)) case domain.Glassdoor: if err := cj.rmq.Publish(rabbitmq.GlassDoorQueue, rabbitmq.ScraperExchange, jsonData); err != nil { - log.Printf("Error publishing Glassdoor message: %v", err) return err } log.Printf("Published Glassdoor job request: %s", string(jsonData)) default: - log.Printf("Unsupported job provider: %v", cmd.Provider) return errors.New("unsupported job provider") } diff --git a/services/api/pkg/http/job.go b/services/api/pkg/http/job.go index a4daa6b..45c10dc 100644 --- a/services/api/pkg/http/job.go +++ b/services/api/pkg/http/job.go @@ -9,7 +9,7 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/schema" - "github.com/jobs-scraper/internal/pkg/utils" + "github.com/jobs-scraper/internal/utils" "github.com/jobs-scraper/libs/ports" "github.com/jobs-scraper/libs/repo" ) @@ -43,6 +43,7 @@ func (h *JobHandler) RegisterRoutes(router *mux.Router) { jobs.HandleFunc("/{id}/analysis", utils.Make(h.GetJobAnalysisResult)).Methods("GET") jobs.HandleFunc("/analysis/top-matches", utils.Make(h.GetTopMatches)).Methods("GET") jobs.HandleFunc("", utils.Make(h.GetJobs)).Methods("GET") + jobs.HandleFunc("/apply/{id}", utils.Make(h.ApplyForJob)).Methods("POST") } // CreateJob handles the creation of a new job @@ -73,6 +74,44 @@ func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) error { return nil } +// Apply applies for a job by id +// @Summary applies for a job +// @Description Applies for a job with the provided id +// @Tags jobs +// @Param id query string false "Job ID" +// @Accept json +// @Produce json +// @Success 201 {object} map[string]string "Successfully applied for job" +// @Failure 400 {object} map[string]string "Invalid request data" +// @Failure 500 {object} map[string]string "Internal server error" +// @Router /jobs/apply/{id} [post] +func (h *JobHandler) ApplyForJob(w http.ResponseWriter, r *http.Request) error { + jobId := r.URL.Query().Get("id") + + if jobId == "" { + slog.Error("Job ID is required") + return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("job ID is required")) + } + + jobIdInt, err := strconv.Atoi(jobId) + + if err != nil { + slog.Error("Invalid Job ID", "error", err) + return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid job ID")) + } + + err = h.jobCommands.ApplyForJob.Handle(jobIdInt) + + if err != nil { + slog.Error(err.Error()) + return utils.NewAPIError(http.StatusInternalServerError, fmt.Errorf("Internal server error")) + } + + utils.WriteJSON(w, http.StatusCreated, map[string]string{"message": "Success"}) + + return nil +} + // GetJobs handles the retrieval of all jobs // @Summary Gets all jobs // @Description Gets all jobs with the provided specifications diff --git a/services/api/pkg/http/middlewares.go b/services/api/pkg/http/middlewares.go index 3b6e0da..bcf4c7d 100644 --- a/services/api/pkg/http/middlewares.go +++ b/services/api/pkg/http/middlewares.go @@ -13,11 +13,17 @@ import ( func CORSMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Set CORS headers - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With") + origin := r.Header.Get("Origin") + if origin == "" { + origin = "*" + } + + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With, X-API-Key") w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Access-Control-Max-Age", "86400") + w.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Range") // Handle preflight OPTIONS requests if r.Method == "OPTIONS" { diff --git a/services/api/pkg/swagger/docs.go b/services/api/pkg/swagger/docs.go index 1cf7ef5..10a6e2d 100644 --- a/services/api/pkg/swagger/docs.go +++ b/services/api/pkg/swagger/docs.go @@ -77,31 +77,25 @@ const docTemplate = `{ } ], "responses": { - "201": { + "200": { "description": "Successfully retrieved jobs", "schema": { "type": "object", - "additionalProperties": { - "type": "string" - } + "additionalProperties": true } }, "400": { "description": "Invalid request data", "schema": { "type": "object", - "additionalProperties": { - "type": "string" - } + "additionalProperties": true } }, "500": { "description": "Internal server error", "schema": { "type": "object", - "additionalProperties": { - "type": "string" - } + "additionalProperties": true } } } @@ -241,6 +235,58 @@ const docTemplate = `{ } } }, + "/jobs/apply/{id}": { + "post": { + "description": "Applies for a job with the provided id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "jobs" + ], + "summary": "applies for a job", + "parameters": [ + { + "type": "string", + "description": "Job ID", + "name": "id", + "in": "query" + } + ], + "responses": { + "201": { + "description": "Successfully applied for job", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid request data", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/jobs/{id}/analysis": { "get": { "description": "Retrieves the analysis result for a specific job", diff --git a/services/api/pkg/swagger/swagger.json b/services/api/pkg/swagger/swagger.json index 8ec2ac5..e8c25fb 100644 --- a/services/api/pkg/swagger/swagger.json +++ b/services/api/pkg/swagger/swagger.json @@ -70,31 +70,25 @@ } ], "responses": { - "201": { + "200": { "description": "Successfully retrieved jobs", "schema": { "type": "object", - "additionalProperties": { - "type": "string" - } + "additionalProperties": true } }, "400": { "description": "Invalid request data", "schema": { "type": "object", - "additionalProperties": { - "type": "string" - } + "additionalProperties": true } }, "500": { "description": "Internal server error", "schema": { "type": "object", - "additionalProperties": { - "type": "string" - } + "additionalProperties": true } } } @@ -234,6 +228,58 @@ } } }, + "/jobs/apply/{id}": { + "post": { + "description": "Applies for a job with the provided id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "jobs" + ], + "summary": "applies for a job", + "parameters": [ + { + "type": "string", + "description": "Job ID", + "name": "id", + "in": "query" + } + ], + "responses": { + "201": { + "description": "Successfully applied for job", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Invalid request data", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/jobs/{id}/analysis": { "get": { "description": "Retrieves the analysis result for a specific job", diff --git a/services/api/pkg/swagger/swagger.yaml b/services/api/pkg/swagger/swagger.yaml index 103a931..511fa1b 100644 --- a/services/api/pkg/swagger/swagger.yaml +++ b/services/api/pkg/swagger/swagger.yaml @@ -107,23 +107,20 @@ paths: produces: - application/json responses: - "201": + "200": description: Successfully retrieved jobs schema: - additionalProperties: - type: string + additionalProperties: true type: object "400": description: Invalid request data schema: - additionalProperties: - type: string + additionalProperties: true type: object "500": description: Internal server error schema: - additionalProperties: - type: string + additionalProperties: true type: object summary: Gets all jobs tags: @@ -253,4 +250,38 @@ paths: summary: Get top matching jobs tags: - jobs + /jobs/apply/{id}: + post: + consumes: + - application/json + description: Applies for a job with the provided id + parameters: + - description: Job ID + in: query + name: id + type: string + produces: + - application/json + responses: + "201": + description: Successfully applied for job + schema: + additionalProperties: + type: string + type: object + "400": + description: Invalid request data + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal server error + schema: + additionalProperties: + type: string + type: object + summary: applies for a job + tags: + - jobs swagger: "2.0" diff --git a/services/job-applier-ts/.env b/services/job-applier-ts/.env new file mode 100644 index 0000000..d1f2c0a --- /dev/null +++ b/services/job-applier-ts/.env @@ -0,0 +1,17 @@ +DB_HOST=localhost +DB_PORT=5432 +DB_USER=your_db_user +DB_PASSWORD=your_db_password +DB_NAME=linkedin_jobs +DB_SSLMODE=disable + +SERVER_PORT=8080 +SERVER_HOST=localhost + +OPENAI_API_KEY=sk-GtdLX9YBOCsBEgBL4rBONA +OPENAI_BASE_URL=https://hubai.loe.gg/v1 +OPENAI_MODEL=gpt-4o-mini + +CV_AI_MODEL=alibaba/tongyi-deepresearch-30b-a3b:free +OPENROUTER_API_KEY=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 +GEMINI_API_KEY=AIzaSyBkGcsPdqhIrjg-wbLOuhAYmjOCt3A_zxM \ No newline at end of file diff --git a/services/job-applier-ts/CV.pdf b/services/job-applier-ts/CV.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9e072c3b3eb3da705b929d6a45f4b614cdf160f1 GIT binary patch literal 114464 zcmV)WK(4bY*fNFGg%( zbY(5HF*YDRJ_>Vma%Ev{3U~pWz1fZ~JCbI1A5Rf4ps0Ty zfJR@m(CBLc3FundRi_ieIRaYw{(#o{jCA+#pmSGd9wcHLe@GetL`|0cXcwIlAx7*kAeESxK>+9?Kdc9L}yT7S(eSckUS0;LU+8-tN``7z%|5oz+I=#}*{p)dmeE#{t znD@(*I+w@S<8pJXD`UN%l-$3r@APx|y5COcl)RqK=YIRTJZ_KA=k@$`0w+qIU)RU| z`tiJ;zHXPZC6BM0wV#*E*X2$DQ@%W3cGC+UU++}7(e^@(+xzQvK2w7^+;2C~+|%aI z&%b{@pw9Iz>dch(e|&USygpeNX8AsUNMYvv`Rns>0$Y)k=I^uc z_06h0Z}+boBwyjj*X8`G7OjRfynfgN|D(l|Njt>l1ht;G*O#Wo3mPn!9R&WMg>+a7J|Fk5(?j}D;dEh6_pkf);hY)b{cwoK*XdMKct75P zm)q$Hnk>ed5elZk19D!ffi);${aF;cd_S(v`u6pLV8FMtD-C=-ziUpf2YrQ3Rks;IoYV zM8&B&8%&D_lI!yI8h3>2PD}cHKj{gs1M|RYL|UK-3cf1eD55L!D;94T6rp=JEG}RN z^MTjxDWfsns7}d!eZu0D+^@8y&E+K?R8d6&z~cp~6yw^?AKvR%^h6pD!w& zqq>=HME7<1dS7EDVlF;jSEwJI$I>#J7lL_$8Kmv|=By~W-`+*t7y1(T`NZT%z5D&v z;jq=4bxVt?p`bt;*a)lejCuH3!eYSa)B$_jD?8BX0~ig#ILeaj3YQ}MfmMvm{27tG zfFw!>_#86;2-|?@;~EJHj1Gbrucu_Sfi+YR5Z(b!$O*)I-`fCJrcDbL{f!)01H}k< zJ71vx<%%hKc9@@U=i{Bh(2zNje0s| z%%aqua#Hh+%6cf;ZO#7re50gqdOoe>t&&odVFOI;8)m(!!vNIz-i{Vc8e^ ze>vFz0foDvKCTJU>=AB7f6i7Ti&dZUiw@Gx!c$ff{F zyzcNBelx2J9(aytT<9u^U%V+nGtyr?FOafMD}fX6B=L*UrO*#6BLV?ub8}E_`~uSx50zAv-s83N_GUtP}nZI>bRruL)~{I&6z`SfrIOD^$Wo zj-^jLqw@)45J|SB%w@?{GeC{!=^ePVE2xaglKzWfb)T&fFv8i#$O>qB0R7A=Z>B1r6msztFFOpY@z|4;|>Uu=&+ z*vD1XOAWYF`1F>YfWc8@`Qh1WykM0+O)9+MH^A#Hl2tGbP|Z*FS~1Ml(1+ElU=Ge2 z^-+ou{9%6@z!hh=5(p2=-QT%J+*3>>j zjvNYkl+i#=Cbfj$;#$FDZGxbOa#fC?2vsC;8vQ6hk7p0EtP3O6(BBsdELl8wEuM)R zLAaX5gH9J*3b%%@Y`@kLR4H_0^eAf9p%ka|q9mqmRi*CKW1in%S3Cskv=SzyuWt$R z8Nni78Q@EcY5>tiiJzu-*>OK*i*7g1Oy(j7V_!r`D# zcMz1^5KrOjN%e~UX1A6(#|we|Se3%!N<*{v3$ z!jNa9Hxd#r#Eprtjvb88M#cV#Td0;4Boyb_IF<4V%`m71(i@A3PO^IY9o1p)AZBCq zFsmTgXw8(;e)H&4Ezq@L=mNRe@BlhB(8f=_EIz0q= z?Nlu(a_JDPhn9dY&-@^Kkz3laHiRQCL?_SPL3AIq?TeYMTNt@JnR>rS(Qt<70#jt_ScH`Eybl)han*#cDl_ zI?DL$N~u90G+Q0?3=VL(BC|qLkXZZBb+LszNho2k(Xbt&b!P)WScP)&z;?2*v@qHT zkH);v$C+s3sY(AEeYnC#Z?d3E4Hi~f(8E>S(Q1SZGOOC(vV{OcXQk_-j&YsD9kRz6 zt?jF{DQi`*Em-oCQMFzJe44v@%7VeohQjEPO_F0E#dwz~dAR1t`6FZC)}NnP4(zk^ zc1uhTw&*3WYFP>-|1GuDbgksORlrhwoQV^bK0x81q_GrG`@G{!G#E;;&o}zfOU~}er9OQURps}Zu&~=; zTClUfsCAjejqrl-D}0Gh#jni8_QJ)&=;$>xHwBm_ZC3)Whig)7OEfy*C0Ifbnk9_E&Xl_5JHU8rUr8}ENhDdTTvcp z3swN`)=vP&e-=ARvi9m9q)u3Ctv+Jo0gNP}qZ7@HfuVrdd9><9lM8$@gapB^iZRo^ zxUX-pL#TjC1y&6emWseA$CXK8c{I@s`C@XmU=gxAz-3TiWE?$iYDfy<*iBVrHLxJ0 zY6K_VnT`BP@TH^^+_Q>!8$SxNNV`s%M>h?QTUbXoYYk|kp|%HQOI?@|5*S9+ z)X9x=I;mbMK{YZbL_Bk-lNg=7ZxY#=X0aX25K1h)@9`}5;M zGzl`*#oQb63DxpnqmsH;|*hCMbx=O)6sDX-$ zY}K-$+F2A?px|HBzy#!cFeM+Cv(Y4%4fevjBb?3FDBv{#X*K}bKE0yL8HJqs4{Q_1LD}y)pnEXu6L*VUlf&(iWr<(kkYK-C2Y?F_ z7bYEXZ8h*dIC;&iwUigV4GHNOs6xgtw58gOwPhB-uO(7o3rO2mIc~ZIIpTm5TtQ)+ zH8ed;gith?wbGc0MOA3_NDFl_VxhHI)b3g$Z^K<^FHE^ctTFpqAdglr(p6ta`3^W%6Fg!jK)SdhRq)M88+w#gL)#DGu}=12FwAE zU9Nd&Gl|AT%L%*taZZ@S@ELk9=aq~U&VnG>j^OL_H+&bEay$jcXf;C1P?uTak;Yh7 zxW#!>vRM*j>MbJ68Z8CO8X;6UR8POIYcJ@&$XX!xgAEmQ26aVnmzC33$crn25^Av6rLXBDrW4`Ny>?VoAT_WRvM_K$IKX&fvMePzm$QU< z2pfM8#0yvvE7%6^r<`W5fo{cj6*j$x&?`Gmq=HFZ$|M*>*7Q*FW*>L~j@OHlfNf9- zj@0Pw091dut?U{I#N$qa1I^VgsApY6_~gara8rVMy<;;dq1$`2ca$gry~I|i7)wyW zO+_U7NJ<2Zs`6tgkWZ_iF*T%WPB%>!ybKFlC4pj+Tc9}z&~G^>7yfn6mW7dUKy1}Y z*7+HZjQf@uJHKAy{16LEJRvGPZ^(}&niVAYW$afFzTECiK=YCy8rDj zYU4V8{_T&?|8hG0`}x0q{O$kw{O^DJ*_J>PJ34uE+?LS!fBigY`@o8@vFh6PeE!&v z`#5Zii+{dB|D$nFf4mRp>`@=?fVO#z@9BCtF2QPyVi%#GQ@);$n?mOk#5x-Ge0?5I znavLy#ltC|AOC(lE>R959m}vgDxc5)aom)x`8`hQm{P9p5ohSiBk^dbgSOW{4#!pN z+}%?5w);81U%f5ylwsB-+kxYB0G# z7}}+T6eOsK6(c>-I&?_p)W@a-@x@b1&zcqLxKY%JXRKS)(^^981d~G@I!tKQ!(cK` zQi4viACzWPtYaAo{8_?TBbr{ttOheIx|EpwiC!SH&|^kA&pj);j#_3g{w7QXY zMJtV8>)8_0PgIpq%l-Kdpl)Juo!+*7hX%c*uK-5;!<{#tPYt0BC%i^>M-6I@Nm4^o zLTV98(t-8M0Ne*RfSKOSmL=|c$_AUoaW9sV3;C)v*p$3I*|)@#^3-W1#v&;BFraJ= z)B^rkYT)#sWSj$ktK?>EfRYF9B`qG=_yQYd1+MibGD@6}>|g$IF$~UHvJn%0l12nf z`oP;v+w0R~3qZ6e^%ID$0t}x?zOWtMxQM{N1ZBzPDLxdY1#Ao+>7vzuwhd-YP|;UK z#2zqU&S@#Z#JS{j!CALe3^TvdFL1;xyK#?q69(*lX6jtf;am6^+6_Yk(#(Zz6>BokCGnwMfgl2 zmR&l`0BB0H8hVKbWw33!G^5p{HKRsOfNWn`P}Phi)FkiAVv5vB(cCS!?hxbRrm4w5 z10Rj_T@dJaQNTnwUv?T8Yx)KabNd7zwVobNGrpuk$A5r1%j-N{7AUg7ZVN)XR#Hqx zU-ZCUl;wiSaNmHz6enCs$o7O$$-XbO^H3>5y_nFdO?3EJ2QvIBzSP~JE0mwc? zZzu0vk%|7N1bmwktP)tW6(lIfCZZKUP4WflB?`x}Q)0hVaigwuyFDa;VAuojtz(aB zF|Ut7uSj?j4mr%tyzm3eHzec5Wvf|nK6AQzJl0?uV7sF|S_N1}En`$9FqxPEjX@$6%~BvnY`DF!YQA?P0WZI?32`2Fa%l$xKe+BTWW1!{Q+%ATC@`8 z6yJj*oTQJsOjP}6#BLi!8K`W;e9rvy24hh+PgX+xsjN(^<<<3}%%`|uO2 z0V2hno}2^UJ+l)J&kIAkn{A`?)Ru^!DT2bWw`@$VZs>sB(9r_FInIERZ;9IXio}2f8(t)-O3}4L`xF$;AfzLa}8JC-i@JSL*rG90a6L>3| zhfp{t7|W8-1_KOjwEs~+a+m($S|T-Jezq?6M*9Y(eXHFBXw&0Pq%?(WpV;xbW*$J@ zxyC?{pG!Atx|Ae){N7ahi7g>vcOu6MFT93N38x?pCVE(+O#~*op%Foc+M-~7HzMbL zx0i%~2@8-6UYD9&lU)fG6k&qf_$k3c0QFv9|N1k|LX&4oRVck*i6$4j9}WO z29qI~+asC-rb0Ftg0W8qQrHP7k-&G7hnN&v0Rto)w-7!KYp_JO+ol9UP?BJdv!o6s zD1;nxSBb=0N*03B3Olr+c@?O(E0xZE;}*db*lIxX*l(;O{Eto$Ho;c-jynM^l!O)F z83Cio{(2TKKu@{@NA4)0n@9Q!ri^<*jI4oKFuj=ovKF;q`bXNm3Drx0IoD`okdnlC zo%!q;0uiSHiW4?Y$pxB01&&*42v`G(_=sL_?)MvE;HwDkfX2~Xe}_oPD&f2W=ZUBaGYGc!(Jo)2b#shyK^=*+JKCK z6;YtjQrS^3>OIFqNF|eFQfyFl&DB;(X+6b%f$3EQ%72oW;KB!bP(9Bz20G*)bjVC! zNpEQ|DXl2y>E@SBq+E}6V%m{cUq z(&o66f0bDx8Kde$E)CrY*GhY*_RziY(2zN~QA5~!3CY88&8@=$BW4+x>l2b*5e9~L zh?@ZuKtqW$O9(*kVRiYCAFiC7Qv<~zWnc+SP={%OaIocAN*Id3c@qwtLSV9MN;o`X zp@nX5cFJ^)kN|VdtQg{fHUqP(=?l&hkGW9-Wff-E|43RiS9Y9-?mL(mp=6&Ye8=wx zKDm1nuIow8s|*z^L23t`b6!*CLWdqqzcF^0oi@1(!b+g!@#X9#UMU5TCE!Dw?d~5` z{Mj=z7txtpfGVfJjVhx=SE}oEV_{uHjwJ}Jv1b)gO*&)NctS5=*hG`dyyazV(K}9C z!C5oT0?VqW21dg$OnP8Q_XpKpv7TF#wIp|*>3oDNB zW!o9221n2C(rlqHoq_NC%@}wjwBRs05nj>8+ZAp>F_}d$ca&Y}IJ=g(YmFi^VF&nq zlfBB>Z7^Kjo!zyBX~)?$zH8`C2`y)LExCr|u|-Noh}$6@g(~q_pxT$SE86!(;~Ye@@S>Fc^a$#bNE zzOFCbU$zo z;GHtvYA@0>VOv)X(&Px(-*j}NKRJLgMZpql;VEL!><>STgcxCNhykOKaVBJ4ckK3| z4`83PglJk4ucfar>xQ)3A~n#_IB;mT^pLO+dLiP$SO)Ro2~NM#0&4b3gI)R*D9AO-%wd(`U{MUl~$jCdY`AELxOQTZ2rCB)CP&K?Ar# zdMpTg2hdnb7>mAHV$UX{qOoM$u-TTN5s+(2I8q~bkh$u;wgSE2O@X#n^yw|pZo)^F z#HblCuor#tT9$nY{j2GyqnllZ?F1Qx5fv5&bAYOFCeMU}@Zm5QVk9|>RZL3ET;n*D zm*?FOBP-qOvjs~1=FHX_UVsbCOr??UB*;#6JJphV^EHk43)l5e9WD~o7T9e@M2Qk^ zKhj5)h*=%ShDqsNIHCVIEr2W~+hc!L>C18GBY3k=m#ZOdVtyqhvd30xwCJhB1RWNe z$F|WDw@9rd<9In-SD2pd3c~H5AY5VWw zMX*G+_qszg$l^3-Nf57GBtk;Dh(Qf>N`f^e*_J#C>t9x2u-8y4TvgKcfH_xdc#}5* z%dRw(i1Tj=o@Qu8q=y;?s9nR_nC{jGG`t*yM)YtOUF$IbYi1ro%zd0CMRF<8xr{Jz z)J(_Zb}a?STP#MEoJk#szXHfgc-!g9=HCWL2lV5Z=#*h8kRO2))1rb7H?AqjD)R-~ zf^AwBH6`SNBL!wnVwrS}yq&~RcM#D_qy@Q;b%wy+6V1lXi8(oIj5zdLK-3mbdf1>U zsAb*q3M+7~i6)Ew+#k9CE>(@-rDK=Cdq>XU%nnq7y9MVA8g^3WpuW*hg@q{(mgum1 zuZg%6Fs!I4@c699S$H$R7EmUWSZhGtYQdzWEJH_5*`C`#1zrk8&r>+TdwU0i(j=*< z(90fCsw{Fri?h;LBkY3~lo7%#EMqZC%%|tp+4f);5HcAEVQxMWCPt8?4Thm(teyy& zJ<{NYB4XMrIF2U_r8bhi3Y8u~QbuO2;e~74v3uRpmB3#l8Ket?uz6pum7CA3MAR9Z zwM0whpKIb4uqYH4X2VVk`j(vnf-2GgfgGYLM8?Gf7>RNBSh_Y2p2U4h|T4nt_Tctl4sNI?4_%-pHrs+wP_ViDdH9g4ddmeSWTorWSFd7=UfC)Mv znlM1v3?T-e6;9A0w5%FZm!yZ(sA!hHc(r(AB;zfXv%#pPu_Kst971XV!PH#t1jAU0 zS}Q$ZNr5cjS$VPK(e@vDD&UOu@z>_cctpH)b~s)1tjro_)}h-GdB7HM;C>80pkiYH z3^;f&n=_dfaNz8*PU)6_Lo1QLK-l`>z62c7pBvI1UXa}|Dd5^mkP1pTRi&f?4#K86 z$>Z500duw3Rj7D_SmWzaL4kx!-co_#Tsh+nUWzj{vBM0S3zQ{z)L>fBCGHLAxRSu0 z_}OE{8>CDNKp1WiG=-S&}bSGd7JOB?iX zL#7o}G1sbcrcGp>9vaIQPcLu`9lfxFVa0m5!CuU_ObKy@f?f2AH*8^0bLuom+^OOX z4$HF@G-&%SU^Jm1d;Z``MF?z(tLp+3msEH`n04@HV+$^N0<=qsFlk@N0UgN32v9px zsBlAgAZJ1qZU~QrFDu*-SjH`+>k{0S45b2O-qK{6EmSzCd8Q18|0#j_mJ;TQA|))q znd_;|ULUXOcvV&g^&*`mEKF0wUY4m6FWeRr$LZ#vk8{AnF);~F0&_cAvJkqmQ7>BlP|-P%$IBuoz7mLNU}6X43>p@?WF4?VLAw(2E) zJ~&dGDeA9x9)yUTRvH`bCEoH%oUQ3mj@3P=-{?_~(OaBazfB}xxHi%%(zn3TqcT`4 zaebpl>48@#AWPGuz;3iQdX!P~PKv}uK6Co7W7ae&bG)U4u1Q4#vnZjXNx5-Uhg79I zMcdOYe~&KZmJ(g{u#?J`6RPqjA(pz7gEONLi!!AIWB$W*Bwdlqqn9q!wk` zN}4jYK59V7=&kL2BCe}WxE{2g+2fZxDRZ#!aO9#)y(tw<*OV!+`Kudk3g+AkihhkY z1u4vPy zDhyD?I5Vk5n^H&qU9_oPWW&_7DMQqG&Z143CW}D}Sc2fKOUkntffN4Nadxz+ZF2az zXj4m?rosIdSI#D2tfov0X4P54C{qjAOUatDgAFQ4t0`09JWB+lR`Avub#TzMDf22D z%zWi#7Nq?Bf~5{V9?GgKSS6C2gGHSJ_C$)NPUQlG*hv|^4VQ#7n>v+i!n=z;6^#uc zqEES6nd=OO(a?jc0wg=8#xRE(WGTSgtcZoTD?_jz<=`AW_dk}f2L&qHL^)VtdTAxD zzE_GYLAk^yfRQe_q_mVo{kCy+Y1diUzEY;V73$Q}Bfh6~QUcAoQb*U(=R2oLQA8rx zN}`AeL~;Ef4uXU9D!Nl8!r8T{?iv=Q?J8MHym(3@v@)tL{LHD5 z(!v5S@DdYgCB+=N+5Jixo;*3T=O z5GIHN?kyns!%i4{w!ou(ym>SsWYJuZ=xB+$aVa5Ks;@{1E}M1ehFfc! z68Xn;6Z}bAHI9Npt(z3oj@pu1->9-Wx}!u7W?KTDIf+~j^S~+>k6D8kUWENnk|ZWd ziZqITvGWKmF!6y9zY>&CYLGTWAvBReapiJpi}+oKYq<>=_YxU`OKZ`|Y=s2t z)ImN`SmdCta(zHco~yti<{p9x`vh;ug=9M^NvdH$*71%z7b{*qRn%dd?>m(Fa`7;CIT9#w5iO#~ zk~Z-tKrcN=`r0?GaJefss2Z^&oWAGAWY)X(O~S@dv%WCCEoyHOvmjb91j?rGo17z0 z2m?be!?q%k$8IT*6TSHCS>_@ro+QU;w0#pbkd<^t)I?EwC+67Fo{PNQE#tQ>!49 zgt=r=ecKdLHBW@3xUP3MFjZWZgFf^~Cb~zR8lhVp374^a1(hI+I=;!@t6n z^pbfYRxw0)nPOtTu5gUqcgFtpflYRe(LWIhMx_>bOa@aJi1}!Kd%mw`!2hHj8 z1I-y=dn{xYne##?S{;sU$an(?EgZwSC>G#k6VsrA+g_K0yVHdwxjqM>(*o@;oY(JF zRcifBi9CkuV-=8bLA?MZNN6&!7l53D*Ca%;E&y4QN6IZhCWJ}H36q&wE^M-GTEu9J z7fQYth<6?pU`Q_jNu{e^&JYcf#T6+SWd9f z>)wW~e{qCT*I^^}ipTC+>g~3<^tf+L0GDGb8XdGI;LxuwauIjmnrl>V!l|WVn8p4| z?D_V8Pg)b|C8=@;g#3g4-fG6GVpo{`N!CB5h0=4k?dQ0{du_# zJi?o8JKKsLT>FVPIp4FfIe<1rvNOvb?K#m5QhO=Cx85?U%l)2`oX9e9i*@X6>u(D- zMB9jLrDlzph0Dwnk`sO4G5dAwJ>F9`H{wDz+<3vn`*EL)UAu{v<1<+9l~G4{>qKzr zVLY}}Tg6-NpB+V9+w%v1+V^1k&18iLXOvIHsCx3Rrg~sx!`Ws)NA(atXgBe%_pR#L zwI+LkOLj8yLF?OcYkDwtPM$ox?yYN0*0Jwh{Cx#rKXXs?E-#re+_%=P%`U99{-^LJ{>*09y~|*1@jjm> zcpJIr1-S{{edZ&)QP>=-Vedy{tGMppe%pZ@(rJH-yG1FjnpW?fW;4 zyBiw$<*LPOxjVCr&}J{gbqhDzgRz75H?_AL+XPecA=v{REcc@AM{I6dEwSqHg1Ow6 z8Qw|iCDYc$a&p>*Tj3yEgER!G)F78 z_y$9~y&r3o@2K@cwIR)M203jx#(OHE9$e;gSfjDUdppqwL-8xoez5uWZI`_jGjNLqA8XX#qHVwrmk6)sXy#oXkCxlG={3j82d$^9 zrdPvjR4dhf;QfD1)VZM#u0tJL@2N3xhtAV_$Lqb=-yz;^^L(fFh-%+`!JS(3r7qs; zURP|7_nd0HC5Fp1!Mhuxk%;+xQ(mb#n)$tN)`ocdDxfgz9kcIU9cZ$cUBv=&XE@} zw9(A7+%u(kA@GZktd``*f^{Jo;m!Nt2AGdqPn}erFvJFK9OOOTQ^mwpOVatgKQy5E zZx0y@MSrO3BdRsr2}%hb)+}%@9A=6GvSt*UULUrZ*U!~_f((PrKYKL&@Y`qDzASUiY z2Q;_!lvDG;DDR#1*8BD5Okoo)-8s3rM?!#XX}z6!@a~pqB;VdyLa(i3Z(9fNu3JWY zo{aQ774t9QoqYIJ#P(=Uy){2TLQ2=#dOLB^o-+$UMLt*ZpAgU?+Pt|%oO#l^Gs}?V zWSR0U#C5^;c+Z#HB;ces3`iclXQsq-PO=QfRLgSr)-TNxbU zG~4s|edc5$m>!Ic^-30d=CW{;j3YhLZjRP#ajr+QO_GQ=mX2GWr(;+82d%mO%VqfY z9`EbS!KDqsEi=mREPb&VEd81VLUf%}mAAC0RmB?@;@zkTGgPAXf zVk1T~wUo=)$BZ`j>N)4AsQCfjQxk-fh0j0|?P%t|)hliw`P|L%iY=Oagm)fxF|}_r z_BZhyahe-Qw)|`Be~M4tKw`J?%LgkKy!$3@I5r2cTvQoT&6zK{e26y(un6}t)us~W z_LEGZb?iOf^Ytbw$9!jzLLZ zm$+Di%j5Gw2YPAB=NDsg7w;b*@cCj%hE(IMf%{9xE4FL_hj^nCUsLNnr8#E}TjRZF z^XWMx?|z$MJf67AJ!dtK8cc>C5iR1~_sv7JxjRbQ>9LT^Nsh0_W@uFoKk%LkC_6}8 zu9aDSu=#A`%zgqP@v`}VS%P<0Oe4Ix--BXu3wh9bY69JWzWe}cZ~X)BnRD(2ls86> z*L!Nb*#LSNxD34|nrNT8Ef}IrK8&B)?(_c9xbwfITCSONUP*+1p%RzYKbC9ebMRCce%{jd$Gl&&c`y5Y%nAA6&wcC5 zl*PSW?loqVVp{L#Jyi@)>W*4LF$~83t@R`b$$noZ9kiCu7SDRbX9OYH3aQBtT2EP= zAf%tIfmi+&yoo~kQ5<+>Ft&J4v&Ff%z}0v?mO<;^suhlAhy)twIA}dpD?}SuG510T zcu%b-jy1t4@8b@}&U(*y4I9M!ZW~_PbLzc+1a7q6WDs#wpcxsA9d4P)Cr2~yd?Kp1 z{-^ky`;uf%AFNo`dzK}icYpn6LLd7F-koYAyiNFi&uw_)2 zd(LWPi~Bgi;lw+y3|BkPoO$){0PQ6gbx3gzc6gXz9ea=V;+GFwbFhO_9CO<5xts@! zc*}M&^KSWvc;i}gX>}}N-)zBf=3avF>yVvld%UN%pdOMj)onoYXtge%!?AVD(W%4M z-}LbiZ-TIAKYKG8+tyQ7Gv2_t)XJzo@SX`tz1OkI$d=1P@>#Io!*1TlS%==YUP8|! zqWP_IvSC)D?I%7*+lQq6EYFB&Vbt!J568y0=97nGHvjE{`N?VC|DCj+k{pG~_d}2I zo>NUfGeOh?Mx6%?IF-V=Kc4xra~?6Ospfp2WK}Ku^2l?(T{VFc+{7fEfU$w&?@2UOeDIA`G@V?anpUbQ^jOD;<~%Fy0~W`IXd2@&&z+~E#+)jjPqpHeBQ-8m9-@8g zr>`TldBqI@^>k-I@}S+lmLb~ss1HA3-dq1z?wNp^B+tXvW0FU_rz9uX@yLaYgPAW% zafmlQD&hWPyyqnM@p8=QG2Sy{uqMfyIOdC4n_J@IJ>8QqG*2As;tpCzK>Lb~@b)|8 zJS)PA9krg#99zIcZs^3;c)#5D**4Ii0Uz#Wxd%i4mdiB-JpT&G2d%@XJsvnho5(XC z_MV|V%RJ^8Q$CEiQhbm0lvfN}qgW8`!Nh;dw^7Lc)RJx!Ck4goq2@2Z-<6s6FkNe9544`0Y_ZUGhf)SW1>y@v>rm)4E+P` znJ)Lk6MXZ2CE5?P=kW&4@`!yOF4!{jA=;=!z8W(gJIg(@BYs1K&l|2J|AF`HYF-|U zHz+sYa)(AnjzjJy$j7xlXuM!OWEDcVCBgi!C>aWd%ow#?r${1mdV3tz44GuNl@T+ z`B*cAS7zGS*C|iF71%j#kM=a=Zi)dfJda8KTW#!Pi#VYx$@h3Kj5@;G1O*bO@XH4z zZ!&UxcP6j;`I=^L{R8irc={Y2Z^NuYvYmPGo-s?_VaxMQM>C(T_k~-W+z+3qrRmrt zxO3BLwP8Q-o+&lVws*u9qHWO*$$w4M5?_{XPi`EbJ>AS)$k`&ber2^E6Q5ZE^AmXf zVa#eS*urWf-Ebj_dU|=M;CC}0GAaQB#O$!~Z6=?5#u18p_}YfvqdlKE(T0;bR|oS( zBOaR(zV_g&7prR6ul9Vs{D!w5r|e_zCq8xRFL;TB%*SI#pUzw@5^#K`WM%UG%$IP% zXuV8NI2!u5dd2Jn9s--7J<}`hx$An_V8MRNre-H_jyTZ_ooGX}iJ@NK6K83O=Z!?< z%9j0=&t4z!>6BS`_kq;Hg9VE|?MIlyv2hZ4;Bc&szpdC1Z?^1gmM5*dHXe@cx8eL6 z)*+kE^y!rtD$it(6-(HBIyUD@JfUzr^W{PV&&<7eajyRVk6)j7^YwNtD%tGBzp8|1 z&`HBG+7;L@js*S&@%{l13fn{djwR$g@Xv20egw=Kc}$iB5I>?zq*?dula&fgA3in6 zMT%ZRAE?Sazuen2)yXq5zWkiuRi(m?Y9FOsohMf-dCzlOatp+35rqeSslWQ&!PF<@ zP(5@^{e+IQ!k_whva(Mgu2rFad7w4eSl>s^?2*%Jo&~_Fup8!~SEK1XfPa6U>fa<+ zQQN$N2RP=}2QZ3_*G_Fr)L)PXv;s>k8^nXAB*R0*LYPAa=2q)QZ?$edqg~@U7eCC<DDu@Tr|8?M zy!vVhG!%w{%*uCHMXK`#k#ZcgYEd-iavs#*atl}HsAHROi`gre`^Um91!tYYEsUBP z_R+bem8>h@`Yhhayd0HzEZkBv+j2{rJ2kxLTDUiIzO-7nzyOgX;b zg@>1=C+3zREMW5{=vR$|d7u>-#|^7w7jL%m4wIKDKKWP-2T-*nl=x+4Ghm646zinI zjmv$4c+G*0kJ<$?;k<|sT%nn>@WEwX_vP(WZUnN5a3kuCR0(h6Kr0+6r-`-Td5A_E zKZ9)xav`&-e#{h#`-(66f-n8B4R>0=PRmbQ`Dq?tzeZjwa!Uz3-Ps_W$Xp;SUE9HMM2S0zoJIM;&Qzn4%>YMdsxv?NoTq;Mx{(J^w z0lin%G##4F3it`W{;wt@X$cs4Rzbsf^-t$0!&ukfB930*UYe?<0xzBSl8jD!UL9qd zk#80`IXD4**Jj^{Rv%CCv3H4zSwO}4ICY0N=(7KUY<^Y2rxXG zM4ycB&^p`t6(k*-$L10O@+6A*T8-zdifQy*>h{VK%L0^N>6QECY`o5%aFMK z*%q}?&8K?(DnPAjPF1TU1`Wk$np3MI`}!ekVLxNI!XBxH!Fmm!hhk+LLt|=5ar2;@ zHmy(YRd|K9n1wMKECbXE_Y#F0p>;D+b>Io43m#bd+`x`%AFYP(nlLl_=p}9L)h92# z7yiqr4c^S7m1Nzac>?D|T}*V8Y8)@RRjHCtt(UL@2$?jX6?<3C5_bjFORHf(zV*vk zh!N_?pc2h|hOfrTo0qLb+SR9fjUc%mE9~dG<)D;+C45FO(>~J!bn%(jWsC;f!USvZ zS!!@}>&&VLX0^h7jJ1UH=+*;Upk|v?9ba%mYWOI_h5~B|^n_Df!(InP z3_k?c1NQPEU%(RPL6;7P4=@8>ysa9vKpB|G#i8(n9mf`S6;XYHkRD9`xY>a~4f;IS8F`2lh1l(l0nj;~s zE$F5*3H0 z)w`T@5=?cQ_@0Ta_%eYLtw()V`sS@$Kp3Uw?YHG@K%Au-c+4W)c;I=h1%*V_pk6?w zLfjgk5NPWo?=fP%wapNqS1n#99%fGGtSEX|un_f4mzpn@ggaPCIBNcZw4aNazUP3Iy{E3BOOrp8JL}dN}jh2(}OTAGy|Iw z;g@erF^1nziBWsktQ^x8#_FVHn}u601MAK*5foMdJs{lh&~)VKWif?Tz`1(XDItt= zCM6oJDB*={N~9(%p>^rRMo?Ce@Ax>S_Opv6mbj?F$;E`S;-uMSaXHIwIB@7i2~6!; zh|`PD`T@hM4b0D8fo(|Vfmpj5X?m> z;4NZnV50+@*d;8X)!Z^wb;6NcR5vAjay84{eEeK`clp_b+X-KTOee+B7I@ZfuWUwA z!Q~Jv!NfbO{_@SYOe6M#9jr$fj znvCC@JLE@XYusydZ}`CuZc#GsWs5ks7*_*;t-ha^6%*dksRkGWVK(0dw+2$6M#xit z)`-n#4sArdi|G75C$VUAYO}ONf9a2W(B$+<_}M4ARQV`B(;Hb*v(HesAHC$yUP2xA zuyE<0pTz08S`CIKqGEDV2dx8++!c6ZIa5L%!wm|MpFt+>3iGOatmDT!iM07@1&Of8 zsheX8c>I}0c8vZ>BaS4hg#7BYWII7rGRKOvDkNyNl=LYZLOqj+m!6>|b^OYuB{gLy zhFc)8ns$MP-KR^oj4P}xI`;`>>M4<(obUAVOl5qx5<(mnm~`BSdO z<1k4u$!x@NWov=N|wMNjjom43u7tU7(ox@ybznVoE?glG0BGQWuN626aOqfh-l`7l*VI9o z?+8LPeitqx`4VR@$$^whObwHsB|9NQU>?yRorx9(kqvn`N8WK>my+4({p}VzT9Wzs z4q_025wQTWWY`VVWKGgfu?Ruchq8i5&P(($%j=|jQNbp~ZE1t-(dXfQ3HW2u=|gJ4|T zJbTcAg9m+KJq+TUU^Oz*GNv0L;4|M@1uJ5w9$hUkj8aBvDS=}^f)22?G(cO(8VtY) zAq)LxK{*vF7V_#AGbDRnx-~*~92%VEVXw#??cy^dv3PTQZpvO=7GwHL35exe$e(0X z@F*uUq2z&Yk1AS9keBxz#lzN&%b^U8h;c>LesHl9>fV;{V21sm)#)#FmJ)0^iicKQ zvyp6!PN*bx=n#5XqTQg1KA$jNxgdAX>i2x2UB{dcQaXyK6~C-u3^w}1Jv%e>l3w%& znhcmdN5EICE(LE*RMd&q7mat%oJyVO(sD;3zpGvtA%|i0at&U*N@g!IQp zBvZeYKz?ulDK_FSExIhqBHClRP+j6A6OaN+env~ii*ypaycv#FZvqpxEso~POv0RO z!5y{$X3!1CriKjGq=XL%OL|m~E!Y7lS0P+VWDZK0CfdfAD8gfjFs>#1L=9M@ao9Jw zTF4iF$9wTCM`S=UOsLX}je-iXqTt&~R0@9Bn>YhcyVJ&vc-Vj_+uLB;wxkKQ`brZ; zqbfn@upF&`{**$$DD)?11N_pg4LXeiuvZO2H%-B&0J{SlQ2=K57>|@NxwT|z1aY4W zm!@Ao^(DC(S`A4n_>A|CO7_r%e{#ym;_LG&vIu#4ZY4!dw}qvp_pmK8UB%+5=3gY5 zjnz9&fxd~o8np4l-s+ZXFKLv5zByN0X-7ZoGigOXF{jJy){>eH%f>Tdvd13ovukT# zvlHh%H^?BhI4-S*+@o?|4%Lmd#FsT7%u*8XgXzIdd_P##(ftuPgSXBtK~15YsH3fm zx3MYVq_28*zx(G03r~iO;)}|U$F!CxoDxu-R!V~soE5RHq7Fpx^U5yq3rj}{=CqD1 zG2c4VACz$UMs{n=1Wmb~Ie-^-jM&IHedM-Q=%HFG*UxX2;4n}NFA#SA>yrpO2iK}M zxRdQ=%|^$D0;7JLf-Y1ZrVU zOCijg-B_ydC2vcVG8u^!VM4G@7ox}7t|hfhOrQ5;*JWyO8TD!b5Bf#J^or!ut6M9r zq!Lfafsz;+rca5N6;i_8SVwlx7)Ha+$8mCYkT7)~qU7ZGyD6x-sf;}?>nzpa&D>-K z?@eWHVctEv$U@w2B2+5|L_rPNuLZs)c>BN#La;Vjl&Ela1xP?`wk?Uji&A>(_!{DVvMr$Q_P&i8T?5iwc0D2}ovMEvJL}S?soa1+x{!g7M zi5%bzTEY^Wh_gC_ln`P5RwAax8Ihi+242+LuCDf%7L+T;?@HFV zFekJ^K|UYu6e|iQ6%rh4ZkaHllgz6eH`n|k#!C$hxZ5XtATdKO(K~jMak?$=1r|%( zt;l4xxko#|j>2Zesn>LrWGl@)b24G%4!AP*n^|w+79PyBn@4Lj`Y#c~w-a?V7l&ZL zZ0c(i-;I|PY*_RAE#W?hg?m^feqR(A=T1b?st`?L!d6*E_oG`zb8nP|wN4`0D*I>p zxM!!PHegi7AkhNWL>auyG;|(Du~Wh?&a<|1nq=4;JUmSHcVVf61`m5zi7+w~eW4GY z1#+Y29u)R3F|AxTgQ$urOBN$W)K7YL5aEL*~gK_jwSz3v+TL;5 z8s_UeI-Cr_a3KSPiq}t6>I#EFs0#ML^GJ(mdBR|eFsN8NXLtzmfD;ARIQT}3;1xS? zKLxkF(;bf=26BsDQ}k&ce>aocl}qNr!mQu9xi}ozC23cKtQC~QEpZ-{pr#6zWAU@3 zls^ONWaU6AC0c$;kbW2zeVvWD&WW@%tKAl$ld}LzOgyv1g_YV}&%IzpVcfrHG2vRS zGBj{XY8cs<>F9xdjSK9H_!%R6cS}q`a8~apYz~tdpD@h&V z4c$y7utKicQj)+5t&Awt;N>DvKzt;$OI*boW^Xtf11o8fktHSTYoD==J+NX4Q8QW~ z(fVX(b{~ck)gfvOU^Wetiv4@5RJ#1HZ@Dw4xhw1c-kP|80!e6K5DU@u@!5OBvdtso(Y?f zkQRxph*w;b3bqL5P|YFn0wh#2x`8XS(n^Xwn1su!qGu2}DBF~^q!4{Ev7R>dO5(c17Kx_O1+ z7PPuc)N8;W2zq)@;jsl}kMQ9bDgXmDy#8zpN@~xI!jn9}w9Q60&b+hWY>Zh+OV|XsSi2KCJ3D?AF0{9c(-#cGO|QBkch0yaA75unNjcbS z2}*&P!7Gg9cEOP=+G@!I4|ORChQ)(A*zVoLC?%>S42UJBuQaWN=x<9pk7-&l;+O_q zdxZ&j%cU-FOenpgwe6#o6i$@Ln2L<;;tz=@Z+66U?ncL)@mRs;BAJI?12qrHjPQi2 zn}@{Y6SMCgQo#(dct}f0Jfxs&94M+?56PVK7VE=9Qr%FJG;gLJ4~a<`Pf?@AO9Fm0 zO>3f*EkMIVW|ksel24J`Y7gQbL&QC<_V>2uFlXNYe=G6Ueb=k;le}P+-{U7aE0fPN zDr~Qx1kCqn=t0k}K(p=9F7cDr5^PrdBpW2hzd1@~ujfc*ag;#J#HEd+WJ%nN<|%>L zNkss;ah2dNl)E95)m5S-F>E~>eI@T2I8FF1z7lX{#uO~^(B|Y1tuB%d^22~vpB z##fS9IZcA>Zl{3D)rqh4k&i4i?<&h9rkVBPEK#DiXHxN%>>%zLRyE!6kPpAYeCX7g zBBAJ%#8+au^%^Go)SWeOh1DoSk zEy`NNRbmz{q@3JbC0XX>au(i=dK*ea6>tSGg3dS|FhLz|zVs8{<_A>?s&(;$u57HM zz!#NA2`~RK`dWfK$BwA-)I@wVqZc5YI!~1Np;S9oTw9$StsvsGvc@A8G6zY(FiMN| z8!QDWN_a0gdY_MI14F0`TN4p}tI68LY@rvrP8FwubuMkq6DM#j4tH~;%+0ceu_Kp@ zSzuE+qW8>3khKqdX|A)!$~I7oI4Nx>b{qKK*G5OU!>o1UtsZSGErmX@vDWi-0*q z4t6LXYUuxotwu!HOk`pAZ?-@gWjBne)O!WS)vyBV5|B&N9gdQPnbmho4KzlN-l7u1 z2hGQ;@SmLGt6R1(K{}kHkORjQ@Oy={K!cv0R4b7$%+TsdMU zXVP!X;!J=1bw;pb-%fDXRk&PvJtbSfdf7js5U6JhO0s4rY0U^pIC^!cq!j#K#=rd? zD=)&HAPaJ(HZZ5?z(iakw&>W}2T+nLlC-EA_$co>rF|GKFde7ytyy?Z2;`#K^??Bi zmT1d04D#s)_Tn?~tfj=-G|tA~{1s~x1Xp)ylX35>`?@MoI})dyHGx;K$@B&fw|%QN z+7=)g3z`~%-BH*;O9Y8ev7h{{`e?s|vara|*a{vEI0r|P;yiO&bE;iEC7ycQrB_O% zGxK-PO$o>USw|wsz|78o27Ol4#gJE#4eoEjc|+CCTBo@?lb}U>t;32pr18qxS_9IV zH9-r{+a#;%qK#++{QY)@qDvr5Vr+bs7*yrM_=b4aln|773LxyRvCCKOX&12M4?p3< z$A+`xZnH)v&8bmF7Cf+|ad(?`?lAcSgOF$rfRjyj8k4FNCl%HVUuh6{{80nNp$ z*Jw^L@GU_%!*x$+ujipu>s;0f<)w|E3J2aR8IN z+O9N$4c^{`SCE_No9z-$yy*tu63oO7p%kl>-{TW!45Jk#ikc8Idb+csODvdO@8S|y zNvu#@V&RBNbBTc~@p^HI!NNyOHZHLxrW#i8kXd1`^wi5G4&KJ}BYvoNDS>Cx1TrqM zSyuW~N|XA)EG%QB4YmcUE7?!I1{7)bG~q)iQ;B2A#`Y47=P`BYO4GUXhrbfyK~vURaK zO{C@G5z{M=yH<(zIk2-XkGO+apXL!;g0G}Kic1Wv3JssCoEf#euEiy$g!c+kBQ7zp zfpJ^J;bk#YX$@}6<%{XP#O0&k>cQwC3;v2*uEQGrDhouxHc0d z%oa-M5pU9M|LuSLe_FeUdS;u7%VUpO$leIw69=ev=VyctQP;`48R_;kjv z{`}#nHS^$Eo=xj|<7H`m{{9T_Zfw#O`I_~n^>%C?zu->P_T%Tf)})1)NP9SQ+@jVx zkACc0nvXkR*~jn>Pc z5^3CJj0b%Bn~58$@XK|bzzz?W$y zI0x7#>cG4Nsi~6^=C_tCtrFQpq0;dS?Vg^|s3=gZDXB!3FbfabNHxGKqU3{qSYH%3 zB`T!!TqQUb>@?EN&09Ex;&ugTBbvBtZ2G{GZdq>%+&hYsQ9*mnhFDdM+IK^;r4-7GP1Ucvjc)@n!rFs=jLYb11e^4K~Bx|NX27t7=jZ_6% za424_+3W&yu3Y`sxs|ezBMfF_CJrzf!0|)@WKkM7H(tQ)1q|I$4t1f^!)3=I$_*;u z++|x}on>%dD#m>S^;6B}mXx!u(&kB|`~u5W1%%nb>fA!n(pWEU7@uON?_E zLub76g?L+1H7KjU2JH65tzOV67VNMf86R1@OCjc5p#&t7k2V z1hSccmj^|Np|8!x}6of>E#pSTDb__A_ocmnizrl=U(_Zys}iNaK9Y6h28 ziC*ARJI1S>Mb4NZtL`qU$Ri5(C9Nd9DZPQcUCMm?SRCMvR(7;&(m)xci6v>J7l1-itzr%FRXqEEBZgs-S| z1PPwx=1EHOL$+v}`)+sqOt1dN|1Yar0D-U9OvGc3}gYEYsq`RAl1=O3~Fe{e-v0;dA$X#tM7 zy(zgvAYd#wQprE+4Rr!yGFO9hD}@+gwMY#Ecjydj80q&OO9JM=1hSo#G1U;C!O&LR zgyGZ$JeZF6#J2D)NA=s1JjzNBd3^-Tc^AtbldvGEqFg9oOgR=(vKav8*j`>M;xiX~ zvm&*N@#{l;w#$Zv--)8J(6iW4e^U6mh5-i^!FAx@w>V2dP|zQWlgn zK2$fTKGqWXPG^{_JmfuU-XIflx|Ec;iV~6$T#beAdcnf0E^*fA!W`>+%n-1`vhhsR;en#~U#tz=|s0Nx$H+MueW`6pkN8g5c;|Y>Ti5ee_ zZR_vPQt!mTxGZlZuQcDSH_Budk3pvpYaVl|c+aWE6CqqA8n<@E-aQ01#M{uZC$$H7 z|7L9OshG6bTkr5D_%Oqpn?B^`j%of|KF_!Pd=tBmy;WS|y#x+Nc>CBsmk5trPifAL zZ!EP&|A5a|`NkVSYt{b=eyOK49HqbWFOITtFl0UWA4aSa9;%??}JuwrbDF>~;3(dYY@fEJEVtEemK7Wh+ z?po^^kg$!F9gHpB=WjvGeQOLnw^W9WMSB@~gtjL*+&4wq2Sa~rJz8!A4#$79VEg5s z5{G!B>d#v`;fsJ!DfmM~jSzwcxsdcrqHSa!A?|Vs}}ta)J2C3LgGA@2&H*kuA0pc*NOd0kgkw--Xwl zJ5@bb8;{)!z+B@oK52-F;5(9Fk~^DzT42N70GrIMOU`y9H;I66ZVm~B+?a{Kd&4jd zh&omm*|C6f@&c)0MK(viXjl-{O-yumx%`Zgfk1-nchJXtU1HlmB_t_U|IENxY^09{ zP7si2MRiaZP{QeqQ!g9|U~p*-71?M2Owt*ATvRg5g2$CJQc4oa0d@*SWq%c_C8*j3 zdo6NpYxLo2u&ecsX~iz5ej6X91*+@(*u~THiUB(tHk`BwiqOn_$V8SJ1(*p(H~ida z#8^wruBd3}4Pq=r2JOB8EgaYuM|!Y(%fhur%hE30E{k6SFT7Fn0&L9FCl0$c_A1m? z_AAs^1`Tl_Jtd8WLq6e^whJKd8+ZC+BxLSyc`#Lkh^ujIu~$r&*%#Qjt4|Cr3tX-D z_IIise9+F9=ljsx#!)R@zDJ6QBsCmvH~DSi`__v#AEAvM!iLGlW2|rexOv#vz)app z8Vs#snh%ZllpMA}^VZ5=fJ zt#F%RM()^{)k3tsogJ~c$w_Qd)`{tr4^ zo}!IXeDihWrS*Qf=UmRi1B80Vnk8J`7i_fLyw~MP;PG-V8g+y=VJ>n}Jk&bCd;V1| zoGR}0t_!xu``#_daOULTaK63jh9BB|CfyhG5?;eP_O^8dw0rhLyiJS0c~pLc_v|a8 zJluy{y|ICNyifgG)Bl&TO%T zYJ0q=iiw>JE|0m52Y4@EQ5#YXwPtepsB!dCU$7zCJYvc>qmIRUKJjGtZQR5?+EX8s zCL$$6GhhzKV(ALY`WXxMzF9Ik{6xXF6V=8I|>;q7~-nY&zqnZ2NI}V_a)%lcLcvpcPo_V}r;quvb=*s4oBjLl< zUaaqMweu*ssZ#?+3$M&J&SY_sa^R`|!E!I0Hslq8umq>a0`~jmMkVs_+eEmh%Z;PQ z8*vjZ@2!V3_sk^uV+VN8KTC;9%#&XSB*(8m{s1bUVIo~%V@I|cF?`x}`Do(Yw=?Z% zWtP2sPQ7DNLOd#3mwP*Q7Hp;w$uBWAU>$qg+L7 zwy@C+d%UNNN?rmIwyN?U6F*HBjNM~P4LMluscK-Q(Z(yy_Vb=H8mIcEMU9y}>owbu zaI4B}w()95HFVoMTrVPnr+iA|0PWjMz&KMvmv2bE$9uZ>WjA2}d+f3No=LWg7F^H)TFD zLu9WJ!Zl0mfX}DO!NW(s1~8_%>pjO?4>hlE9jw@`J6l7%xopA1|6`RJyk~n~KE;|u z$0M{CRvQwHqma3mIAs=mOm1NuFZW{khiG$DPlDMo+EW3&`y%(nq}7AvUM&9*Zy#Uw zGf2JlRzRc8XO@3C8RB?|ykb%`l3BNAW-KB15#E>rp8CPBKWaT)F|&F|-R6L>jr~Ks zdw6MtH+OB{H(xgCt+zA(tvbiQ%D$%J}1e%2W~Ec*A+Zv7R={g+7Yxcxd{H4EDA2rd(Li(L(8JD zgxQ*Bkz;P*%0;MyaAB`bBq_o(WebRRqP;4Mg0%5^^{O4U3nS&8gSW$YOUR-0)IJCG zfl+@s#HT-gO$L5D;9q}E7Da1qjzls9gw-NRDM${1CBC~=DF({~0h2|sVnB&m6y!4$ zj&2B|41u#^EziN;RPry6e-Yq&Ti22#=OT084;{XMrIR^Iy1{5{L--BN2_n zDd5_W}GJs zi-lN&j={=gh2zSEMFZf{-9;sNXkp~gQQhq#7m*jBR^WFDMmOlem(aQ!`Oy=6r#m$x z3Y>evAKzOq>A|=Em_(2WvU2H2emrVDbBG8jko`zdy|>A*E{N=kuI;+q?&6ctG3 zCF+ZcS%|YJp+-L8qP4_bAt>J!7`swf>1p~4P zzbP;Rqt{h|K`(M`wh9b*xiGC6U{cK7Q?ahVgg4X=v?#E2=+9k&_13kt>8B)1WWUzai!a1fFh4`8a{z$AOejE%Jr$?-6=C&}dUXfX}nBo8Y)}OyHS@xT4V# zQ5>%7W5U2Z49D)#M}kI8Pcui0JaSNVJ7m+nQ#mz`rak&N^l;Frg5U!iysP2EXQTN} z`2o+F+V@qx%WZ86poO5%w5Qc;@o(VisYJLtJO+C_IA3R+oeJ zF-J@#j#O(l*av(#XJLFa-o!hi@XQJ3ZWTMgBMMJ-+LaR!-jO=sVVX`@f(zZa-Uo{_ zUsu#2Z;T(v+|-M>>fjmA843E7z&yZBV%MP#o+VH4x-f}4hdOw6YwyNO5qvA|z{sJQ zjtQK+!$VxBO2VW=ULiPO;i*CAdNnM35;>YUu*y>h z#M25Qy9e`~GXx$U7ZDDmb;))b`N22B|F}Iz3^6-)z;E%Q&cUej2^)s<#lPX&EYErj zCyY*e`gl>A?#~a!%nD zFdlvDCBEd#hiC)3-6st6@+%iMcyYh(mnCFjS;N~351-qGE6yLnrpvlT(>XRv-0z3d zf)#wCNO(z?XV{*?9HsG>JQ1Ve9lrTJ+M5qft|bI-QMbUn1jlON$ofSz#yI=t8W}#! z%BSrpi3#Gm4Tp(N(%1Jscqou#5T_4e$@>=e^<)|lEcMd63FTbU;V*7NYIw*C&)_zQ zj=uwY(80EqG=p9OsrdnILj@nu^#wG)+$+o-5Qbpi$Q2yAqQp1)^X@I*1|pk5Jn{l0 zojl@VY)dZLSbK8_=Yv*kT}e#k%@n5W64RT0s_>h06Elr^y3>BH7gL5o|-$TGDKsVsTN8_xI35GlBF)`SL23(no5KIM1kP2MXKk$FWY zTf$}{Y^G%p=A}(B;S1~*{*5JXKU-92V>?U-fY@wJABqDG15;*kM3%Vh_L&C?d~E8Po45!ZXksZ5Z`~R!@^6gO7CDBf&KrsBqE) za^q9bmiVeYHF#3AYQ%{!@))&&|5Rgogz0Q(kyJo+1fk}rI zB@PU{3ADf%fN)PMP{6H__g;B;wk+JYk}$q*W9qQ(C0^#<(X9y&k);|H2CJXBqEPel zbF5a#v{+O>QWMzM2T?qJo~^?{om=Kh0Ts?oSXpHA19+0L>U)DfqcYY*bo2n-ez zpdMUcXoy)!i&n!-u(aS)lTM0nBwL?1DO@f5G-Zhbt}q0C>Lf6*N?}8iZ$$g`!Gqg) z9A2%}O+cKFyIJ7W4wFRGWH_t;a6*NvfQcVZx5z_=m0rF6^#vD0h1=4`G$MtUE2>6LP`&yWw zkHeVOM(k>VX`lj-MN{E}Jjy)xp7zWP_~r&DBSa1;$@at&FEIoAW^ZcvFj|9yqZh;& zVyWTiX~AC6NwGi}Qhw*%yGEyoCMB`zy3f3<%p7^9enV!cm2Vidg$NekmL@M&LmYFm zG;XFg@yNK@{(K$PlCndsggTf^rpAlVQHZwEG0$t%D14K9zm+_UVNm6M@%uQ;*Mj28 zyjP9);cln6GVfI{B~92E8l0NVQ~Fv#lx#_#%PR&eSN4*QQ}9H9e0?54a3)%wHLcb&eQzbsR;ojF8y2hkJa-pWi^b;<$(2-Za zQv=!g0LIQc2A!jGfU!IN?k-MOlZXg1CVQF6au()^ePK*G?AKuEj>v15dj(C$8u2t*J2HRQp+Y1yrTEsC;FbRWoxRPjZ99%$ z*19udg5zR8lXOCtuttlOaQM=Y?nCNk4E+RQZ%Q=wgF)$G}WdBc6pLaYm ztsCvA&JC2Wm$FT;d965J@T& zB2yzvpa^sl{n19L58%=Wh~XmxU-iGZ9%e%a1{0+QjT;Ri5)&dNG?Dln5^7~}14f&e z%Rc(+H&TKvpV?7`-R4J{OD@nil*i2f3`uTA_#)E%!TaP2mXEYFnEKdLO)e}D(R{@+ zq5#e&*Ar;9Q^<`k#0dMI($5@!1_mdl6qF(Z*8B|!#Y6*f$F+Xe z%>zLLgI1yofA*s+i(G|ZaCOT0Nexjz^FILcdWU^*MmS%=Jbq*y0>i8V=0LU-ozFLT zJ)CQpHK%~7;Vy{3{6?mVD2PJ4BGSu5YAe=-;F6SPfvzSmoaq% z9!^Wd@eGzRbzsVv@&jcclr;s+^e*chYl;aE=A9@FnkZ`uqH$K$QmZ)y+)P*@%A-n) zzh!QDr6(&S)iJpN;dW~v#uOM{eo`*@14RkU zAw+-+18Louk{^ixbymcfl9)2>PM9Wxv8J2|bV;c!nR`*@j38;MnLEXn8JH$wl2glL zwlW>$Q&AX>EgRx|GW(c9jQ%K+m$y#8d-x+u?F(L$`-eZUzRv-`$k_SW5S`rY)EcGK zu7J6E>(tvB`-eZUdAJbmDn_?2wYgux?z**|+ZmU(*%I=AMm)gZ)XqW8rM4M5K6U2x zwlDo=r?cs|mIM(E>wf?7E`$1|Z7nh~avyD+#I|$d|&5-cV zuYT`@kV{gZtV#31lhaxCrDH~B4@+=D+y225BdnivU#*5@uw^!&23cdJmIZii8&Uk` z!4tv~a~#{-z7EO$!4uDm%x|}Gw{M*|xK) zvXz?E_KMT~!4vE?PCJ*C@54W?!h%)Cpt>bOCD`s zhkN_p32_nZ{IGI5<=5KVw(*N$HN=-S?f0SeW@66kei(Di>RH?0in%erpM;%FYe)0+ zJN`4xgGUfC$E29GZS8%(J$S5*=Dgp>^v0h3weOv9-Ig`+Z+-8i*RBCP-a8q|3Z`fL zI+08mJFD%S8G#hDONAauFxe5kM)|pIYu`^AJ^QD3UhrU|otbfqZf}DVa5rK)>35zq zjqH5$+zIZ1cVn1lwIzJCJ?pl6KW;X$Vout=t36}6H>!9@i=|=F?=90iYeg)o~a(SOSd0MZ-%VQMwzm-!B0b&sjo?i!S zv_0)NxkPZ%uDLd9YgbLpD`z-CE9`%c>E zugYEEz~e${gL3!<>5Qet^D~rlde&-tTBgHR_&(2vm{XpQPN0Wh=DZ#~7Uq1#YKaSx`)sg4J(#A+oeuzLd4_p#b)B!dT>f|>eV@#`qO$fshkNSa|n06L{ zX@Xo#1TOxMJRb-9ze{O=a(4Uoex0aK{ff+NqT=4=Du#Z)MeS`Qq@H-4alP_YKW)RM zF8okX>DKo zy=58)1eg!{kSCPe@3ErH+V;jT(`{bc6=mCR4nOcE87weq+uGY`Tkrkir$vh{`n|mz z;>MKK?5O=^yt|)b+HW_e1pbY-&$aJX(%E>M8SBG51D*0~?bqF{3tukrB0{fx9q#Q# z5wtw^;$pxu+}my&Rp@1>-u9L7@c`+x+w6&PQLn{(Yw$g)j$Isk+h6*<3+i@9{lLq} zqxP9{`@L`5%(31A?QNfHN6b6dru{|%=MnQ|?cK!_97lR|56Z9o-sS-y!war?y#BMc zqt-JkEVFJu<JCRc@!v_mCc$=(ka_cTZZG;ca$x|<># z^>PBSSiB>qeR9vl09HV$ztmS#$cuL@pyw%)!QL_)mH6yaP@QSles8xV?5Bx&(oHoH zwDh|xmf7TXqfXM0gXdSuw`K=dDhO9N^vbpJc3bew&YQRDj~Vi%+uQlY&YLLdgK6is zGdp({&of!MaFfiLmI3Qbn+`Xpw(Zv5ZYq!yqtOpR{W9F0=TpkP1RW>mqTgFlr zlXA289h375$97`odo5&wTlaape|+&i2ca&@vEHf`b8He^vzeS-txU(7dj{`Zv6!|U zN$I=K>1g4N=Z4cErPs0EOQ~L3T+`FGqtg0tP1`2Y^UeG>vl$X8?Q%Blw*kYRLab_U z*Iu7oAt3YM+O^+%Db*Kgyzi{-@8>oL_ymBI5i58Q=>Xk>Oa=8U8_2dq=4^}k^Au^^=xNa zfKo?=TG2ZB$FW%JAO~r6YzBNzMlJ)2MdR2Gj7)6DcE#d;Y}fcdj_rWY$;dVN&&kMr zIMFLpr(-)X#8IEE-tU8{Um z1=}6=adJ0lS{1mheVp7)YB$xtj;J_J?pC#T8{OmNZqe^8(~gt7N!wBVyS9&$yGiYD z`+c0;EoyI-_i=K!==avZIZo~-ZD+i9yGf~-x+kOwTaJJ+fCwGh5+uTju|L5+Q zM&+vC-OoH*>N#fBgXX;cI>j>HcR`^SZxDx-+!W)U%dEEj9@~Ivzt#5KVqVtXj~E*D zi5o}L#iyi^O&TQ%HIz+u_=VC9)y3aZo@+bp`cs7aQzmeZls zW=u2unETP}KpNAogl|)25;em?&YEyE!`eqp#0@s@Mozyu^@HQNhO;7kCo7~|GqKNH zHAK>O+W3hk3F%w2e2n%?pArFzIlig9KlH=UCLCcsUfXVS@J1S=qxM;+Wf|F-4V>p= zMbCNm?J7cyJ1f{pGn|{gQ^r#<<<#!;)bv$Kw*fMvIL zR^GFBC|FBI?W>gUyUp(P(|j7e?Q`u6SeLZvaGMoV574Ui)>z^eH9-`59vj!b4EI(y z;Ou=$B;kbT-)c4QAv42|wy*sj^K;g3FIsa&b-sRnE9N9G#ie6-*P`Eh?>xZ-+^wB! z)Nf}u_fC+tg~21`Rz-+HbbLT<=?}wIMB2%5~ZKYRPK6x3k1l#2m|y%2$?i*txc?UkG_+ z-#lkphI`9$%tX#K4yIiPY%i!BpM994u6O%P`M3JmcO%~(@gdG?hw^UnOa}~8@A1RT zYZK*=3A*h!D)lp2$rclIKVV$yC)Ml0wQIlk$J4}TeUJclN%=U#n)aK$FRJBl6;#}b zT!7pN>TXlLvqqskOLxwymxRgAA!$2h@fR@3SiR;fN47-hF@CYnGxq^~lAwi68=>1zP?(}GV(3fYD+ zB}ftohAzW2Jy1SE=rEZW4)_5ZD=;3xViI0^Yj9)JW4;60`Ww~{4=Nx-2#81-t#lj`BnflYy81xy};tf?mmOs3W;X`sE9|XCEgWxtaoZ~|Tf7l~) z$0if2OQ{~419SPXjTsxIpIae?y??pEUYtydcGt=Wri(w}sic&nt3&*~S(@0(hQaFN zyoHG=%A{tqmn0mL{LE&1dt z@{Xy<1w@$y;XO?sS3y9aj*w@z=Zu)*hD^PF-UiHTR&PiW3B-B{SoX?x7?`^aa1dpI zJHxDfN#u-_yyjvsnCB-}C&rUKjcE+u-lHMuB`p(6%Zmi6H^c`n(`#nFJ4R$gk@fNd z5eI?u0Xof?@j?2)2g9Z}L-ra1uJF#7!%P8YvwVfe1rsPn1vu}mYD>J6D?TIBK+3D- zs{QoXu{D)%N;{bjgo5tk&?O{u!IiTkn#e?o7=m1@r_ckZWTY?U9D)mDeAw|z_E-WY zB@~!tn~tE?O0Nm0r#wS0rB#B4U~~3*Vo*?U3&)UZ2rjUl0me|R8caO`I1FB9gP9Uu z+X5yng(Zxapj8IMCv$|2N~ea(Fbv>hNFBVUZ|@^rw0tK-yU##Mu3xT)N=HKDlo$@}(+Yn{o|RPx7x?j+K^6zmJfjX>F_t&P2POyszyn8uMVOcPSLv4Oe_Qv9mcZ6Nks^5=& z(VUG~+{dQd{YXv)@)yr*+wXDsH0w7{rJ-W6K|AH=wp06VVY>7?3o<^PQTsRbo6RW}2NvtB zZRLC`Esx|>KtKNqNuBna8^EMwp0|BBOQt;M$d22~N88taACk)899h#&CRW1Gxf#mO z6*5_7YR^Mk6 z2qrnhEFlHJUU?CeK#dkJ6bkF}O$Agka}11q#4G3Zi(BUbFi#}BhbP4){do8(gOQKI zv`}0gPz?d!_gB6KC)m9mb@A|>vZnbv&qPU2yZ*s51Z?qhd!pz|bSm$Z^d11~3{DTy zMG$WgIyeAqer(|45%5PPHe++*fK2f8RGJPO=URGp{pb$V_OXb}%`cd+}Kw@dKi`e2#eDsgyD%qh*= zCw*DKuvRk%?eCT5?ZdGwKrBD(C4#RKnp zQf@40{{4n#({-|+QW?x-V+5E^l{fBDk`?wby7GBn_X#biS9#Iy`;KOo92nhsb(QC_ zOiyX%dPE-Qne%xYh5TsT+atT?In9_RYuWH>1b508RkvZKxE--e@XakqzT1RcrmqRj zc)|m_VmiFAYD2Q=%sdd9*WFrTk0vwo*kexfHY>;@K1wqkFWTMHtYS1!Zcg)d>XWS( z$MH~;U&MJk1@TPcKJTXj`OCOEvTFC?cRQNN zw1dsCRP8%nc>{t}99<@_eBR2H8Pt=BdcmvR6y(WI&VBH!FY4}gl02n_HRsM^z6{>E ztS)o#2~|h@$f{jray60pQm*b?gkRd#9$YVjyXA--J3eyvzEizHjP8bJV+lUE zAG4^tl@0V2$Mk(U`fakIz2*}^^LFtwY-lzOgl@%o-EBk_`v(mASlCl@V`pAzf}>+s zeBLf8+4B%4v!Z#+XU;E(d+&9xeBSCDZZAReTAsIjM%VEQ+p53Yh#+3jVtX)5l`GAA zy+J7OqdDvrq*|gnC|gJB`?zawN?;!h+^4 zpHY(!?tM7<^j8C3dG@?-ry299fr`UV464wW zPcd45!6fDrfg-_2@zjvd!2I4LK6Bbt@uw}~^Jw`xRzx0@PfuSv-}P-DB0hCQDDn*h z{NO##YKdS=pQRTdR7)2i@l9O107J;b6K_9bC=-MdIM4beEQi52W3bUn6yQjOadnCE z0Z9iOSD<~xAUQymlDxEVU~s{i)P)bchF#hc)MzoN8DR8Fn&7<)8Oq@gGtd}P-g`pa z7;rQ$Ft5(rBs*LfP7OLu+Hf!~e(_}T)+2KW*qhq=DNV20U|o)km4Sn!1V*o~mkD!~ zQJ*7eHR}ud?c$_t7&wKnF{pCj6BjKL!ywCn;VU|i691m50F%G3WXEXBpPmE|K-_9r0{^P^7!TO27!e5PX%1&zJz> z0V`N}h=joI!uXCJg!`^5gdq(@cu5ejvUZj!B%Mi`&hp?UERXEx6W;6z$RnaO%29)2 ziq8;!r?2lmJ1T||Y%~fKYs`|A-vF5?o>9rmDxm&gUMP$NZ&gC5H`H6;2RfUnMTDg_ zw6mC6=o14U)tc`{8W+qYVCXR&Q=^rDL#!K=;F&o@7Y%_301~oR8im(NtAag-E51Dq zNzc3j8kH;CF9Sn)Vqz5u;tES}eIweaNX`xi250@JL4FZT=77$71Ehu3YlQF_)Wob6 zcoDejyKkUQ8(3!FW-{r?fn6fOdB;^h+Z6+ zv`R4A&1jY}SN5e_2p+aYXaRJ6f)G%zZUt*&Jdvh81f$OxP3SXf$SblP@1692L zDolV>$)(Q30W%{zIx|7qEG8buoDr|A zd(qrH-Ku<}7-E6awwJH&F-pEANM#8Jn33=f_$_l6TmAV>)eXX`7Vu;YIPYRdfCz{J z=H=h~TxXMvryt5Z_B=|(^6_2*hDw4-KA)YRqZM~ztf7lMQ zIVR(C3@u}h3~)wfbIf{Fnlka(V4~N^?&ygD0QH**P6lMwG@!NCJXjG-)7^w&8KjH$cR?^e|)+39WAR?4|vlTY#-N zT8*DSyv1V!j5go*?9nyM35l2jqv%Utkc;^S{2Awp6v0z&6{yBF_GrOWheY`@iOu{k zhGC%!*XJ#Ogcwpz6IgW2D|Jy=097B(PUi?rfhyEITz5*K$LSSXyG@Dcb=dHh{dASM z$v6|_Si_YH7$fFrla4Xs++LsrQpvIoj3Tq6us|xAAY3epCaQsC26-*QvCwmV%rKO% zoF6l;%r@sGCqG2A7CS$=o$!|f!&ewU3b2Jxo~T6Y_yu0$=0ZG5et5@*5w}_FNmfz= z!?`JfNMQOx7*10*ABwmhUsdiIqlhs8ql~67(?%gwOCyt9l)eFDXi5djAiD-JHMx;v z1Ob?}*&4dT;P3?b$?W zXtX0}9A8=5YcR3!^*@3^NvG8w__noZ=cB z425Q2TlL&-&wdzA(F z1S$;pi5pCT5mmk*-C# zMvd%U+_o=~Q^0qelbghzEzcB!*M)QZPdZtx;k zNG(2H?zz>NdQIbi;k65Q$3z_}VVQHRJ57OJFpf>iu#CTF3kt?T(l$;VCHdtB{PK}a zEik{Q9WEw1B#dVW`yd{tKqs<0d(X=F*#n9qtaKO{7@d%T&x{|=4 z!o--OIaJ5t`-}-XOAYLYN&TTN4IcI^? zV$9c$2k}dspco`D)94YefUvhGt-;nx81tg$)k74Sn2)xJ<6e^`@G7b@sDafFoNE{j zmIJsG%EU^EA4Z>fdn_do3}7^#lfO@HFzlXKzQwSw1Lk^Sm2e|wDmQ17zn20B!w$!9 zV%5=)aFEr@sipQu>RAa*BC6DL@yl;wVZn1?HpZRKW|qC6jG&vf=?E_t>ZJw4jR-}yFurn~ur*+2w9((OX%(pt^&uFhycJlD5G`P)3(W$nB@dX6 z@Zr>GC49yjLiqyz4aAgHQo@n;Fci-uH&?8Iykqe2O{N^{7BKo&u|*JAw}3xmh!-qX zu87UMnGzBc)5A$iKQx#5wqNkdFf~||m`h;XfAUR6f@@5n%H#|U&hJQ2)vyE@ha@#{ zsWeV73uz-a!~LDaBt;bwveGRx5gmSDK?*GU=|?73u|EC28y>Ip!7VOVNtbiXgAs&> zXjtjc5uycgV9+~I1V(;jiZY(G5e$BQc^Zrpn*lP9zZ3f%2l@$v5Sq*a4D3r}9vGk7 zIb%>M!mQe)Gl`g;;0Ush1{lC0c&EwzA3iLt(Fjv!nRVRMmlalPV7$eF7vk`}8l?%&^DSo_wn*|iU zQ3Liw?W0WG*#YB=QEk%H%gfC?Yd)0F;NlYz=A(q1#J5l--jRJc|HiWpX_G{u)L^u@ z&97WQC44zX(=`dzJw`XHNEy2o#-}hhU~&aW2NnmCo}NT)(n=OE zllgwaM%y*TEO6~!VWx|3gW-O66CWj%Gh_h6G}f_l=6ErXxX&35^g22*{ChKy-ZAEQ zPO6^RS_IbORcg`?=XHM$G6hiX)N7t{^n&5iIORZ`hz` zHZ(9`{(xcB7#L(m#<>1^2Ozc`-DY*rB$F-dLDK|`Eh+MZN(#wO+c|yop^e4Q8{@eQHw_Js*7C64h-`FZvFX(_i)Os)O68JOk$Kv7p07bc+H}yBJKeWgo0I{ z$(DqP)-lfS@@R1`M?)OqWEhZm6r*=)%d3WhW1H|Zm>LGw9N0j`K}r^KG2uL1_#vdtJ2fv);GCB=1F5<$CRG7YwnSp(;oaDFmnx7h(V0w zEJgccjGi#GtkH4Cz&K1b>u`1oZEg*uU0o~#wO^boVjiL&?VUB;r>M9d8| zA|d=@VkI-V_=F(0-%`NyA7Qdz4!B13z4<1q1b$-0f)Ak8A_i@}bUSYaUD!2$H6tT9 z&7GoX31cbH1hl=sgrSz*9p%27py6`y{QVE@GH@=kFy!Y{ z0=WISe!Z^!@W;D=t4nR2rEH=RHbr{U??2py5nXGWwIh4ms{DoUw{vXUr7}hjuCBO> z=e3{iqJgfpvv#R^r*uEwhcI0#v-;EUQTa+Yx9wX@9_G>Xf?dyHU_c9Z34XBoMt5sj zzObFtvD7i#alk$LbmNg^c4FMo*MN7{1Dks(VLRN3W|Fc5dS*jt2zP^-b1b4db`O}Y zV%pz2g7p&Tgkny_3CFBVHZVGs&lpq%3W;zAd;(;9qRa(zORdTXr|SgOmoLW%hWud2 z{i9#VFAogb96b;0_0*6{-5c$oMD2|FgcD>8N?qe(cwEOAAToYf9%aUI4$QXVfT>|8 za6yd{?AzS`<_l;qqLJ{_RvYFpqK2PBlw=t)l_(z69a~P8CMwV}`LpZ;tVnU-Q!n`< zx8_kr@-Lh?FoRp{Kb~CIihn2|f?XP>bKUS5GmM0P8JUg={}^^^wKCFbE;?W|>}z`K z*(k&&`YHm#(gnGYfLFF8h`evw*D$h6*i{C#GK{j}&Io1TjP5qi?e$-YK>JI_z-UEA zLNv0|5W56aI%Nujgg9s#A{^i&a`Fy40XKUv1mm+13=DA#9Jk0#Yzw??9DoiCMvD%S z!~sMokS2C@h0$fjLv*arg&=uXU>)7oz=y&|@#tW|`^oG}fZ032Cc`C&OHK(7etO4E;c3(GQrRup#*Nl>0S)6FSuh?9ACW0l&Sn>j}(CmsZ(#D$kUF z2b5qnS0daOgidxxEH@2K&t7%=bPT+JdoKtXn^CL~U=n}xGfOc|QlTSUj1nP$(?<_s zB#53t_rU1gaS(@)iEzjUB2wq_OK0Cr;;XcKO^isKfx%=mG=ZO<1#2X6M)&< zwjtr$`GzbF!^mD{2`tYVB;yN>OEBI>%N1LPdAG(fKBVvB17DDgyx zq+4*Nraw!ffhCDqbjna2wy^z7dBAY`5reKHU^FKbE>q0O$CsP26NtE=dW-U~x6f)8 z0YJ2&tw}z|pz4JvC_TTE<>!?i)CZwF>>_2ern8A?hOrd4TD6#i(R2LHdQ>Hs;%uI%Y+2O+LPDJJI z7qH}01+yOzuZSOHNk;oKm?qGx2BPg4-KZ|Qjk0s>3I0qcP22+PQH}Hn(jvm7^R{D< zKF-W+56+@CfH@!VcN%0W5VVLuXI803plE0lOn$7R11^1|OC%E522dvsItU%|!0|&? zLu)CNVK$RPx|z!_Xd{33G1{;O-J}9Z)UOsgU+!{x4X28n+yQ6K{eQc=eQyoSQ~2rpK=O$gqQds zT^z#Myn%TBU@nU=S_7Chym}{xfO9D5@4|ACupsUkjm)8%J^{5K2qxMa zGLXGTWX+b0midZNmOK!yTI}9nMlcM7N3I{yjEL&F>1>zI2uHQG?EWvK5<`zqfH;kT zmSk55opXg?AmJ&kHhyQYV4y{fu{ss;xa;cx3^9Yn;Gj9AUJth*a%&XjqPcbXHIboyIxY0p5qm) zA6v&TksKwogNPX@`S0-;G_ zP=ior1%sl6;jT3}5LJ61W=v&jXllovDhvk}U_LS$&Qt0z3HT_IVf<=(NkSw?2bC2w zgMQPByhTYNOrFI(hYF+(#<7I)%1wR-{ORc)W?<7<{0>tfD8Cb-3>)%io!Y?@o@}uP zt|#!+&1rP6it5cCVuO@&Wg3PB1P>{0?9%Hr2rEc$oESHwe!{?YVWbO?vyPH{_uG5| zf>5#)K*#V=;0I&px&&V(ItTmIF)ZD&;Ox}{95BAl6AWrCFb29X)8P#Ilx{Wad%+g) z;F}V@rw-#0QHBWE053r{Dpi3H#*$$vhpvxAIVmpiADAt5-*_?FIfXXutRj{YECim2 z`%SauGLqVVf=(t1Jc2s(6_- zIU=NtD~TM|@Zt!HwZhQXjsgB8o*BwuG^0YhPhke?vpQ@1RB2d)-K4XrQ&X-1Mx?A> z4!VZngUBmiqN|q4Clq}_W90nZBQ`)TIn_*QC%}+k08f-o4ogE}KsiGXJjUs8ID>s% zb19KPZm6B2A!49WMC5UXuAvTkgcfc!|mBYuuu+L*ek9ho+qva_zN5(II8H@PyP_ScA+u zjAx|8MAHYDnObm}KF~v0q{|aFHbN z!Z`_}2F4Gkq6lVw@77OFt{PDoKZ53Is8KHv0k>CW{G`B}d*B#9RU$gOj30lmeR(&2 zD9D8zInj+DF?#OBVEo*GYk11`0c79g&)xR1buU9kERady55vbJ7H|&HHQ#%I>T2D z11O)!>Z<{i^JsVMDxNZcLRB`=WdK1m+OCwWatwSY*Q%z}aRrb4gQyom=7K>K3U=1U z{yBqX{{)j67W*f3afOEpW&iLGmoaPFmjMKZ=QK)^r5u>t=!d~WOP}oAV^M4FzUfiMnNmnDJDRs6CChe3{Kb4v{xt#hP6PH??`kt@zs!q*2(((HpM3s4wnF#j^ z^V$4z<rA2*=@izhpH>xMVZ3)2lCU*vHUD-1;br1B0YMg(CnrnrbPp$bpI3Y!M9RX4~+! zfry+TTqTPxr*^meYzkw?=-20BF~ef_Su0fnz**1Z)|WD}v3O}hJ*!*ae62mc4uM?)+`V9kJtVzy!OT=_!RJ2!sjUmsN#NHOuc^cl+sy-}B;MTuMMbsc5 zy3Izh&6;7NTUpp35i97kf8@OZ%)9|x16KuMp63#^Sfl^c*>v11vuTo*s92%zQcO-p z;g#Yi%zqHYWT;Usr@pLqE94v~r+%@!F!_{K;DjYKgaK(<2%moAep@E>=}S|heh~Pu z2UuP2gix@mR|$P_Qu#r5iM0*Urh+S^dQ16iQ$*m%?FiYPtZLS_@4oF2^14@F3>Oxn zWHmR0jW?oC)-!+FyrLUorGA zt~5V`OPAqfc4$BV`$AIS)@;cbTL)MsVVv9Ks1-~~V0<+n7=C*QL)*4>Q9pjwAcJ(i zDMoe~Ph=ZDl@sm1eFdyceq^psSqOtd1ksXsr>}YoppF4 zOfYdgfV%b_L0?Kw6D6hm5QPRKoMecAo^1|_HOhU?vz6-L6_s@{F!Qbi0)Vj3C-x)6>gCU7wpTwyXn?0Px(qZBg2co48HY*PJK zC2;OTlxl&-G_0c!54c?XM~U+C3!5t`WV#B#2qSoeAa*Y#Ne|wVh2BKqutLk`L&wsn>{8A&!9? zq5aI&&|u(>U>zu`+^x`IPV6#m8sgMZzL`7Jvapy@m@~u3XO`7yaIhl=+n?E@E3XNu zlIw*C`!G{Z2^1xsOWeL_-K%`ehOc$daX3`tQ>nB-@=&+$)&9fN)E|E#KO)4@FI?S}O z&5%XJG9{aCBUBh8I;MHTR|tLb!OFBjN32oySWFpc0_4JYXw=uh*;x*MR+un6PKv z{V6yq9N?TZ0C(4)Jf@9-fjdkUxr3^SM^+AIYg-s7>r7^5B!2oUV$>PaX?%BJGQk*4 zCLM808fs4d2TV97osmpX+%c#{7VCnQ8T39AILe|-OEBz)kXv<-BZZO3VUch)A_GRA zIir)Sy#z-ckwd-!SEJ+&7v*=u-&`&|jkboaIZL2;w4#8$(U%2P?gA&XgU9H=)?fvT zO3#KX| z0hU2fS=;j6oCVnSXFMUbGFpah_Si$^t_2Z_J#0&jPZ`)=b`c%teD93utuwg^@$d+( zZ%beyks=lR*ETK$YpI-ArB${BUe%_Bl_g;V_Q_NQ4xYYqVeWe}nB_ZfFu!--`5BYV zEH$zdbkWfT0BnMCIlxZPrIS{Z^4LMtrZ*gnML26O3mJ9 zSva2)1p1xiUy624VL8I5XC>QJ0;R|@lL=z~2)pV0#G4{jq%BEG45@NK0?t{JWCzMF zs?3in9z^q;TRNi}&-u;TM%`!Alv5;oW%BWkVFVL67lcs*g4tr0+H$dqYRK?7o% zftpwCxe~}2XTwR){T+so%EaPvn*;m$ig0tA!xApA1Y_KQEUFkT zw?3@IbjjOMih^$Q^wAxsdxSE>DZxGoc50TwvU}pg;5C)yV<~A48A`c83u!KkC5mDWAz~x#* zSBzh|!_uiO2m)uPnqy#M77(mOC<;r1(u($j zF_tMz<_o$>47x4W=U7m{xF@zuv%HBOBQhlOviZ#m6ULO^oJ#Hdp`O^}u~v!=6-G?4^3A}?+Mas~tu)g8_H#)l%j8eBES601PB$-Y%{%&}nyglf$k%^m@S=?oeooX^HaMawF{NSI{N zpS>L`MRXSN&m?t3fPIHnn7@HpFR%>)XLf){h9?T-xW*7ke)e?74ls)t;=qWb4HQN$ z>i$D%(u6MxHjJhx7?Ux`xaM>h)21~kwJ5=;xvvUqOK1t;OFJeCC^6^KJo?PFrC3vr zwYHDs3!cLGyZcIOAYAb+^b#5w?VHAi^LrfOQiXc18g)eF&bNk(+{K~lHA0CQ+u(Ld zmH`EWl%e3FpeQ_+4fX%fn$86EIdsl^1Xvm<*E`$8+M zCPrOW;4o1`l781x=RT`O1Q|&UhMMT3;y;R%erUM)4fbI1k?6N{?ocw*5%HlHHLAqS zq=q`uQFd42bj&*CsK@+ZBA4FMx2R@AVxvWL1vt4NA1jvNhXGS2YN%t-meDG(%nD2) zjZ9joxF>hiHMyWZN<<%$qKnMvW&rLU^YCP)%q-_KootUHU;`x9rGXyI6 zCIFaq(Y9J^me5FYwZX2;Y6L#34@9AbZK7kGxY;uK)oAEo!v4E;fhmGTM+~L;a@{63 zmjswR|U> z5>4&TMyXeY%ns(E1|$WNL7W0ck_qeOu!8sLFs#6s-A0W03q<)tS;o+*=f?2b8E!!<{1u)zQk@YiI{j0fM45*Re~;o zV}bCy{zck=S#=YXlL&etiz$9P>nalcb5Q4S@FPO?@z8^aO%8M^x< zx|?S7j3t4XSQ4+MHAKj5EEuk%bPQiZ{mLb}lvN0=B721)RMH)Br5{W@G0b=;8*p74 zRpVC;++EV9e## zFMkHMQ6@k}sW9PEP8ghfHiZsPgbhT|lr`YI)0%IXdP+D-gdJ(zLo60?1V#Dv@--r= zM`$xnhMsHeF@!a!v>AI6uAdN@HQ|IBtBmtLvw~rMGQ(Jf?eN*s!^jb5E?{u3 z;!?ASI~5sckZ_vLU&;^Dp96i%@K+>#EGE>)V_ftxcP!H}2Z2~+Sxg&-Zgva=BIVX{ zhDi@!W~WhYne^E2F>Lj3n5RmS$7n{r1S{*96FB}J$F>~C^7odJ1XIczNY1vA#Lb&gjbIOX}?bO0>oO$kNH*7oc? z!b$#ZVSatSdiJO-`kj^KDh>au0>;|e)JNYbV$B#^&5(Mpw}+>_6u#S= zqv3Y;ZF)z(6C_Lhbf&*MH#ux*fH0L|=gAth>67+sh*VupCT;GDi?P`A1SkF9`oKZ? z56M=4n1hTabyJ;RGv&=*1o0$8%mU<>sM=drWh?g^^vuG}zu!4!KBT%I&D-1%g{LO8 zoWJgsAd~PA#DI_2qCe!$`=U47GDA+|^mjj|n7v$;P-4(At==^6Zn}IFZfVNYJg+no z>rs_~M5nn=dGcBVDE?mcQ$-H0Nz)7Ne#=C&TwVfaW-lO05}-=f&qz9)b< zvK1vXbze292mUUyZe(~r6dW?K!87#5;k12STS+T;8_1;nP882ZU@hkpxvBHlbNu4O z&MNHg@9 zY5%*hTNVe8$AQ!wE~2GzBtu+F)bq>Vi}L}w4785TTVn_BIdf@Y7xg<4Vd@QfaB`;` zJ7*ouzF}TVPA%D*ySKJ$D_f@8#~Z+3v)9$D3sS*0swose5~Y7}V%K>S+0-V>UxiV1 z>sOu@wlnrP@Kxa1>Kbb(?;gG`gWy*l7NbbdU+bwgmyQt5tfe6$z)@PlT}KcZ;CYvt zrQ;3-ksW+ER~HeGsa z$JnneHiVi>c~UXUIu}(WPSkXrU-g-FI2gw{&I`@kJ8jeVSdbGv=X3`eY{N|gCg>Mc z(U-VAPr7o|Yxb;UaL+}WuUOZEOh-ZWqLXnBwp=vP2rz<1+4VI<+wmgny^qQr^7Fn5 zJUE1lC8*J{8%22A9*JMx(;d3N_?(PA3C*LHkeQ+R>m65kgHIBF8i?BFtHmw`gB~Ix z&m-1SGyAp{Kn3*}O>Vb9_ES7w3AkZkp@7GhL7gLlU9Ml-v|#{k1YF6FI3hrpHkdHw z{l%#aj(xqB;t>CaP(On?4khEddA?K5Z!3_Zw(36-0&*CiWU!qa@a_D<1DOBV?c1m# zX!|pXAKAC{K)HfC-db^?Y*Tk)&l0YwKS;u$obRYW+EdA!>?*8a=xPP%6aP**d+7`$ zIT0Oul|g#wNH{ntB6stAI&7@Qe+sY{f*eQG9Uc1E!+%CN3(E~t3VWNxbMlVcjEzgs zUD3~t_xpPrLax((6LASr==)Wiz-SVu|K6WEqYUDmyiJ#M)kWgT1<3K&O40@V${M>9jvYtuo`UdKsVebQaHzwj@)L`?AHM<7o_2!J{!ZX>%q~zs$!@2+L0m^!rZSLK6U#@5xl( zjQz!KAe*|@rW1Exx7h=m4AGC1{!?)tCU``(96vO&yD5EmARK!q=pGGW4{>GS$1+r< zQ}Xl=A#m`!y!(s{$U!TH7HWfi3~5+T$O0IbDQHmYDSED7{3Q)ykUu58x8eZ#q_e%0 z&GDmWde2N;mjwC{ z(E?u2y!`X{!xA&K`dzGBn_rJJv)9lDP2K;O;I56tEjJO>SKL0uf_!fT)N;JVa40K} zusHP4gaTkiqmLe=V&(_-4fi%Bmn?Sj_YfT8@kt(ITv6*K6ghL;HW+0xx+c^OREB;o zI{$1hq&<%L8cFbeQLwof$w?_LyBOa#9q7Gu={;F5)@-ynDO=cNlOFA;9_;RAm4>#( zQt)MV8-iFkEDpeJ^T-|nH7|bf7D=Tcm3f#Ns&H7%d=~c0fQ9gSbOn@BKG2Yc2aASk zG)$FVJMtN)-&M`A2ix&r9`~LgRhHp)c)j{sXh$2v539aYGme-{>O>4L40f)`Z#UF_t%yV$& zx(Iz(x8PWhl-{9*EX{^?#Owg`S_VSk#d`gX0pKoE9*Fw8r`0nT0H%UucZoYU(HEPx z7OEhyXDqXxRcO@|>!uDcGbw@W5Q3*ja-H~AbB(E{H74o(vT^*Qf+~$bn@tB3d+;*Q zPplFn55`kGD?!{j;cHLf+W|w-JaO9Wk~!DC_&R@9^?~^)oox}5tr1(4nUa7sdSbI+ z-?B&`qVWWi+w>@+G+ZMMBcM<{k;Um}^{G-Z;1A-jVikiGW#4P)`khGg+3(buGDN$} zDB}tP136JdUj1C zTYkO%OczQD9_*fbUW=?2lN;ySz?T)q;f6gp!z>i6 z@Lwmep0pXkjUnEIsWWl~Wb3Fzo&J?tJDdQ*q|pVX}GQB9y=Mj%O5ooqga{zSaH2wszCQ zSNe@hXn100BN6UEg(b!K`O)FG!(9tFm6v0ipv|q?f3E^xJT(AJ!d%+IChTLDt9!L} zA3)!gB8@p}Z-JpQG*gm#f{2q|uieu3#6pbPN3caD?NDChkHJJPHRIb@E9);(rwVRL z9P#%cuw8*kIlSB4TYxv->b*uFmmfKnwjvCO861}pZ`mpJ=FT6@MsG9YGb}GaKrkXI z`(FG(Omzj*z{2L&3zUXT=PMWOH`}Rj$@AyQ-2&>@hH4i8H1pdr(zJ{clK`52qc_@W zQgZ9O!1M;CFYRl$QhoxoctK4ZZGX5>v%+xG{R!;X5*0FO`X+i!v+V6xglJ;;{M$Z- zCbE2fTk-d~2*>YCCI1oz>je5Fd8C$0+bk&*C|7XN6GcC5JcU5tLhU4omjLV%*nDiG z@9zRn-*0wb$NLbtiP=oNd|x$XDek?jH#12(B>1P{!U7a~X&$%@B^E zI!403QWCRCbv{!29 z65p!=ttzTz;p%+$kTAkG1v7fEwbr>vaO9NPo|z99#kh~Ug9-j&gwS$?<-UR!Y#7HHih!8f zR;p@kuXX4)=%x|se4XUu=M^(?0V?%_AuzP}{HT>~K68;9AQ!LCU4S{>4jE~^hYK!R zv6jJyPYB!fjWq|0HMP|DT=kau%9mrRCpYQ|jHxOoNE-*k9CX>!{&Ke5e-0bQRGjem zxSI*W2*b8kCgq=SJXH5&>a6>5#pT2eN#;@a7+4p=SV|rd1Zk!*3(84=8P>2czBF!2@Ay}?; z!f=}!g~Wfxe_Ly5s!;GZR_^94H21yR-pTU3*3Lc@S=q+aO@liSt60;5n@!16V8_Q7 z!MA_!=&cS}r)D+e@yRW!n@H+zKGcon6eKAp!+byDjYPjHKnM&E+*e98LgbHd%C14a zEiNJcCBMC{$bptneKs=Hiruza?}-UH;PY`_*2^RHqJK{Nez1{EdoK(Sc-{2?T;0#S zN0+cGlj(rZwo|z=zPQlGO3$IS2{p8Q^#Hug3+Z4c$jL@PGVLN14GY72GBZs`_Yu~LXTu86@8_(mt zrItG13*Wn@2_?XZLC$a%q*pOAC89P6rBxxs7@~YCzWCZG=&=O^N-&WH1aR%?!(Q(# zr47(iD$2ryb0ri7Qh)u$NIm1BVg>WBxD$#pOWdZ#u^2#h0OXKZT0kIV#0y0oQ_v7h z>I(VkT_9Y{SVZbq*;;5ZP2;!%%rVq1K(X`N)Cqc>`(P^pYF49a(|+qix_K)#%ffn}9G%veuQ&Jqwq~tC@P2Nc zsU9^lfI6h^Tx&tj!QJL%S+fZF`)50a@EuUFBm;FBqw-f0a07jI(36Ez!tLmh<63~* z{mp@s;?a3n2YI`WM04RsYYo>2u5QGczQEO(%?%^Y`460hBZw<93gF{F$}R8FY<731 zT~~`w2HZe)ua$sZQggX3gn=pPH5|f&L_>vR9s{3lX>B+<{IovjRjadu-P~P{*g8FdsQdf*2n?z!O=uxI z2b><8U7%={&P*NrwIGDSShZBQ`72*Le0=3H2V-HA8X@&dJvDgs4~)!S9ZuBTZ>_sM zHO^nCVA;KTG`ICEbLVET=b}JkSHQQFcKeN9!bra?jT2fHer^Cbfe@4#&67g<8sCwc ztMFsjuT-!$A#gb=>Gdv02s7CAw5X^m#u5k}mg|_$_w{XZ*lqEP?EF5Aoc(;2LKYz3 zJT^N(_d1x^g_^Z`Q3yQMwEO(lJ232)buNaR+%1(uo*~bNgS(rh&BEhj}fZSrHbOCT2$_cMP4k^f__uW&%`PSgHaA zq;STAUmJ&UCts>cl!(qM&C^_H!!H}S_qKJjcj&rtZOHqeO_2i4SgbUo{W#hO5@NMM zbmlNO-h2;UuMgvEOuSPK2wN~eOrt5yxlx+dG8Zf zG2MH7XV>jHQasxY=I-{LU0cIzq|&~Mr(cU+6q_zp#GT$twu&~EsyOB2pS_^Tf*)qD zxoFT&4jzl8;y)0VT5pL(6wiE`tke}0jnw@n^FFS^l%BhIVl_N%-Mi;n$x4JDvt4aq zKb4Y*x?e-O*VsNy8U{u&T1~jJx*Xq`v8-zrrd;3GIA@`EXX37>9^T(Bn%@F`Q_pql zcj{Cxq+mVJu{?K*+Md2zcNZl})c8F9?!J1^Ot>xi=4fl}C-9n}UQC{arsuu?d4LQy zwk+n@aPyM{PV$+d+itfo^;4Jc<8teb=i8@iW=$CZ(9+B2>6Pg7F-G?zA?;gv8Cy)^>eLFraC_sDG)TX{sH+c zo%=u_@Z|TzXIF8usSvN1UP-h2$&t@ygE=ic(~6~z+iSbd9lIoBo+r)Y$I5FB>ENek z;gi_MBhg*==UdI^EuVK3`POAo_<+{a-GtJk>iTzqONMbrntw@O)j9;IuLHm%6i z%VjFHmxZPJq{h}Qb;!S7EzW!Wj<%uhDimpl;N_%xjR+{O)%r(bU8NBkl68i*(ejjTSNC_wEBY?pyskKzS>B zHKFKNrA*YS>HNa5aG|?2AFmt(;!n|N$?TW0{*)xg%HUs%xz^9pizvXmyIMzBJZcip zsC+tZp>wC3(ic)P|5kcmzwo7r?Gme_9hBvf3@^QU1Z>fqvBK5k85C<>)X*6#Lm{uN z1+LTq?4)fmhW!y7o|2W$o%M#vx1{5$9Oh*0$ax2K#Por>K@W149M}){9fvh_C7Kfvw8B*4LU(bV(mxnw5V8@kb@1Eg_2pdt;cECU4X|WW%3!JDQc5Rm``m11 zf9OakRAut}QRdeh0s}b-CF2&O61_w13`MrQ2Zy7;xyIUV+dhkhw&Mixg&k5%%uI)T zdvbuP0ewrD&wR&tqK~#D9jrC?wZ`Hv-9Oov(^OZx9iB^A(9c1uQ4qZ(I^jdqixxVi z8^;W;G@GAl^ICYj48nE^>@Q|_f)}E}Eqd3N?cot@OT`!P6>xrlr`5yv^ zEeY6iS4`vX?dm+Ok3$RoMdX#HX(e2lI`{<+c`gM#9h-U`{r;;HZVqw&W5!>P?`Czg z(W&{Y46S?njc;D5yY%JubWsjhH#F4>(Rr;WAH%%OB0rLGE4bIlbkPJ!^XJhg^0p@u zDx9&6k)xA?vA*>`En5Q%I7VUs@jpjyZeoU??smq+4D$M>|N3??ws9h6`eG{*GbkE6 z+B!QJ8aooR{F@+TYvc6AcO+*1_qmL*k-5I0tsAi>;ETY<$VAM}!K4kxplIu)?_~T% zVi2&gvHj}L_zz!^n3wmz0R9p9UjXtBwuVZ^PQ;pDD1=4702#YE5o;4O2-#ZMIw;xc z8yXY;gHyRe{x!-!JTXT`IL7~g5_4oCX8bo`US47b(J##Zx?YTt znC)MeGl+fR{7;LS82Inj&!3FM9RHCH_dhuQ1^geNn#7F%7)_D*OUZu@+W*z$U&H=a zm4Dmvf3Pta>VNhAU*i6^-v7VOjQ_jyKid9Rvh0`583e4Hj2&$LF)f3zv5UE(v7(qD zF@vZ_Zuv7xPz@joK}GyPvL3(2lOl~yoAraIX>r@xUB zQ{3Zvz!J}LiDN)0gh}|yg9Qgtx(GvxqN7r)82#)46$>FX6hVg%@Hbe3y+x59>QxXH zIoOD_guSX_z1vKGA6DO5Ici>7F}?CyY6Fpf^M~kjQUmp6$x}nWJnPl^hBh|v2!@RU zilheGvtwccA@(E;HviVila^L0K2-m3N$+dazDCrG&bZ?->>Y?QDNBd}M3gvztuKFZS2TOKjyZmN4 z#ZEyaM4nbE#kOg|FKFY=pAzXH2an(9=AJU8K>+A!y5mQbc~)UeBIr1mVYgr)Fo^>k z9S%ql&m3miAa2E``{58_5(dSnOR44u$0H^$H}y)-28=%v`(<`Q-K3}P8xW!aSiUv3 z$s(W>>egvyaTiti7Z>V_hi1>+D_ex#O7A=)9${-EaQ2Ylwm2y|aAh(bp4V#&Fguzxi@dLXERPYh!=T?rOIhOQ4j@e&x{B8eq z2Xj28ja|N~a1~?m`H@LrFdK^;6qgiB36GB>Af-$hy+5LS?)7k(PmK1{X6E!@-2@#g zD;y`n28Nln2XXtU#nKzW1^O)P_+Yb9LF0I5S)!Xc>w-uJ|M&xIZ))&;uLHJTJ?f`i-mT| zg=6uf2Y}J)A>#Wo+ktNqg9Q4Dqkt!a(~E7;2w1Y?R72}5s?v(<7Zu*(S_+O)7R9KpN? zqV};U_?5~zkp`o<4`>-s*F`T2bV%S+$|j&o1e0Sal2fNO`2CcyAl0Viq3k3{809n) zbHG;T*O99x&mq>OK#u2=Py9}aE+>gEBDf;UA=%3B&hIW%q=;B*Gp|;j+Lomu*D35J z>BS(Hz?^cIvNu|lP!VUDbe7^tA(yz6;+mpMpF^cf;v@M{{MG`d(97SeWXd&r zDodB;bN?;mw6-nGqvgfoZ~rOJX##FKZWk^Nt}<>4%MdG0x=s3M`a${;%W#9427Dug z#(~CBqYfY`?Q;C52}OMbM_yI+MG4&uZ{bD-PnBV5y_#u(Z2csfc09c%yFjy6vy!Wz zoD_o^gKmw&RUWTGPE}4}w^+BP&!3)Y;#Iu}fsEeLyzsnk{cQb$9YeD6xQZE?0*wN$ zA)sNCefSOUo+f2&Se8V*MAWE^((?S~PrbIOhA9NgLo-U23fA`F{MvPO^-9sjT#kP3 zkzWPw8P7l9c_P#zvM@I=b!jhXmFUiBiPWjpA=RuKPHSKKR|nE9wGF+7gDv(9>nDph z4jZe==(b$CFTA+YwQDyp+hyCQ9*`bLUnyT1p?N~wLOPKimz`X`w+DJHpq?G?v`pSa z?m<&|Qq|2#lx$_GPf|}JP8Pr;U|nO)(_uTz*zdI9JH*i&(MRgK+2zeEuO;nioV|?8 zEJYo;3|J3rEc2|qx4iy9mO_pbix^HUNjSotg4YR6sZotnt!a*bG`O<3D&ghi4dadC zMeB0us_kTe?1aS$dU&076v49>Wzvh(f49^TIR1euH#|=QU#uiqDq)QBW5=D{Sw!|)_`aVUG7-H-~|JKu%dyQF8< zO0GX&b!a>&8=I<*)$LWcry;~6BG^Lt3`6THJCLrv*KKt@fLKas4|$D08v1b*b))@c z{1yk168JlCMmS|aJIqj`S^`624@^#=aohQ~?ycW`Sz>Hr0X1{kOxc8aV8c_xoFIjM zc>L%@}(I3)(#UsfxC<4^s>n(n7nu;0c z{81Q3J-pvf-?tef7`+>P^=36;b#*rFa`^~;>t(&PM5+o}tkzm^WtD*Sp#J(!W&hM!{%KpcsgJIY@P+tI+>}_5SkZ7_jPnB5{1SU9 zdm>w3RQyogp8c@x`}SWUPqc~?dFF!FCkP-3$Ib5W6VX*R0{4o0xL5fJ)qBoE)3ipo zM&9E3Vpvllo4Kd7=hlhuTo%D30Uck)=db(7hqUziKc-Ssag!LC85}vT-nW87K{pX$ zSRtIMPP}am`?Fcqwbh$reknCNH?0n3Yn`ue$|)75)g0PfURhq97s2mQi-hgj$-3v> z<|mC;)}1GI_3z1hRnc9LUPmw5uMC?HZC(e_pILa|5l}Ce^j)5xI_o?qUYqYT&}GOv zeB&O#ce8iH^Syj3I60c^a=xnfg{LKxvd;VS+$FuK`cTuD;1(f<&!p!Xxqis0vZ)`U z4Wg%FRbo?-&ygD*b@zkUW05IydqLgQA5D)*GZi`BuCJY@2BQPlmNRE*e3F}`T^uiq z57tLU$NO`x=2yif;hPm*d=Gx_d9Q}|!*M6)a!+zeIZ=GBzhK{P-6*Y14!0fy?_U4k zw}=1wrC$!agpiP+zN4`bG2_2o^Z#*+|M9T@>%9K|oYMcgt&9LB0Q3Llq9$ccRi6W+ zOCE2~Zy-wgKCN>VB5)8qHu%BntGjp>f`%qQJ#OFqo_Q7HG2(b9Y4>*tnaCpwOLN2} zT|OS~wz5Cpf8A_xMowK|=X_p$a(_PFee!-SrTE}w^LcwB-E7(NX=LcHnBu(?OfuEz zu6@%Uc7MiA5qpb`dE1D|iShPA++*Z>Hr0XpRM&k?h)ce)bhqL5IPmMv-fyV-cE>b% za|f`Vo%<~_J+>t9c+6A1hmz^MX3ZFRhI`*Z7i0XyiK($_$c^NQpo^z5f;}YRup%1C zIHfy02_9TyPWbNiu^L0oh0x!^w{Hbar=$DI#sP6p8n&Ob`&`2yta|vu9Dj6o!9ye- zt#@>{Q;C;9rT}I}9wH4&g1Qd<6LFi^T|>e zsI0*Chx%z|=9LXP$*Z|WKecddj-9s1Z#(gZ-MS3yPe{0C>dU;IA=7oe(qL9{=p1dW z4`ascDh#OW(d+Y`pF#wNE4RP8rRe5OHXGzOMsQ>Q-C@u=u?alwzoAUM2Rm8t`_~gf ziku_6g352ZMlu4&yMgAub21fS+vsAlWP&B{jkOR{;TXP8pL%ugF}m6P*MAMNu5~{e zySkd5dxDzXUvQt#p2upYy5G)5!ru?BrhMExi!W-XbU)6XyS?4D{_3dL^zY{`b*2y% zJuWPNWm{g2IYqxeVSA=sdRR8^z2+Ug*TkT_SsZ)4tPUAJcRe<2B6Jezj{T;q9{TyJ zbp2fY$mmlw{1P=onEFfIW{6h}YR>Xg=e9Nd&7v05YjylltL?g6?$=7>l_*zRev(&a^@d&|om*LD7x zm4(!6_)ZyuH^CY|`i7Ce@TO^8Pf0dXx?sl^E%H9n^WU}cs`-!l@oszU-Hv;7#ESx? z&#ia+CC=&K*vUqsKmte0Pxi==`-A4f9(usu7NuFbXu$WlIfT0 z0Qb#nY|8n?;tH;N`rvs1a{D_d+9VVKC+YTypiID_k%-uFpstxUNR|Q^i;80L;mB}` zOW-8+^hneQ&Xdp+!BP#D)F+z6Q1^yjZ@6aUnQW$_$Ky7EIMe3+CJe&iv)*UdG2MMP z4W<^!Xp>jxqL(rBTlHi2R%9n$Ex{S}GhfjoJwnTT_0WevWnv?BK8ZYx{ zqjDaA9qp0`#hu&WARkjDEzoa!To#j+x5Vobyt!p~r2pb!T|jgA0meh4JEv#BaCwsf zWl6*(Wb1W~(8-_)=1{jQjK8?^2=b7$AH>)H61M&^O567J6B_pQGu5qNk z3wOYba-C={1a?xQN`~F@tCz`7d&6==Qcx?t_g@*z1}0&n$FM4vm%V-GK-lQD}s7m@P*l5I8l**B>WtOlQ!GLY-}) zDmwTvQWXLj&Mcmk-A1hJ;SR5OaIPII=^&Eq;GcQF3|+=lOfR0K^AFGKi)W=Bi|e#z z7JuqL|KcdnzSs^?Hhb_4d9nXpAfMm?4wUPt40qe6E^SqiK<-frk6$cSqjDayY$3YA zE`Dw#NPO$lut5`nd)OwW(#b#v^1CmQbVJ5(0OvZ1=m(6+lFD~$c;BqL%sHm ziXT3*Hi-bDtnEL5)v>_v`mFkH`I3_Y;PIpX9$X0nQ1TH{?4;SBY(u?h4rfr{>&(Z8 z0r;hDv_La(l88B093|mlIPySJ71L?RVfqSEjA#)9pCYN6gdeS2UrnJ_AKOXS?x%jX zVt#$-!FDF(u*I&w{@@2)a=;~gbko=8Ow#(2Cplma3*UMM3;%1PqM2U&_u8UuzXpEv zFUaiL#=eI&_|PaOo?U!#460jgA96UlJClVf`2oZPmBY5g-A=|C!yw2*8_Iq4FOYL% z0IG0>YsrPF*d;zWLJm9YyK+WfI7+Z$w`;^wMJ8?Xz(6IgW8%YdK1TJlMQ=PJ0ZRFu zU@6p$SE)&Vt-Z1(lli+(kfF}7d$41-&t`zRYn7EWGL&l=yKf6SQAd zZAt~zg+MzpOx2kF5#++@eIZ36MST0qq2#dzE&M$*ENOIQ<%JXtM32fcz({{~j1!#H zZryQPzPxA?U>$5EG>wPs?`@3^0c3m}t-Kip31%_%%Dd{2nEFBgTLanX zFtVnb$EV26AI!Be#>yr6b;C$~W~8F0mdXM}cX3ifP%~_Q#mioEObX@9Uq1O5`gzFw zh=TH*zAp7cc(EzW?C1PiEX4DosjV-*cLOX9)zik1}|6FN;NdjW&;2bVG1*883->65#f!@k)JBKtOC2M zc@26d*z3>;=n?nnpq~yfm^L05atL|SC(a$#xI`>!$PS<|Ix0 z2pshoa??+P46}RuC!p$wSQ?m|R$LwcZH{FZ^}n#1c9-y-?FmwHJzzYQ)dLQCkxMVv zX08Zjx%ODcVJG2)XHrr-;+5J(Fo<*3hhhDotQ;){DA5A~e?pBaDa^+wlLg@wf0f@% zuYi8DGDOMLu9Ju{21T)i*>STCq;QJ; zoCVXoJ{R4?B$HtEU&L&g1r&ti8n9|dR3jg!wMA!#2)bMsPiI%e-{nxT3&?A486|nZ z{29qK6Ii;%w-|?qjOZaMUTHDywJaLsJvQv4`jNFVra!5x5$jzUQ4P%l&ULx^%tc{o zLns0pE5#<5O6b6ig>MeWqHsyfbAdc9EY2B@o#JLC? z4t~&Qd9W>J2NM;*e5nWd^#ft6ra2v=bF>?l=;Z~KSMi*9SN}~(h#}z^jHjLe#rPX0 z=tB^a60H<>K_d|AXc+T}z8Ht6KZ44n6T#5hEg%ySaVC)N`w2L=e-nD8RB3WJ?#7%( z&l9zwJ?Y=yQO?$%WaK>oKjwe=5j-d>ewnRgRt;%@cT8-liP6b((S!k9gT3-IpA_@rR}ZnVG+{ zv+U6~Fcoi9f@goW2+hGA{$sP8lhkOA$55i)ZxCK=>;Vu-+8Zea3z4;YA*qO6bL3~A zMnIC%{4_?b;^xB!SNMZxh^U$uMy<<2jjw`>_sg#3{?LTNV|cm8#EKPQQdB=q-eIr| zeDAdl##R$zZWaXs!@Er(41uZN`@idC2s+-wjkaifpIC{N6HZZ!BveTrvd=GpZyxgz{MLm(peq zxYrvvxcb4U#l3fMQNHc=d%Fw40J5_dxZX7>BHh+HHz_ zJwiANJDo}@KZ~1Du+11uDZUA;_~}^ZN`sF?^edK^KnFRB6=C)5C6HOhSt#Ni)W^=S zx2$gxByHpfGhDh*sz3~%Db8qlt0=p8#=!}NU8;ofwsNE4Y?bTL}Y8hkb=)Ov3A_0TFo zsv^iWfJwg?28Kt(u|H3aJIu^(#ryuOT@hGZDsGEIy zAvCS@cz4pftzR_Zu#~k|FkL8fPpO>`7X5t(%4i@$4-!w0FGnV$t9GP-N-Q0z7qL8% zw5%JcW}0FgVW4I^biNpN?0C}5w%b{%slzD$1V5Koh=MbLcpd!!IgkrwEHxHKtWMQx zo?DaZUCrP!Vx1yCxXd#iVW)?di@Zrq1DGgoU$d+qi^J8H*|#_93PKvpN{N8XUMnPq zPFywP8M{M{*;i_&?Zb1gXqP%2xk&8CboT_HXjo9;1bm>5eG^x~`@n(j#K5x4A2jB~ zj7PGH*J-Q9uaO#zFDj5q@HGRyd3V=_Mx z&%V?lThxOFIRjzngz)IE5zLR0fQ@F4jSaP`OaoGqAZ7j`7e+3Dq;S+&FJhJ958|pd zU0jZqD&Sr)-Zz=S=0|)*LXbw)6gcmum*jVkDbjT{N-`C6567$ZblTg_*yMU1#o?Vk z5QT!4;_=~?mA#qo=YcgoiN^3R#S=(c8$KZQQGzJ@=;c8$)D``{{90j1ufSqq_RB%b z`_%Knff~E`K9j)ogaZT}%w`~2(<)ajW&v_S@~2NRR?8bn2s2O!bRWX9#fuTY?!Aub z3Cm;bIG_<7w8%IF?t<)f+355IPP9} z4~I3-T7a(z+(FS7ZcjxRWCEYy9%AZ_&-MzO#EK(g@gwmdfmB1I(~6j0pB@QHzY_3e zK+Vz!U`*skrW9^X0Ke#!N9a{4T&)RhTlcTZ?$ft6hXIVX)9*Jz)J`A67vJBbPkT6| z3MxWTB8VIYNH1j-9$hE4C1rMc55qDCRp+3AtW0yo5~5!ut{x;BM)8+WCABMrPflr1FKTI44mPQNaA>EV@tw@Nl4s4i=EE zzZsP_LV+5WSq?1l%=3L6g)NLKfQO8ApftT+vAbWv-ia4yq7TL(NQxrMcd>}$0frSu z1xU3~mF~!{a}C>IDhq7-b8@^vfbhpGM$3S*v)LRQhbvoV(?j%;{8i+#>LWi7+->2R zgMHv}kMNHd)G*{%S=FSBGN5fmJ+PET4)gvU^cS=G=k`9Q{GlWtp|*CURyJIvEFTB7x82VFMjygI_Tq&2XDa(pVT<3Kg;rQ4S?bnM)iH21d>z z8D0h_GNYYCW@dya7+~^1Y&1ZXH%dQ8JByIlwRmO;4^oS30g|*Al<-|y#GX@z)g7BM z9Au6qH~s`1W=*|`YT{!!*}upF{*Pd&o-|gH;4F}CB2XA##Fx7VIgUxc=M4CyB76u{ zluP|ox_`{3V&;i5YM;|#pIbJSTg3g);yIU=PuO`^gu@JU6v8w|J&pwkcLURGy2!zab z%~-AJ&JJJpdaTs~EwgiUx{i`BZ!0lfi)dmREjc47fo!nOL%pfn*3i}sE@lEIc3}v| z8?Jbd(M_N#y*)f)h>)VIsG~7g~ofmT<*a4mUN@y%JHaovp`x94NC%4}3#! zg^9@cEH2Xbu2tu!_nfsf|mwXyj!e2v$`921Eh$Ay` z5sVfh>MpL*GYSioA=cn!5+&EP55?fyn}FQd=?j$8ay$G4n${;H(S2xWlbykV8@~w} zAc1gx#U3k%AAg%E6)b7p3jbS$#u^NddIS&zkLXa9E(Aj07i#_NeNTb7M4w{QbVdD( zFYXfr3nWDjh_EQCT8+!^12+e|-7uW7Yv74Ecibt_PN*|d8J3@*xTJlX6@p!2$09B5 zmJmZkk+DhE1&uRVy)2&|jsbjM_%GjF7=rIc7s0fo<2khhxU&)si{oR7PhT~@2{L(d zLw}?_gLX5eef@=j^#OaL4F^^i)vN-Br2Q!kDJQN#90n+POna`UArzto>>>h~pibJN z&02CbImj4;tPHKnrt#^WTm(io9rR@m0}U|<<0r#eH&FhNpLRlRptduHRZ9gps1hiN zO8XlGpOIjC2l-EDnlJZ6F>;EB{H^z0(FmZucV7m%!Lxa>guC?ig z?5@-kM3VzjH2RMM53I5=#KGnDs9_GX3$VLVEub+QdXR?=bGBg&f>9y+o#0s@oAyj~ zm1B^`RsR^lCSbaMg6!+(1n+y{nM988{z$b8WgY-jo-A&=;0s#%)w1Dre9OHsNFRKy zcWV`b5EF7Ex9Q1S$KF4hYc|>|9)&EyzDMoMPhXWjcii8$S0Avl-c}n(r2LG*B%x!` zHL=`fta2LczlZ^C>tqHBiYvRJ#Awo{6kDMx; z(mpkM!QQ|eew?M&AIDJR!FTdoaVD@*k3hZeXN@X^o-x{!nrSu9q%^3oiloGwcToQ% z3U3xi3yX$uguA==kZ1uPs^n;~f!gU`BOz6Tlx_sGqjuyTn zKb9s)2}j?N7$!*7eQua>2+j1+v?B^@XbW+Liwe5srZH>GJ4#6x>EMVzCyoViXs+GN zkYZ8|5%)l)>x61*w<$)*7%FtK*bQ;fcbozvD0-?N(P@4^_A#NM7AA>>g`*@&aNpNc zbAz2*K2bqo%7MB%8zNXHB>o{o+wL&MWadc56|gP%ax?H$nTX&JFY3|t@i=fIJ-TRi zv&w?US`rGQg)#@JPFw&lbw~n?AH>Lw;pe@h=pFX_JL&#t@tT)nO)Yo)aNZ8PKq@o& zL`;a@N+q91NN#Hb!n}jZad`cK@8{67W+4l}+WqCYQ@C0yHr77hH0vRDcXi4+raUnLzv3w2~SWaFAJ%nci_apft*E z5z~F(G|jf4jwcls#WcpOSA3Jbb{JQT$W47b+4=wQ_7-4uEZe#$?izw5xI==&oVY`9 zNbun9?hXNhyE}y74hilaoZzm(9fI3!lAXQQK6~GD?tAxpFR3xRyR555jT%)+|9>~Z zR;p{Ho}l=L%WWltDDjVByJgduWo-0F2YAn^7DCmKh1jj_ul#2yk2?rDR0Z^@pU3#V zLx{c@;N=P{a~k_$2LnG#uC~p*vy}sSF~Z5o!&ifnAjXBa-iTIVzHk0*i?B{_OTmu? z-Dt&6P(UZp+$pjilSflVC~A_FkeyQpqb3IWIevX39L72Ilu%HGvM?owEnzGe!7#tS zEjpM#hE)`4+MXjNDEGjT?{>eTWB^^0mf};)NhH4J{#W3cN0D7nUnVpgcJiw^zidw* zA#Cj8j@1}NOGr)4cIt|CL3d5&o^P&rppQH3@+2?@8B0}(A^fA+(yDY-kes&DXh+#( zg!C?vu{cyWIq)3B-a8FP?ScQ)9dk^GUt*#Qo+18Ah?S!?8^m|L`{^L8c zRL95AB=;GdxeEBXF8Ty%A$Ed8dT<2mFn#1P!Z2hfv|epR%`;UwKL{%vj!lHfU^0lj zUi2K!PFZ@JS>n~AcHI|o^zmXPn6+j+k>dm)PF+bH0fK@p<(UGL11ELdcb8q4hS;gR z#QedHa13%hIC%c+)NvYFJ{Ymzu;Mkz%2 z1M;Fe2s`^i;(JfXYCxvan>{9+hQ%*I&FE zvP_NxA3X>YC3Du?RiN$FC`LD}D2>$+E&qWnMku8~zkm=mVKur*I!k^^k(EzG;Llew z_4IBPVU&q9v(RkmuwKA zFU3G;*z{M_BqGT+!DEtGV(4SG7>^9E?GxKb_IJxTd;9xiYCu7Tgm9DcoQJ z<^^D$C$%l4{QU)WhJ-6mdM zg`q$QCx?rZLNM5gsUM?4XF#BRl*(`*8v9A_LJC`G_imp2T<3#mUYt#;eqCl4gW#b~ zWI^O=+^S!I@S)Vo)uAIb_5ifI(6EjDb5!b3E}6TQ_dVHx&LsyQV!OZx?J8DKCC=H> z$^l1STz#$wG51}9pUxt%r9O&Bi?#J3``G{w3zF*;UX5I;pT6xY#Ap{b_u_{LUO`{Mc~IBWAc$S2GJ z^qi8)-z=K32HWBlwb1}?8jd07AqqA+9hy!yLrhgfyE3i|kq#6+objmb*85NWR_{62 z)b?M|oua7A$?hXUmLDP>VtGBQYJxsX^uD)rdDyZ(kwk?YdFGKo?R}DgI1PMrfO6`7 z9gCpzvsaB(iM77g`R3Yn7g4#db@QIsQGx7F?c6K8`Mo(2xaCW&$cOI3ktHi8Jq(#XxCt}eml z&D!4O%b^WboV{+`w)qTkG2YwvH<8@UoNOd~a)AeVjcBFc8yGV^5UeVLW25c2<*Kd2 zjWm6@9yu=pS32)5uzf}3pci>!?D~Q_&4vA)?4xgq!*8KW3~MGG zq<0&M;-HN=)Q*bx@)lH%a1^(mycL|tI+Ja7PPoR5`qAqmSaXlZ_-(3eAtMskAa@za zY2QLJpL5pGd?b^;)=5e$c$iHxy|qs1z4HM3-c-8FNhI{@Wf8%p`_9)oD!xJw-m{!r zT!VDtLzpWt{8pmI>BZX^OUewb9{Bmnby{KIyTmtcTpUU%$hM^(dfq8*mNSOxN~~93uP14 zHqA+=Q>6bW%C|ECk>BayCa~+mr8~FcFkbP@9wMK^Zsx?{=EB!P6H?jekUvQBB?kAv z7IM`(zU&L8t(NEBwxu!iOE5{*&J51kFGd$}MXlZKnJu?|it`Uv-Y&J!%xMibdkBJu zJvE1ye-h2-?^Ixs*+qtXHhK{zuN!WDf`%^dfOH?~A3f4``6AXdB)=7m{CPpnMsIz* zhM=H|LV96Wu`?*SAOoP3t`4DH!-BcCWzac+z z7Es?&Z%DPFlmO-oK0}1AR_GA;h8@Z4?8{iOE^YANa7p*s|+N$uS*wRfCx9N(g`?)bI2z$;8RkOPg;iMO!!MXRhDQ z}unf>1ck*`uk4r49=JEZnY!1=6QC(@)DyH5BBb+m+H;d zt%rKyR*(t|?!8-(Ma7!Cmdy!5U6Z3uEyQzk5VNKScZKJr<)!q;Q8S>enq0PKKc56)zQ}Nh5bQ} z$vg8>;mhIEURl}0zSqcG{+<^t&Wcv{c03QwbG2DLuZdfN9X?+W7NBZ;UR7R1rJF2I z(2%*@cI6;eFu}SgygbNXs~-~Pk(e!;yR&bYcNQzlbN@POl=s&CQvZ>(z&1EeCT#aq z$$G1CXeM`wKw$8BcFP{mf-C1O!^|k>K>l3kqHG&H>5xOnik`&cHo4C-=6EY-e`SpW!sti14dfofR8-G%VO*HPFvOBhxj$)a)Wt} z%6Af$vmaZBQk8NM`~v7>d#o0@rjNO*Wx2M_a3;m?V@Ec=Z4ajCNP8l}rl8lP7#Rkj z-aq8363=?mPEALdbq!}~w#4x#(eaw)E903&S{4eSS?sPDnsd|O8sNxf2_Ik7o2viR z4v9Z(eLf#%PsP*iLCNdeQAefDtfkgMDdiE;rh;$>dPZ+zJzI~G?E;k^h-t&}-mf!J zkNdhQc8pRA88^LQ4e^z{^+720=zXHzeR@MvJUkfDdS+m!^*KIU&8zE})`S}+9J!cw zMWd;>=ElPCt$fC)qaSA@jNM>JIpDFsj@cKgJdmURFp}2Xiu7J;yvaKajst`Piu`Cw z7v=O1{B#*v$43fzXw{s74+Uv&L7^jhXO->C&dsn8r@42poE+{&n&(xAE&9|?ru|y5 zMJI0WIzHAM15rCe1Ywfml@J5SheF;T3*3YZoy|^gxMV;QhP@|17 zRQF-hYG>QKlFO=j!|C~nX~mE)eBkBRAFMAKwcfAt#3>~2z02qRfXKckQ$J94`GPS@ z?RdMRqqaC+b&kB#nr^V8zLF{P*=l%@wL>#;d?dwjnf}hrNIi`8xZKG}l2qc=e0p0G zyv)#74n5-ccvy>G9&X2V^hL`m4BsE9Q8W{GlQt{RHw=`RrT1|X6#Dmdyn0F*N%Mrl(kbAZSsNJ7IXZfN0T z=wJ%4rGVQh8UeYDX@NdIm1O{Nf|=Mk0C?%&&cMtdFbA0RsZPPSNG!kEWsEW;G$LFq zOe|m~W+qk^CMFOwn;H`nH4uQxerxp?AMFXY`k$*tVq*Av)g&yx)%uqxZwJ5vFfD+aX!UCdOs#&6 z0==QduYsUv)?;M`OhgaN_%B292i5$p;Gev{*NO-* z5Rh9C>;LX#e-->=O#j8-{oPai>hhm_|KD}vA0GVwc{lzWxBd5S{M|r)>-`r6^e?49 zMFP}RQ7eEPX=(+yK?wtZ)N1PBN-zGztF+e#NV2+C4o{^3&&u+P6$*^R6VCm2O@dE< zgkH-S+S!>p8|pfk7~0Xx8yY)W=mNw`MQbH1(_gep*57XFFFW}a@khb`=sz1LJ756+ z18Gw;Zr*HN043n){<-~ZGtJZ$lY%f)T!Ik8MVrwj5jM1D=Fa z)}55e&k7@Zzio|sdH*OG3DUj=xe+~IR!km1wGEIzVM);cgPgD ziv2;)VnU;?YS%@gG~`YFDJT+2ZF>n|V|uxDul1st3Inkcq(WdCdDAMi=oDjAsClvl zPIpD%?FH6Qk}HKT7qRkVrz_zXXa>pW``&>h;$mJ6s;U>}<{;rrDZ?qtck7?MymMbR zBsXy9uc2S43CBf!9YGkkn6mlvgt3C5_<7qF z`2WaOkr;tO3a<7JhL#dmM%KSb(tsJ80zSo+ghmh`iW>s!rrgsa{xnT!{!9}fQo+&2 z#=_9@i7Wog5i+Wfu>M{u0saEO_GSX+*Hd$zW|N(%jf1rv3G07d<9-!*nhn5y0$7b% zSlLOyzcvoQGywt_Sn^p}p8^;J95|l>JBWjXjq@phLBKYO<5vJSIP6Tn0vi(&2MY-h z*Z@Weu=W9gm4g|erhtD52>5>>W_H$J!p_D-!otM)OTfTJh6P}ZKM4rT3@nU58c-Lo z?EsFfzXSx(yjg(zJ_#7u1h9aBG(bRXKsk2c91u33*DUNn9zcGTJ^(`JngSi+x# z9cTwTJ8%Sq71+-`ZM>d@6&M3{PC(OuJl*@BJfB~~`p2jJRI}0-Es;-_F6r3jUv+`++;6 z1AcF^k&+`cDilc&K1us$Ta2tAKX`H9L>i10N_lv6LVwec9Gcw<1~qwu>lk|V2um)$ zXSar|wlRH86z1IP`{r{A>**A%$+eANY>n8z#3mEO&Ze&$vh!s`mZWz|WVGEE;b(~x zwivPBZt^-uZ|@eh7CqcP7LSs|Vd#-MKl?Cfm1$zKM42IlDMT!|w}1D&Ui1vtOp8jy zr0Yy>Q!X=R$hbH%>v79PYp&I0#l-qtxE52M;hDVK&$fj=(XZ+!k1rok;UgBBmLE

ra=ek#6Nq%i)#$)4UW@ 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 BQkVb$ literal 0 HcmV?d00001 diff --git a/services/job-applier-ts/cv.txt b/services/job-applier-ts/cv.txt new file mode 100644 index 0000000..11bdd02 --- /dev/null +++ b/services/job-applier-ts/cv.txt @@ -0,0 +1,65 @@ +# Ziad Elshimy +## Frontend Developer + +Results-driven Fullstack Developer with extensive experience in building responsive and visually appealing web applications. Seeking to advance to a Senior Fullstack Developer position, where I can utilize my technical expertise and leadership skills to guide a team of developers in the successful execution of complex projects. + +`ziadshimy7@gmail.com` +`01223381370` +`Alexandria` +[LinkedIn](https://www.linkedin.com/in/ziad-elshimy-1b31601b6) +[GitHub](https://github.com/ziadshimy7) + +--- + +## Work Experience + +### kari - Fullstack Developer +**Jun 2022 - current** +- Led a project to advance the company's online shopping platform, attracting more daily visitors and boosting conversion rates. +- Led the development of multiple internal projects. +- Mentored 2 junior developers, culminating in both earning promotions within 8 months due to enhanced skills. +- Led task planning and coordinated project timelines, reducing overall project duration by 20%. + +### Callibri - Frontend Developer +**Jan 2022 - Mar 2022** +- Collaborated with a designer to develop a user-friendly website interface, increasing user engagement. +- Collaborated closely with senior developers to manage a complex design project, increasing efficiency. + +### EJADA - Frontend Developer +**Mar 2021 - Dec 2021** +- Troubleshooted the website's problems and stay up to date on technology. + +--- + +## Education + +### Ural Federal University - Bachelor's degree, Computer and Information Sciences, General +**Jan 2017 - Dec 2021** + +--- + +## Skills +- HTML5 +- Cascading Style Sheets (CSS) +- SCSS +- Tailwind css +- JavaScript +- TypeScript +- React.js +- Redux.js +- Next.js +- Node.js +- SSR +- Webpack +- Vite +- Go (Golang) +- Microservices +- docker +- docker-compose + +--- + +## Certifications +- CCNA +- The Complete JavaScript Course 2023- Udemy +- React - The Complete Guide - Udemy diff --git a/services/job-applier-ts/package-lock.json b/services/job-applier-ts/package-lock.json new file mode 100644 index 0000000..1063696 --- /dev/null +++ b/services/job-applier-ts/package-lock.json @@ -0,0 +1,287 @@ +{ + "name": "job-applier", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "job-applier", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@jobs-scraper/browser-automation": "file:../../libs/browser-automation", + "@jobs-scraper/rabbitmq-ts": "file:../../libs/rabbitmq-ts", + "dotenv": "^17.2.3" + }, + "devDependencies": { + "@types/node": "^24.8.1", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } + }, + "../../libs/browser-automation": { + "name": "@jobs-scraper/browser-automation", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "dotenv": "^17.2.3", + "puppeteer-core": "^24.25.0" + }, + "devDependencies": { + "@types/node": "^24.8.1", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } + }, + "../../libs/rabbitmq-ts": { + "name": "@jobs-scraper/rabbitmq-ts", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "amqplib": "^0.10.9" + }, + "devDependencies": { + "@types/amqplib": "^0.10.8", + "typescript": "^5.9.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jobs-scraper/browser-automation": { + "resolved": "../../libs/browser-automation", + "link": true + }, + "node_modules/@jobs-scraper/rabbitmq-ts": { + "resolved": "../../libs/rabbitmq-ts", + "link": true + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz", + "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/services/job-applier-ts/package.json b/services/job-applier-ts/package.json new file mode 100644 index 0000000..8bf1f03 --- /dev/null +++ b/services/job-applier-ts/package.json @@ -0,0 +1,25 @@ +{ + "name": "job-applier", + "version": "1.0.0", + "description": "Job application automation service using Puppeteer", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "ts-node src/index.ts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": ["job", "automation", "puppeteer", "application"], + "author": "", + "license": "ISC", + "dependencies": { + "@jobs-scraper/browser-automation": "file:../../libs/browser-automation", + "@jobs-scraper/rabbitmq-ts": "file:../../libs/rabbitmq-ts", + "dotenv": "^17.2.3" + }, + "devDependencies": { + "@types/node": "^24.8.1", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } +} diff --git a/services/job-applier-ts/src/applier/work.ts b/services/job-applier-ts/src/applier/work.ts new file mode 100644 index 0000000..8f46596 --- /dev/null +++ b/services/job-applier-ts/src/applier/work.ts @@ -0,0 +1,347 @@ +import fs from "fs"; +import path from "path"; +import { + Browser, + randomDelay, + OpenRouterService, + FormField, + getMockFormData, + FormExtractionResult, +} from "@jobs-scraper/browser-automation"; + +/** + * Job represents a job posting + */ +export interface Job { + id: number; + title: string; + company: string; + companyLink: string; + location: string; + jobLink: string; + provider: number; + jobPostTime: string; +} + +export interface JobWithDescription { + job: Job; + jobDescription: JobDescription; +} + +export interface JobDescription { + description: string; + criteria: Record; +} + +/** + * Read CV from file + */ +export function readCV(cvPath: string = "cv.txt"): string { + const absolutePath = path.isAbsolute(cvPath) + ? cvPath + : path.join(process.cwd(), cvPath); + + return fs.readFileSync(absolutePath, "utf-8"); +} + +/** + * Read CV from file + */ +export function getCVPath(cvPath: string = "CV.pdf"): string { + return path.isAbsolute(cvPath) ? cvPath : path.join(process.cwd(), cvPath); +} + +/** + * Handle text, email, tel, url, search, password inputs + */ +async function handleTextInput( + browser: Browser, + field: FormField +): Promise { + if (field.value === "") { + return; + } + await browser.clear(field.selector); + await browser.type(field.selector, ""); + await browser.type(field.selector, field.value); +} + +/** + * Handle textarea inputs + */ +async function handleTextarea( + browser: Browser, + field: FormField +): Promise { + if (field.value === "") { + return; + } + await browser.clear(field.selector); + await browser.type(field.selector, field.value); +} + +/** + * Handle number and range inputs + */ +async function handleNumberInput( + browser: Browser, + field: FormField +): Promise { + if (field.value === "") { + return; + } + await browser.setValue(field.selector, field.value); +} + +/** + * Handle date, datetime-local, time, month, week inputs + */ +async function handleDateInput( + browser: Browser, + field: FormField +): Promise { + if (field.value === "") { + return; + } + await browser.setValue(field.selector, field.value); +} + +/** + * Handle select dropdown inputs + */ +async function handleSelect(browser: Browser, field: FormField): Promise { + if (field.value === "") { + return; + } + await browser.select(field.selector, field.value); +} + +/** + * Handle radio button inputs + */ +async function handleRadio(browser: Browser, field: FormField): Promise { + if (field.value === "") { + return; + } + await browser.setRadio(field.selector); +} + +/** + * Handle checkbox inputs + */ +async function handleCheckbox( + browser: Browser, + field: FormField +): Promise { + const checked = + field.value === "true" || field.value === "1" || field.value === "yes"; + await browser.setCheckbox(field.selector, checked); +} + +/** + * Handle file input fields + */ +async function handleFileUpload( + browser: Browser, + field: FormField +): Promise { + if (field.value === "") { + return; + } + await browser.uploadFile(field.selector, field.value); +} + +/** + * Handle color picker inputs + */ +async function handleColorInput( + browser: Browser, + field: FormField +): Promise { + if (field.value === "") { + return; + } + await browser.setValue(field.selector, field.value); +} + +/** + * Handle hidden input fields + */ +async function handleHiddenInput( + browser: Browser, + field: FormField +): Promise { + if (field.value === "") { + return; + } + await browser.setValue(field.selector, field.value); +} + +export interface WorkOptions { + useMockData?: boolean; + openRouterApiKey?: string; + openRouterModel?: string; +} + +/** + * Main work function that fills out a job application form + */ +export async function work( + browser: Browser, + job: JobWithDescription, + options: WorkOptions = {} +): Promise { + const { openRouterApiKey = "", openRouterModel = "", useMockData } = options; + + try { + await browser.init(); + + await browser.navigate(`${job.job.jobLink}apply`); + + await browser.evaluate( + ` + (() => { + const acceptBtn = document.querySelector('[data-ui="cookie-consent-accept"]'); + if (acceptBtn) { + acceptBtn.click(); + return true; + } + return false; + })() + ` + ); + + let result: FormExtractionResult; + + const formHtml = await browser.getOuterHTML("form"); + + // Read the CV + const cv = readCV(); + + const cvPDFPath = getCVPath(); + + console.log({ cvPDFPath }); + + // let resumeButtonClicked: boolean = false; + + // while (!resumeButtonClicked) { + // resumeButtonClicked = await browser.evaluate( + // ` + // (() => { + // const buttons = document.querySelectorAll('button'); + // const resumeBtn = Array.from(buttons).find(btn => + // btn.innerText.toLowerCase().includes('resume') || + // btn.innerText.toLowerCase().includes('import cv') + // ); + // if (resumeBtn) { + // resumeBtn.click(); + // return true; + // } + // return false; + // })() + // ` + // ); + // } + + // console.log({ resumeButtonClicked }); + // await browser.uploadFile("input#file-upload", cvPDFPath); + await randomDelay(5000, 7000); + // await browser.uploadFile("input#resume-upload-input", cvPDFPath); + + if (!useMockData) { + const openRouterService = new OpenRouterService({ + apiKey: openRouterApiKey, + model: openRouterModel, + }); + console.log("Calling OpenRouter service..."); + result = await openRouterService.applyForJob(formHtml, cv, ""); + + console.log("Form extraction result:", result); + } else { + result = getMockFormData(); + } + + console.log({ result }); + + if (result.fields.length === 0) { + console.log("No fields extracted, using mock data"); + } else { + for (const field of result.fields) { + await randomDelay(1000, 2000); + await processField(browser, field); + } + } + + await randomDelay(160000, 180000); + } finally { + await browser.close(); + } +} + +/** + * Process a single form field + */ +async function processField(browser: Browser, field: FormField): Promise { + if (field.selector === "") { + return; + } + + console.log("Processing field:", field); + + try { + switch (field.field_type) { + case "text": + case "email": + case "tel": + case "url": + case "search": + case "password": + await handleTextInput(browser, field); + break; + case "textarea": + await handleTextarea(browser, field); + break; + case "number": + case "range": + await handleNumberInput(browser, field); + break; + case "date": + case "datetime-local": + case "time": + case "month": + case "week": + await handleDateInput(browser, field); + break; + case "select": + await handleSelect(browser, field); + break; + case "radio": + await handleRadio(browser, field); + break; + case "checkbox": + await handleCheckbox(browser, field); + break; + case "file": + await handleFileUpload(browser, field); + break; + case "color": + await handleColorInput(browser, field); + break; + case "hidden": + await handleHiddenInput(browser, field); + break; + case "button": + case "submit": + case "reset": + // Skip buttons - they're handled separately + return; + default: + return; + } + } catch (error) { + console.error(`Error processing field ${field.field_name}:`, error); + // Continue with other fields + } + + await randomDelay(1500, 2000); +} diff --git a/services/job-applier-ts/src/index.ts b/services/job-applier-ts/src/index.ts new file mode 100644 index 0000000..3de44f7 --- /dev/null +++ b/services/job-applier-ts/src/index.ts @@ -0,0 +1,91 @@ +import * as dotenv from "dotenv"; +import { RabbitMQClient, RabbitMQConfig } from "@jobs-scraper/rabbitmq-ts"; +import { work, JobWithDescription } from "./applier/work"; +import { Browser } from "@jobs-scraper/browser-automation"; + +if (dotenv.config({ path: ".local.env" }).error) { + console.log("No .local.env file found, trying .env"); + if (dotenv.config().error) { + console.log("No .env file found, using system environment variables"); + } +} + +const JOB_APPLIER_QUEUE = "job.applier"; +const SCRAPER_EXCHANGE = "scraper_exchange"; + +let browser = new Browser({ port: 9223 }); +let rabbitMQClient: RabbitMQClient | null = null; + +function createRabbitMQConfig(): RabbitMQConfig { + return { + url: process.env.RABBITMQ_URL || "amqp://guest:guest@localhost:5672/", + queueName: JOB_APPLIER_QUEUE, + exchangeName: SCRAPER_EXCHANGE, + exchangeType: "topic", + durable: true, + }; +} + +async function processJob(job: JobWithDescription): Promise { + console.log( + `Processing job application for: ${job.job.title} at ${job.job.company}` + ); + + await work(browser, job, { + useMockData: true, + openRouterApiKey: process.env.OPENROUTER_API_KEY, + openRouterModel: process.env.OPENROUTER_MODEL, + }); + + console.log("Job application completed successfully!"); +} + +async function main() { + console.log("Starting Job Applier service..."); + + try { + await browser.init(); + + const config = createRabbitMQConfig(); + rabbitMQClient = new RabbitMQClient(config); + await rabbitMQClient.connect(); + + await rabbitMQClient.subscribe(processJob); + } catch (error) { + console.error("Error starting service:", error); + await cleanup(); + process.exit(1); + } +} + +async function cleanup(): Promise { + try { + if (rabbitMQClient) { + await rabbitMQClient.close(); + rabbitMQClient = null; + } + await browser.close(); + console.log("Cleanup completed"); + } catch (error) { + console.error("Error during cleanup:", error); + } +} + +// Handle graceful shutdown +process.on("SIGINT", async () => { + console.log("Received SIGINT signal"); + await cleanup(); + process.exit(0); +}); + +process.on("SIGTERM", async () => { + console.log("Received SIGTERM signal"); + await cleanup(); + process.exit(0); +}); + +main().catch(async (error) => { + console.error("Fatal error:", error); + await cleanup(); + process.exit(1); +}); diff --git a/services/job-applier-ts/tsconfig.json b/services/job-applier-ts/tsconfig.json new file mode 100644 index 0000000..2c37fd3 --- /dev/null +++ b/services/job-applier-ts/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "exactOptionalPropertyTypes": false, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedIndexedAccess": false + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/services/scraper-google/analyzer/get-job-description.go b/services/scraper-google/analyzer/get-job-description.go index b5cb22a..8b6a30c 100644 --- a/services/scraper-google/analyzer/get-job-description.go +++ b/services/scraper-google/analyzer/get-job-description.go @@ -1,19 +1,15 @@ package analyzer import ( - "fmt" "os" - "github.com/jobs-scraper/internal/pkg/domain" - "github.com/jobs-scraper/internal/pkg/openrouter" - // "github.com/jobs-scraper/internal/pkg/openai" - // "github.com/jobs-scraper/internal/pkg/openrouter" + "github.com/jobs-scraper/internal/domain" + "github.com/jobs-scraper/internal/openrouter" ) func GetJobDescription(htmlContent string) (*domain.JobDescription, error) { openRouterApiKey := os.Getenv("OPENROUTER_API_KEY") openRouterModel := os.Getenv("OPENROUTER_MODEL") - fmt.Println("key", openRouterApiKey) client := openrouter.NewOpenRouterService(openRouterModel, openRouterApiKey) result, err := client.CreateJobDescription(htmlContent) diff --git a/services/scraper-google/go.mod b/services/scraper-google/go.mod index 644817b..7533b06 100644 --- a/services/scraper-google/go.mod +++ b/services/scraper-google/go.mod @@ -4,7 +4,7 @@ go 1.24.0 require ( github.com/PuerkitoBio/goquery v1.10.3 - github.com/jobs-scraper/internal/pkg/infrastructure v0.0.0 + github.com/jobs-scraper/internal/infrastructure v0.0.0 github.com/joho/godotenv v1.5.1 github.com/levmv/sked v0.2.1 ) @@ -18,4 +18,4 @@ require ( golang.org/x/net v0.44.0 // indirect ) -replace github.com/jobs-scraper/internal/pkg/infrastructure => ../../internal/pkg/infrastructure +replace github.com/jobs-scraper/internal/infrastructure => ../../internal/infrastructure diff --git a/services/scraper-google/main.go b/services/scraper-google/main.go index 498d820..b3c8552 100644 --- a/services/scraper-google/main.go +++ b/services/scraper-google/main.go @@ -8,8 +8,8 @@ import ( "syscall" "time" - "github.com/jobs-scraper/internal/pkg/browser" - "github.com/jobs-scraper/internal/pkg/domain" + "github.com/jobs-scraper/internal/browser" + "github.com/jobs-scraper/internal/domain" "github.com/jobs-scraper/libs/repo" "github.com/jobs-scraper/services/scraper-google/analyzer" si "github.com/jobs-scraper/services/scraper-google/setup-infrastructure" @@ -49,7 +49,6 @@ func main() { sched := sked.New(ctx) googleLinkStream := make(chan domain.GoogleLink, 100) pages := []int{0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100} - // pages := []int{50, 60, 70, 80, 90, 100} log.Println("Starting scraper with scheduler...") diff --git a/services/scraper-google/setup-infrastructure/setup-infrastructure.go b/services/scraper-google/setup-infrastructure/setup-infrastructure.go index 269f121..608b69f 100644 --- a/services/scraper-google/setup-infrastructure/setup-infrastructure.go +++ b/services/scraper-google/setup-infrastructure/setup-infrastructure.go @@ -4,7 +4,7 @@ import ( "database/sql" "log" - "github.com/jobs-scraper/internal/pkg/infrastructure" + "github.com/jobs-scraper/internal/infrastructure" "github.com/joho/godotenv" ) diff --git a/services/scraper-google/utils/build-url.go b/services/scraper-google/utils/build-url.go index fac437c..352b695 100644 --- a/services/scraper-google/utils/build-url.go +++ b/services/scraper-google/utils/build-url.go @@ -1,15 +1,28 @@ package utils import ( + "net/url" "strconv" - "strings" + "time" ) func BuildUrl(query string, page int) string { - var url strings.Builder - url.WriteString("https://www.google.com/search?q=") - url.WriteString(query) - url.WriteString("&start=") - url.WriteString(strconv.Itoa(page)) - return url.String() + params := url.Values{} + params.Add("q", query) + params.Add("start", strconv.Itoa(page)) + + now := time.Now() + twoWeeksAgo := now.AddDate(0, 0, -14) + + startDate := twoWeeksAgo.Format("01/02/2006") + endDate := now.Format("01/02/2006") + + tbs := "cdr:1,cd_min:" + startDate + ",cd_max:" + endDate + params.Add("tbs", tbs) + + // Optional language bias + // params.Add("hl", "en") + // params.Add("lr", "lang_en") + + return "https://www.google.com/search?" + params.Encode() } diff --git a/services/scraper-google/utils/get-html-raw.go b/services/scraper-google/utils/get-html-raw.go index d05fe67..4b07c37 100644 --- a/services/scraper-google/utils/get-html-raw.go +++ b/services/scraper-google/utils/get-html-raw.go @@ -9,8 +9,8 @@ import ( "time" "github.com/PuerkitoBio/goquery" - "github.com/jobs-scraper/internal/pkg/browser" - "github.com/jobs-scraper/internal/pkg/domain" + "github.com/jobs-scraper/internal/browser" + "github.com/jobs-scraper/internal/domain" ) func GetHTMLRaw(ctx context.Context, b *browser.Browser, googleLink domain.GoogleLink) (string, string, error) { diff --git a/services/scraper-google/work.go b/services/scraper-google/work.go index bcdb3f1..750c65d 100644 --- a/services/scraper-google/work.go +++ b/services/scraper-google/work.go @@ -2,10 +2,12 @@ package main import ( "context" + "fmt" "log" + "time" - "github.com/jobs-scraper/internal/pkg/browser" - "github.com/jobs-scraper/internal/pkg/domain" + "github.com/jobs-scraper/internal/browser" + "github.com/jobs-scraper/internal/domain" "github.com/jobs-scraper/services/scraper-google/utils" ) @@ -21,9 +23,9 @@ func Work(mainCtx context.Context, b *browser.Browser) func(page int, stream cha browser.RandomDelay(1000, 3000) - // query := "site:boards.greenhouse.io (Software OR Backend OR Full-Stack) Engineer inurl:gh_jid" - // query := `site:lever.co (Software OR Backend OR Full-Stack) Engineer lang:en` - query := `site:workable.com (Software OR Backend OR Full-Stack) Engineer lang:en` + // quer := `site: ashbyhq.com (Software OR Backend OR Full-Stack OR Frontend) Engineer lang:en` + // query := `site:lever.co (Software OR Backend OR Full-Stack OR Frontend) Engineer lang:en` + query := `site:workable.com (Software OR Backend OR Full-Stack OR Frontend) Engineer lang:en` // url := utils.BuildUrl(query, page) err = tab.Navigate("https://linkedin.com") @@ -32,8 +34,9 @@ func Work(mainCtx context.Context, b *browser.Browser) func(page int, stream cha } browser.RandomDelay(2000, 2500) - + fmt.Println(utils.BuildUrl(query, page)) err = tab.Navigate(utils.BuildUrl(query, page)) + err = tab.WaitForNetworkIdle(10 * time.Second) if err != nil { log.Fatal(err) diff --git a/services/scraper-hiringcafe/index.ts b/services/scraper-hiringcafe/index.ts index 70a31ab..8192d17 100644 --- a/services/scraper-hiringcafe/index.ts +++ b/services/scraper-hiringcafe/index.ts @@ -39,10 +39,11 @@ async function cleanup() { } async function initializeBrowser(): Promise<{ browser: Browser; page: Page }> { - chrome = launchChromeWithDebugging(); + let port = 9223; + chrome = launchChromeWithDebugging(port); await waitForChromeReady(); - const response = await fetch("http://localhost:9222/json/version", { + const response = await fetch(`http://localhost:${port}/json/version`, { headers: { Origin: "", }, diff --git a/services/scraper-hiringcafe/utils/browser.ts b/services/scraper-hiringcafe/utils/browser.ts index df532d7..33d20ab 100644 --- a/services/scraper-hiringcafe/utils/browser.ts +++ b/services/scraper-hiringcafe/utils/browser.ts @@ -70,11 +70,11 @@ function getChromePath(): string { throw new Error("Could not find Chrome/Chromium installation"); } -export function launchChromeWithDebugging() { +export function launchChromeWithDebugging(port: number) { try { const chromePath = getChromePath(); const args = [ - "--remote-debugging-port=9222", + `--remote-debugging-port=${port}`, `--user-data-dir=${userDataDir}`, "--remote-allow-origins=*", "--incognito", diff --git a/services/scraper-linkedin/go.mod b/services/scraper-linkedin/go.mod index f9e8ef3..a919a2f 100644 --- a/services/scraper-linkedin/go.mod +++ b/services/scraper-linkedin/go.mod @@ -6,9 +6,9 @@ toolchain go1.24.7 require ( github.com/PuerkitoBio/goquery v1.10.3 - github.com/jobs-scraper/internal/pkg/domain v0.0.0 - github.com/jobs-scraper/internal/pkg/infrastructure v0.0.0 - github.com/jobs-scraper/internal/pkg/utils v0.0.0 + github.com/jobs-scraper/internal/domain v0.0.0 + github.com/jobs-scraper/internal/infrastructure v0.0.0 + github.com/jobs-scraper/internal/utils v0.0.0 github.com/jobs-scraper/libs/repo v0.0.0 github.com/joho/godotenv v1.5.1 ) @@ -23,10 +23,10 @@ require ( golang.org/x/net v0.44.0 // indirect ) -replace github.com/jobs-scraper/internal/pkg/domain => ../../internal/pkg/domain +replace github.com/jobs-scraper/internal/domain => ../../internal/domain -replace github.com/jobs-scraper/internal/pkg/infrastructure => ../../internal/pkg/infrastructure +replace github.com/jobs-scraper/internal/infrastructure => ../../internal/infrastructure replace github.com/jobs-scraper/libs/repo => ../../libs/repo -replace github.com/jobs-scraper/internal/pkg/utils => ../../internal/pkg/utils +replace github.com/jobs-scraper/internal/utils => ../../internal/utils diff --git a/services/scraper-linkedin/main.go b/services/scraper-linkedin/main.go index 902c10b..e073553 100644 --- a/services/scraper-linkedin/main.go +++ b/services/scraper-linkedin/main.go @@ -10,9 +10,9 @@ import ( "time" - "github.com/jobs-scraper/internal/pkg/domain" - "github.com/jobs-scraper/internal/pkg/infrastructure" - "github.com/jobs-scraper/internal/pkg/infrastructure/rabbitmq" + "github.com/jobs-scraper/internal/domain" + "github.com/jobs-scraper/internal/infrastructure" + "github.com/jobs-scraper/internal/infrastructure/rabbitmq" "github.com/jobs-scraper/libs/repo" "github.com/jobs-scraper/services/scraper/pipeline" "github.com/joho/godotenv" diff --git a/services/scraper-linkedin/pipeline/job_pipeline.go b/services/scraper-linkedin/pipeline/job_pipeline.go index bdbb779..c0b64f4 100644 --- a/services/scraper-linkedin/pipeline/job_pipeline.go +++ b/services/scraper-linkedin/pipeline/job_pipeline.go @@ -9,7 +9,7 @@ import ( "time" - "github.com/jobs-scraper/internal/pkg/domain" + "github.com/jobs-scraper/internal/domain" "github.com/jobs-scraper/libs/repo" ) diff --git a/services/scraper-linkedin/pipeline/job_pipeline_workers.go b/services/scraper-linkedin/pipeline/job_pipeline_workers.go index 5b39bc0..f71c827 100644 --- a/services/scraper-linkedin/pipeline/job_pipeline_workers.go +++ b/services/scraper-linkedin/pipeline/job_pipeline_workers.go @@ -4,7 +4,7 @@ import ( "context" "log" - "github.com/jobs-scraper/internal/pkg/domain" + "github.com/jobs-scraper/internal/domain" ) func GetJobs(context context.Context, scraperService *Scraper, searchQuery domain.SearchQuery) <-chan domain.Job { diff --git a/services/scraper-linkedin/pipeline/scraper.go b/services/scraper-linkedin/pipeline/scraper.go index 97f6b2f..e7404eb 100644 --- a/services/scraper-linkedin/pipeline/scraper.go +++ b/services/scraper-linkedin/pipeline/scraper.go @@ -12,8 +12,8 @@ import ( "time" "github.com/PuerkitoBio/goquery" - "github.com/jobs-scraper/internal/pkg/domain" - "github.com/jobs-scraper/internal/pkg/utils" + "github.com/jobs-scraper/internal/domain" + "github.com/jobs-scraper/internal/utils" ) type Config struct {