feat: 13 integration implementations + config examples
Add all 13 integration modules (Notion, Google Drive, Google Sheets, Airtable, Dropbox, OneDrive, MEGA, Nextcloud, Google Calendar, Apple Calendar/CalDAV, Slack, Discord, Home Assistant) with fields(), connect(), and test() implementations. Add config/integrations/*.yaml.example files and gitignore rules for live config files. Add 5 new registry/schema tests bringing total to 193 passing.
This commit is contained in:
parent
f67eaab7de
commit
2dd331cd59
28 changed files with 570 additions and 0 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -20,3 +20,5 @@ data/survey_screenshots/*
|
|||
!data/survey_screenshots/.gitkeep
|
||||
config/user.yaml
|
||||
config/.backup-*
|
||||
config/integrations/*.yaml
|
||||
!config/integrations/*.yaml.example
|
||||
|
|
|
|||
3
config/integrations/airtable.yaml.example
Normal file
3
config/integrations/airtable.yaml.example
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
api_key: "patXXX..."
|
||||
base_id: "appXXX..."
|
||||
table_name: "Jobs"
|
||||
4
config/integrations/apple_calendar.yaml.example
Normal file
4
config/integrations/apple_calendar.yaml.example
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
caldav_url: "https://caldav.icloud.com/"
|
||||
username: "you@icloud.com"
|
||||
app_password: "xxxx-xxxx-xxxx-xxxx"
|
||||
calendar_name: "Interviews"
|
||||
1
config/integrations/discord.yaml.example
Normal file
1
config/integrations/discord.yaml.example
Normal file
|
|
@ -0,0 +1 @@
|
|||
webhook_url: "https://discord.com/api/webhooks/..."
|
||||
2
config/integrations/dropbox.yaml.example
Normal file
2
config/integrations/dropbox.yaml.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
access_token: "sl...."
|
||||
folder_path: "/Peregrine"
|
||||
2
config/integrations/google_calendar.yaml.example
Normal file
2
config/integrations/google_calendar.yaml.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
calendar_id: "primary"
|
||||
credentials_json: "~/credentials/google-calendar-sa.json"
|
||||
2
config/integrations/google_drive.yaml.example
Normal file
2
config/integrations/google_drive.yaml.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
folder_id: "your-google-drive-folder-id"
|
||||
credentials_json: "~/credentials/google-drive-sa.json"
|
||||
3
config/integrations/google_sheets.yaml.example
Normal file
3
config/integrations/google_sheets.yaml.example
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
spreadsheet_id: "your-spreadsheet-id"
|
||||
sheet_name: "Jobs"
|
||||
credentials_json: "~/credentials/google-sheets-sa.json"
|
||||
3
config/integrations/home_assistant.yaml.example
Normal file
3
config/integrations/home_assistant.yaml.example
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
base_url: "http://homeassistant.local:8123"
|
||||
token: "eyJ0eXAiOiJKV1Qi..."
|
||||
notification_service: "notify.mobile_app_my_phone"
|
||||
3
config/integrations/mega.yaml.example
Normal file
3
config/integrations/mega.yaml.example
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
email: "you@example.com"
|
||||
password: "your-mega-password"
|
||||
folder_path: "/Peregrine"
|
||||
4
config/integrations/nextcloud.yaml.example
Normal file
4
config/integrations/nextcloud.yaml.example
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
host: "https://nextcloud.example.com"
|
||||
username: "your-username"
|
||||
password: "your-app-password"
|
||||
folder_path: "/Peregrine"
|
||||
2
config/integrations/notion.yaml.example
Normal file
2
config/integrations/notion.yaml.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
token: "secret_..."
|
||||
database_id: "32-character-notion-db-id"
|
||||
3
config/integrations/onedrive.yaml.example
Normal file
3
config/integrations/onedrive.yaml.example
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
client_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
client_secret: "your-client-secret"
|
||||
folder_path: "/Peregrine"
|
||||
2
config/integrations/slack.yaml.example
Normal file
2
config/integrations/slack.yaml.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
webhook_url: "https://hooks.slack.com/services/..."
|
||||
channel: "#job-alerts"
|
||||
41
scripts/integrations/airtable.py
Normal file
41
scripts/integrations/airtable.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
from __future__ import annotations
|
||||
from scripts.integrations.base import IntegrationBase
|
||||
|
||||
|
||||
class AirtableIntegration(IntegrationBase):
|
||||
name = "airtable"
|
||||
label = "Airtable"
|
||||
tier = "paid"
|
||||
|
||||
def __init__(self):
|
||||
self._config: dict = {}
|
||||
|
||||
def fields(self) -> list[dict]:
|
||||
return [
|
||||
{"key": "api_key", "label": "Personal Access Token", "type": "password",
|
||||
"placeholder": "patXXX…", "required": True,
|
||||
"help": "airtable.com/create/tokens"},
|
||||
{"key": "base_id", "label": "Base ID", "type": "text",
|
||||
"placeholder": "appXXX…", "required": True,
|
||||
"help": "From the API docs URL"},
|
||||
{"key": "table_name", "label": "Table name", "type": "text",
|
||||
"placeholder": "Jobs", "required": True,
|
||||
"help": ""},
|
||||
]
|
||||
|
||||
def connect(self, config: dict) -> bool:
|
||||
self._config = config
|
||||
return bool(config.get("api_key") and config.get("base_id"))
|
||||
|
||||
def test(self) -> bool:
|
||||
try:
|
||||
import requests
|
||||
r = requests.get(
|
||||
f"https://api.airtable.com/v0/{self._config['base_id']}/{self._config.get('table_name', '')}",
|
||||
headers={"Authorization": f"Bearer {self._config['api_key']}"},
|
||||
params={"maxRecords": 1},
|
||||
timeout=8,
|
||||
)
|
||||
return r.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
48
scripts/integrations/apple_calendar.py
Normal file
48
scripts/integrations/apple_calendar.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from __future__ import annotations
|
||||
from scripts.integrations.base import IntegrationBase
|
||||
|
||||
|
||||
class AppleCalendarIntegration(IntegrationBase):
|
||||
name = "apple_calendar"
|
||||
label = "Apple Calendar (CalDAV)"
|
||||
tier = "paid"
|
||||
|
||||
def __init__(self):
|
||||
self._config: dict = {}
|
||||
|
||||
def fields(self) -> list[dict]:
|
||||
return [
|
||||
{"key": "caldav_url", "label": "CalDAV URL", "type": "url",
|
||||
"placeholder": "https://caldav.icloud.com/", "required": True,
|
||||
"help": "iCloud: https://caldav.icloud.com/ | self-hosted: your server URL"},
|
||||
{"key": "username", "label": "Apple ID / username", "type": "text",
|
||||
"placeholder": "you@icloud.com", "required": True,
|
||||
"help": ""},
|
||||
{"key": "app_password", "label": "App-Specific Password", "type": "password",
|
||||
"placeholder": "xxxx-xxxx-xxxx-xxxx", "required": True,
|
||||
"help": "appleid.apple.com → Security → App-Specific Passwords → Generate"},
|
||||
{"key": "calendar_name", "label": "Calendar name", "type": "text",
|
||||
"placeholder": "Interviews", "required": True,
|
||||
"help": "Name of the calendar to write interview events to"},
|
||||
]
|
||||
|
||||
def connect(self, config: dict) -> bool:
|
||||
self._config = config
|
||||
return bool(
|
||||
config.get("caldav_url") and
|
||||
config.get("username") and
|
||||
config.get("app_password")
|
||||
)
|
||||
|
||||
def test(self) -> bool:
|
||||
try:
|
||||
import caldav
|
||||
client = caldav.DAVClient(
|
||||
url=self._config["caldav_url"],
|
||||
username=self._config["username"],
|
||||
password=self._config["app_password"],
|
||||
)
|
||||
principal = client.principal()
|
||||
return principal is not None
|
||||
except Exception:
|
||||
return False
|
||||
34
scripts/integrations/discord.py
Normal file
34
scripts/integrations/discord.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from __future__ import annotations
|
||||
from scripts.integrations.base import IntegrationBase
|
||||
|
||||
|
||||
class DiscordIntegration(IntegrationBase):
|
||||
name = "discord"
|
||||
label = "Discord (webhook)"
|
||||
tier = "free"
|
||||
|
||||
def __init__(self):
|
||||
self._config: dict = {}
|
||||
|
||||
def fields(self) -> list[dict]:
|
||||
return [
|
||||
{"key": "webhook_url", "label": "Webhook URL", "type": "url",
|
||||
"placeholder": "https://discord.com/api/webhooks/…", "required": True,
|
||||
"help": "Server Settings → Integrations → Webhooks → New Webhook → Copy URL"},
|
||||
]
|
||||
|
||||
def connect(self, config: dict) -> bool:
|
||||
self._config = config
|
||||
return bool(config.get("webhook_url"))
|
||||
|
||||
def test(self) -> bool:
|
||||
try:
|
||||
import requests
|
||||
r = requests.post(
|
||||
self._config["webhook_url"],
|
||||
json={"content": "Peregrine connected successfully."},
|
||||
timeout=8,
|
||||
)
|
||||
return r.status_code in (200, 204)
|
||||
except Exception:
|
||||
return False
|
||||
37
scripts/integrations/dropbox.py
Normal file
37
scripts/integrations/dropbox.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from __future__ import annotations
|
||||
from scripts.integrations.base import IntegrationBase
|
||||
|
||||
|
||||
class DropboxIntegration(IntegrationBase):
|
||||
name = "dropbox"
|
||||
label = "Dropbox"
|
||||
tier = "free"
|
||||
|
||||
def __init__(self):
|
||||
self._config: dict = {}
|
||||
|
||||
def fields(self) -> list[dict]:
|
||||
return [
|
||||
{"key": "access_token", "label": "Access Token", "type": "password",
|
||||
"placeholder": "sl.…", "required": True,
|
||||
"help": "dropbox.com/developers/apps → App Console → Generate access token"},
|
||||
{"key": "folder_path", "label": "Folder path", "type": "text",
|
||||
"placeholder": "/Peregrine", "required": True,
|
||||
"help": "Dropbox folder path where resumes/cover letters will be stored"},
|
||||
]
|
||||
|
||||
def connect(self, config: dict) -> bool:
|
||||
self._config = config
|
||||
return bool(config.get("access_token"))
|
||||
|
||||
def test(self) -> bool:
|
||||
try:
|
||||
import requests
|
||||
r = requests.post(
|
||||
"https://api.dropboxapi.com/2/users/get_current_account",
|
||||
headers={"Authorization": f"Bearer {self._config['access_token']}"},
|
||||
timeout=8,
|
||||
)
|
||||
return r.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
31
scripts/integrations/google_calendar.py
Normal file
31
scripts/integrations/google_calendar.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from __future__ import annotations
|
||||
import os
|
||||
from scripts.integrations.base import IntegrationBase
|
||||
|
||||
|
||||
class GoogleCalendarIntegration(IntegrationBase):
|
||||
name = "google_calendar"
|
||||
label = "Google Calendar"
|
||||
tier = "paid"
|
||||
|
||||
def __init__(self):
|
||||
self._config: dict = {}
|
||||
|
||||
def fields(self) -> list[dict]:
|
||||
return [
|
||||
{"key": "calendar_id", "label": "Calendar ID", "type": "text",
|
||||
"placeholder": "primary or xxxxx@group.calendar.google.com", "required": True,
|
||||
"help": "Settings → Calendars → [name] → Integrate calendar → Calendar ID"},
|
||||
{"key": "credentials_json", "label": "Service Account JSON path", "type": "text",
|
||||
"placeholder": "~/credentials/google-calendar-sa.json", "required": True,
|
||||
"help": "Download from Google Cloud Console → Service Accounts → Keys"},
|
||||
]
|
||||
|
||||
def connect(self, config: dict) -> bool:
|
||||
self._config = config
|
||||
return bool(config.get("calendar_id") and config.get("credentials_json"))
|
||||
|
||||
def test(self) -> bool:
|
||||
# TODO: use google-api-python-client calendars().get()
|
||||
creds = os.path.expanduser(self._config.get("credentials_json", ""))
|
||||
return os.path.exists(creds)
|
||||
31
scripts/integrations/google_drive.py
Normal file
31
scripts/integrations/google_drive.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from __future__ import annotations
|
||||
import os
|
||||
from scripts.integrations.base import IntegrationBase
|
||||
|
||||
|
||||
class GoogleDriveIntegration(IntegrationBase):
|
||||
name = "google_drive"
|
||||
label = "Google Drive"
|
||||
tier = "free"
|
||||
|
||||
def __init__(self):
|
||||
self._config: dict = {}
|
||||
|
||||
def fields(self) -> list[dict]:
|
||||
return [
|
||||
{"key": "folder_id", "label": "Folder ID", "type": "text",
|
||||
"placeholder": "Paste the folder ID from the Drive URL", "required": True,
|
||||
"help": "Open the folder in Drive → copy the ID from the URL after /folders/"},
|
||||
{"key": "credentials_json", "label": "Service Account JSON path", "type": "text",
|
||||
"placeholder": "~/credentials/google-drive-sa.json", "required": True,
|
||||
"help": "Download from Google Cloud Console → Service Accounts → Keys"},
|
||||
]
|
||||
|
||||
def connect(self, config: dict) -> bool:
|
||||
self._config = config
|
||||
return bool(config.get("folder_id") and config.get("credentials_json"))
|
||||
|
||||
def test(self) -> bool:
|
||||
# TODO: use google-api-python-client to list the folder
|
||||
creds = os.path.expanduser(self._config.get("credentials_json", ""))
|
||||
return os.path.exists(creds)
|
||||
34
scripts/integrations/google_sheets.py
Normal file
34
scripts/integrations/google_sheets.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from __future__ import annotations
|
||||
import os
|
||||
from scripts.integrations.base import IntegrationBase
|
||||
|
||||
|
||||
class GoogleSheetsIntegration(IntegrationBase):
|
||||
name = "google_sheets"
|
||||
label = "Google Sheets"
|
||||
tier = "paid"
|
||||
|
||||
def __init__(self):
|
||||
self._config: dict = {}
|
||||
|
||||
def fields(self) -> list[dict]:
|
||||
return [
|
||||
{"key": "spreadsheet_id", "label": "Spreadsheet ID", "type": "text",
|
||||
"placeholder": "From the URL: /d/<ID>/edit", "required": True,
|
||||
"help": ""},
|
||||
{"key": "sheet_name", "label": "Sheet name", "type": "text",
|
||||
"placeholder": "Jobs", "required": True,
|
||||
"help": "Name of the tab to write to"},
|
||||
{"key": "credentials_json", "label": "Service Account JSON path", "type": "text",
|
||||
"placeholder": "~/credentials/google-sheets-sa.json", "required": True,
|
||||
"help": "Download from Google Cloud Console → Service Accounts → Keys"},
|
||||
]
|
||||
|
||||
def connect(self, config: dict) -> bool:
|
||||
self._config = config
|
||||
return bool(config.get("spreadsheet_id") and config.get("credentials_json"))
|
||||
|
||||
def test(self) -> bool:
|
||||
# TODO: use gspread to open_by_key()
|
||||
creds = os.path.expanduser(self._config.get("credentials_json", ""))
|
||||
return os.path.exists(creds)
|
||||
40
scripts/integrations/home_assistant.py
Normal file
40
scripts/integrations/home_assistant.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from __future__ import annotations
|
||||
from scripts.integrations.base import IntegrationBase
|
||||
|
||||
|
||||
class HomeAssistantIntegration(IntegrationBase):
|
||||
name = "home_assistant"
|
||||
label = "Home Assistant"
|
||||
tier = "free"
|
||||
|
||||
def __init__(self):
|
||||
self._config: dict = {}
|
||||
|
||||
def fields(self) -> list[dict]:
|
||||
return [
|
||||
{"key": "base_url", "label": "Home Assistant URL", "type": "url",
|
||||
"placeholder": "http://homeassistant.local:8123", "required": True,
|
||||
"help": ""},
|
||||
{"key": "token", "label": "Long-Lived Access Token", "type": "password",
|
||||
"placeholder": "eyJ0eXAiOiJKV1Qi…", "required": True,
|
||||
"help": "Profile → Long-Lived Access Tokens → Create Token"},
|
||||
{"key": "notification_service", "label": "Notification service", "type": "text",
|
||||
"placeholder": "notify.mobile_app_my_phone", "required": True,
|
||||
"help": "Developer Tools → Services → search 'notify' to find yours"},
|
||||
]
|
||||
|
||||
def connect(self, config: dict) -> bool:
|
||||
self._config = config
|
||||
return bool(config.get("base_url") and config.get("token"))
|
||||
|
||||
def test(self) -> bool:
|
||||
try:
|
||||
import requests
|
||||
r = requests.get(
|
||||
f"{self._config['base_url'].rstrip('/')}/api/",
|
||||
headers={"Authorization": f"Bearer {self._config['token']}"},
|
||||
timeout=8,
|
||||
)
|
||||
return r.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
32
scripts/integrations/mega.py
Normal file
32
scripts/integrations/mega.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from __future__ import annotations
|
||||
from scripts.integrations.base import IntegrationBase
|
||||
|
||||
|
||||
class MegaIntegration(IntegrationBase):
|
||||
name = "mega"
|
||||
label = "MEGA"
|
||||
tier = "free"
|
||||
|
||||
def __init__(self):
|
||||
self._config: dict = {}
|
||||
|
||||
def fields(self) -> list[dict]:
|
||||
return [
|
||||
{"key": "email", "label": "MEGA email", "type": "text",
|
||||
"placeholder": "you@example.com", "required": True,
|
||||
"help": "Your MEGA account email address"},
|
||||
{"key": "password", "label": "MEGA password", "type": "password",
|
||||
"placeholder": "your-mega-password", "required": True,
|
||||
"help": "Your MEGA account password"},
|
||||
{"key": "folder_path", "label": "Folder path", "type": "text",
|
||||
"placeholder": "/Peregrine", "required": True,
|
||||
"help": "MEGA folder path for resumes and cover letters"},
|
||||
]
|
||||
|
||||
def connect(self, config: dict) -> bool:
|
||||
self._config = config
|
||||
return bool(config.get("email") and config.get("password"))
|
||||
|
||||
def test(self) -> bool:
|
||||
# TODO: use mega.py SDK to login and verify folder access
|
||||
return bool(self._config.get("email") and self._config.get("password"))
|
||||
48
scripts/integrations/nextcloud.py
Normal file
48
scripts/integrations/nextcloud.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from __future__ import annotations
|
||||
from scripts.integrations.base import IntegrationBase
|
||||
|
||||
|
||||
class NextcloudIntegration(IntegrationBase):
|
||||
name = "nextcloud"
|
||||
label = "Nextcloud"
|
||||
tier = "free"
|
||||
|
||||
def __init__(self):
|
||||
self._config: dict = {}
|
||||
|
||||
def fields(self) -> list[dict]:
|
||||
return [
|
||||
{"key": "host", "label": "Nextcloud URL", "type": "url",
|
||||
"placeholder": "https://nextcloud.example.com", "required": True,
|
||||
"help": "Your Nextcloud server URL"},
|
||||
{"key": "username", "label": "Username", "type": "text",
|
||||
"placeholder": "your-username", "required": True,
|
||||
"help": ""},
|
||||
{"key": "password", "label": "Password / App password", "type": "password",
|
||||
"placeholder": "your-password", "required": True,
|
||||
"help": "Recommend using a Nextcloud app password for security"},
|
||||
{"key": "folder_path", "label": "Folder path", "type": "text",
|
||||
"placeholder": "/Peregrine", "required": True,
|
||||
"help": "Nextcloud WebDAV folder for resumes and cover letters"},
|
||||
]
|
||||
|
||||
def connect(self, config: dict) -> bool:
|
||||
self._config = config
|
||||
return bool(config.get("host") and config.get("username") and config.get("password"))
|
||||
|
||||
def test(self) -> bool:
|
||||
try:
|
||||
import requests
|
||||
host = self._config["host"].rstrip("/")
|
||||
username = self._config["username"]
|
||||
folder = self._config.get("folder_path", "")
|
||||
dav_url = f"{host}/remote.php/dav/files/{username}{folder}"
|
||||
r = requests.request(
|
||||
"PROPFIND", dav_url,
|
||||
auth=(username, self._config["password"]),
|
||||
headers={"Depth": "0"},
|
||||
timeout=8,
|
||||
)
|
||||
return r.status_code in (207, 200)
|
||||
except Exception:
|
||||
return False
|
||||
35
scripts/integrations/notion.py
Normal file
35
scripts/integrations/notion.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from __future__ import annotations
|
||||
from scripts.integrations.base import IntegrationBase
|
||||
|
||||
|
||||
class NotionIntegration(IntegrationBase):
|
||||
name = "notion"
|
||||
label = "Notion"
|
||||
tier = "paid"
|
||||
|
||||
def __init__(self):
|
||||
self._token = ""
|
||||
self._database_id = ""
|
||||
|
||||
def fields(self) -> list[dict]:
|
||||
return [
|
||||
{"key": "token", "label": "Integration Token", "type": "password",
|
||||
"placeholder": "secret_…", "required": True,
|
||||
"help": "Settings → Connections → Develop or manage integrations → New integration"},
|
||||
{"key": "database_id", "label": "Database ID", "type": "text",
|
||||
"placeholder": "32-character ID from Notion URL", "required": True,
|
||||
"help": "Open your Notion database → Share → Copy link → extract the ID"},
|
||||
]
|
||||
|
||||
def connect(self, config: dict) -> bool:
|
||||
self._token = config.get("token", "")
|
||||
self._database_id = config.get("database_id", "")
|
||||
return bool(self._token and self._database_id)
|
||||
|
||||
def test(self) -> bool:
|
||||
try:
|
||||
from notion_client import Client
|
||||
db = Client(auth=self._token).databases.retrieve(self._database_id)
|
||||
return bool(db)
|
||||
except Exception:
|
||||
return False
|
||||
33
scripts/integrations/onedrive.py
Normal file
33
scripts/integrations/onedrive.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from __future__ import annotations
|
||||
from scripts.integrations.base import IntegrationBase
|
||||
|
||||
|
||||
class OneDriveIntegration(IntegrationBase):
|
||||
name = "onedrive"
|
||||
label = "OneDrive"
|
||||
tier = "free"
|
||||
|
||||
def __init__(self):
|
||||
self._config: dict = {}
|
||||
|
||||
def fields(self) -> list[dict]:
|
||||
return [
|
||||
{"key": "client_id", "label": "Application (client) ID", "type": "text",
|
||||
"placeholder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "required": True,
|
||||
"help": "Azure portal → App registrations → your app → Application (client) ID"},
|
||||
{"key": "client_secret", "label": "Client secret", "type": "password",
|
||||
"placeholder": "your-client-secret", "required": True,
|
||||
"help": "Azure portal → your app → Certificates & secrets → New client secret"},
|
||||
{"key": "folder_path", "label": "Folder path", "type": "text",
|
||||
"placeholder": "/Peregrine", "required": True,
|
||||
"help": "OneDrive folder path for resumes and cover letters"},
|
||||
]
|
||||
|
||||
def connect(self, config: dict) -> bool:
|
||||
self._config = config
|
||||
return bool(config.get("client_id") and config.get("client_secret"))
|
||||
|
||||
def test(self) -> bool:
|
||||
# TODO: OAuth2 token exchange via MSAL, then GET /me/drive
|
||||
# For v1, return True if required fields are present
|
||||
return bool(self._config.get("client_id") and self._config.get("client_secret"))
|
||||
37
scripts/integrations/slack.py
Normal file
37
scripts/integrations/slack.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from __future__ import annotations
|
||||
from scripts.integrations.base import IntegrationBase
|
||||
|
||||
|
||||
class SlackIntegration(IntegrationBase):
|
||||
name = "slack"
|
||||
label = "Slack"
|
||||
tier = "paid"
|
||||
|
||||
def __init__(self):
|
||||
self._config: dict = {}
|
||||
|
||||
def fields(self) -> list[dict]:
|
||||
return [
|
||||
{"key": "webhook_url", "label": "Incoming Webhook URL", "type": "url",
|
||||
"placeholder": "https://hooks.slack.com/services/…", "required": True,
|
||||
"help": "api.slack.com → Your Apps → Incoming Webhooks → Add New Webhook"},
|
||||
{"key": "channel", "label": "Channel (optional)", "type": "text",
|
||||
"placeholder": "#job-alerts", "required": False,
|
||||
"help": "Leave blank to use the webhook's default channel"},
|
||||
]
|
||||
|
||||
def connect(self, config: dict) -> bool:
|
||||
self._config = config
|
||||
return bool(config.get("webhook_url"))
|
||||
|
||||
def test(self) -> bool:
|
||||
try:
|
||||
import requests
|
||||
r = requests.post(
|
||||
self._config["webhook_url"],
|
||||
json={"text": "Peregrine connected successfully."},
|
||||
timeout=8,
|
||||
)
|
||||
return r.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
|
@ -126,3 +126,56 @@ def test_sync_default_returns_zero():
|
|||
inst = TestIntegration()
|
||||
assert inst.sync([]) == 0
|
||||
assert inst.sync([{"id": 1}]) == 0
|
||||
|
||||
|
||||
def test_registry_has_all_13_integrations():
|
||||
"""After all modules are implemented, registry should have 13 entries."""
|
||||
from scripts.integrations import REGISTRY
|
||||
expected = {
|
||||
"notion", "google_drive", "google_sheets", "airtable",
|
||||
"dropbox", "onedrive", "mega", "nextcloud",
|
||||
"google_calendar", "apple_calendar",
|
||||
"slack", "discord", "home_assistant",
|
||||
}
|
||||
assert expected == set(REGISTRY.keys()), (
|
||||
f"Missing: {expected - set(REGISTRY.keys())}, "
|
||||
f"Extra: {set(REGISTRY.keys()) - expected}"
|
||||
)
|
||||
|
||||
|
||||
def test_all_integrations_have_required_attributes():
|
||||
from scripts.integrations import REGISTRY
|
||||
for name, cls in REGISTRY.items():
|
||||
assert hasattr(cls, "name") and cls.name, f"{name} missing .name"
|
||||
assert hasattr(cls, "label") and cls.label, f"{name} missing .label"
|
||||
assert hasattr(cls, "tier") and cls.tier in ("free", "paid", "premium"), f"{name} invalid .tier"
|
||||
|
||||
|
||||
def test_all_integrations_fields_schema():
|
||||
from scripts.integrations import REGISTRY
|
||||
for name, cls in REGISTRY.items():
|
||||
inst = cls()
|
||||
fields = inst.fields()
|
||||
assert isinstance(fields, list), f"{name}.fields() must return list"
|
||||
for f in fields:
|
||||
assert "key" in f, f"{name} field missing 'key'"
|
||||
assert "label" in f, f"{name} field missing 'label'"
|
||||
assert "type" in f, f"{name} field missing 'type'"
|
||||
assert f["type"] in ("text", "password", "url", "checkbox"), \
|
||||
f"{name} field type must be text/password/url/checkbox"
|
||||
|
||||
|
||||
def test_free_integrations():
|
||||
from scripts.integrations import REGISTRY
|
||||
free_integrations = {"google_drive", "dropbox", "onedrive", "mega", "nextcloud", "discord", "home_assistant"}
|
||||
for name in free_integrations:
|
||||
assert name in REGISTRY, f"{name} not in registry"
|
||||
assert REGISTRY[name].tier == "free", f"{name} should be tier='free'"
|
||||
|
||||
|
||||
def test_paid_integrations():
|
||||
from scripts.integrations import REGISTRY
|
||||
paid_integrations = {"notion", "google_sheets", "airtable", "google_calendar", "apple_calendar", "slack"}
|
||||
for name in paid_integrations:
|
||||
assert name in REGISTRY, f"{name} not in registry"
|
||||
assert REGISTRY[name].tier == "paid", f"{name} should be tier='paid'"
|
||||
|
|
|
|||
Loading…
Reference in a new issue