job-scraper/notify.py
Bastian Gruber e8eb9d3fcf
Initial commit: Job scraper for privacy/open-source companies
- Scrapes job listings from Greenhouse, Lever, and Ashby platforms
- Tracks 14 companies (1Password, DuckDuckGo, GitLab, etc.)
- SQLite database for change detection
- Filters by engineering job titles and location preferences
- Generates static HTML dashboard with search/filter
- Docker support for deployment to Debian server
2026-01-20 12:40:33 -04:00

178 lines
6.1 KiB
Python

from dataclasses import dataclass
from typing import Optional
import json
from db import StoredJob
from scrapers.base import Job
@dataclass
class ChangeReport:
"""Report of changes detected during a scrape."""
company_name: str
new_jobs: list[Job]
removed_jobs: list[StoredJob]
total_active: int
class Notifier:
"""Handles notifications for job changes."""
def __init__(self, config: dict):
self.config = config
def notify(self, reports: list[ChangeReport]):
"""Send notifications for all changes."""
# Filter to only reports with changes
reports_with_changes = [r for r in reports if r.new_jobs or r.removed_jobs]
if not reports_with_changes:
print("\n✓ No changes detected across all companies.")
return
# Console output (always)
self._notify_console(reports_with_changes)
# Email (if configured)
email_config = self.config.get("email")
if email_config:
self._notify_email(reports_with_changes, email_config)
# Slack (if configured)
slack_config = self.config.get("slack")
if slack_config:
self._notify_slack(reports_with_changes, slack_config)
def _notify_console(self, reports: list[ChangeReport]):
"""Print changes to console."""
print("\n" + "=" * 60)
print("JOB CHANGES DETECTED")
print("=" * 60)
total_new = sum(len(r.new_jobs) for r in reports)
total_removed = sum(len(r.removed_jobs) for r in reports)
print(f"\nSummary: {total_new} new jobs, {total_removed} removed jobs\n")
for report in reports:
print(f"\n📌 {report.company_name} ({report.total_active} active jobs)")
print("-" * 40)
if report.new_jobs:
print(f"\n 🆕 NEW JOBS ({len(report.new_jobs)}):")
for job in report.new_jobs:
location_str = f" [{job.location}]" if job.location else ""
remote_str = f" 🏠" if job.remote_type == "remote" else ""
print(f"{job.title}{location_str}{remote_str}")
print(f" {job.url}")
if report.removed_jobs:
print(f"\n ❌ REMOVED JOBS ({len(report.removed_jobs)}):")
for job in report.removed_jobs:
print(f"{job.title}")
print("\n" + "=" * 60)
def _notify_email(self, reports: list[ChangeReport], config: dict):
"""Send email notification."""
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# Build email body
body = self._build_html_report(reports)
msg = MIMEMultipart("alternative")
msg["Subject"] = f"Job Alert: {sum(len(r.new_jobs) for r in reports)} new positions"
msg["From"] = config["from_addr"]
msg["To"] = config["to_addr"]
msg.attach(MIMEText(body, "html"))
try:
with smtplib.SMTP(config["smtp_host"], config["smtp_port"]) as server:
server.starttls()
server.login(config["username"], config["password"])
server.send_message(msg)
print("✓ Email notification sent")
except Exception as e:
print(f"✗ Failed to send email: {e}")
def _notify_slack(self, reports: list[ChangeReport], config: dict):
"""Send Slack notification."""
import httpx
blocks = []
# Header
total_new = sum(len(r.new_jobs) for r in reports)
blocks.append({
"type": "header",
"text": {"type": "plain_text", "text": f"🔔 {total_new} New Job Openings"}
})
for report in reports:
if report.new_jobs:
blocks.append({"type": "divider"})
blocks.append({
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*{report.company_name}* ({len(report.new_jobs)} new)"
}
})
for job in report.new_jobs[:5]: # Limit to 5 per company
location = f"{job.location}" if job.location else ""
blocks.append({
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"<{job.url}|{job.title}>{location}"
}
})
payload = {"blocks": blocks}
try:
response = httpx.post(config["webhook_url"], json=payload)
response.raise_for_status()
print("✓ Slack notification sent")
except Exception as e:
print(f"✗ Failed to send Slack notification: {e}")
def _build_html_report(self, reports: list[ChangeReport]) -> str:
"""Build HTML email body."""
total_new = sum(len(r.new_jobs) for r in reports)
html = f"""
<html>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #333;">🔔 {total_new} New Job Openings</h1>
"""
for report in reports:
if report.new_jobs:
html += f"""
<h2 style="color: #666; border-bottom: 1px solid #ddd; padding-bottom: 5px;">
{report.company_name}
</h2>
<ul>
"""
for job in report.new_jobs:
location = f" <span style='color: #888;'>({job.location})</span>" if job.location else ""
html += f"""
<li style="margin: 10px 0;">
<a href="{job.url}" style="color: #0066cc; text-decoration: none;">
{job.title}
</a>
{location}
</li>
"""
html += "</ul>"
html += """
</body>
</html>
"""
return html