146 lines
5.4 KiB
Python
146 lines
5.4 KiB
Python
from datetime import date, time
|
|
import os
|
|
|
|
import httpx
|
|
from dotenv import load_dotenv
|
|
from fastapi import HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from pydantic import BaseModel, model_validator
|
|
from job.job_post.models import JobPosts
|
|
from job.job_post.plugins import (
|
|
BufferError,
|
|
create_buffer_post,
|
|
list_buffer_channels,
|
|
local_status,
|
|
normalize_platform,
|
|
parse_buffer_datetime,
|
|
render_job_post,
|
|
resolve_channel,
|
|
)
|
|
from job.job_post.serializers import serialize_job_post
|
|
|
|
load_dotenv()
|
|
|
|
|
|
class JobPostCreate(BaseModel):
|
|
title: str
|
|
experience_min: int | None = None
|
|
experience_max: int | None = None
|
|
requirements: list[str] = []
|
|
optional_skills: list[str] = []
|
|
salary: str = "Anonymous"
|
|
location: str | None = None
|
|
employment_type: str | None = None
|
|
platform: str = "linkedin"
|
|
description: str | None = None
|
|
platform: str | None = None
|
|
channel_id: str | None = None
|
|
mode: str = "addToQueue"
|
|
scheduler_time: time | None = time(0, 0, 0)
|
|
scheduler_date: date | None = None
|
|
due_at: str | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def validate_mode_and_due_at(self):
|
|
allowed = {"addToQueue", "shareNow", "customScheduled"}
|
|
if self.mode not in allowed:
|
|
raise ValueError(f"mode must be one of {sorted(allowed)}")
|
|
if self.mode == "customScheduled" and not self.due_at:
|
|
raise ValueError("due_at is required when mode is customScheduled")
|
|
return self
|
|
|
|
class JobPost:
|
|
def __init__(self,session:AsyncSession):
|
|
self.session=session
|
|
self.buffer_api=os.getenv("BUFFER_API")
|
|
self.channel_id=os.getenv("BUFFER_CHANNEL_ID")
|
|
|
|
async def _resolve_target(self,payload):
|
|
"""Pick the Buffer channel to post to, and the service it belongs to.
|
|
|
|
Precedence: an explicit channel_id, then the requested platform, then the
|
|
configured default channel. Returns (channel_id, service) where service is
|
|
None if we did not have to look the channel up.
|
|
"""
|
|
if payload.get("channel_id"):
|
|
return payload["channel_id"],None
|
|
if payload.get("platform"):
|
|
channel=await resolve_channel(payload["platform"])
|
|
return channel["id"],channel.get("service")
|
|
if self.channel_id:
|
|
return self.channel_id,None
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Provide channel_id or platform, or configure BUFFER_CHANNEL_ID",
|
|
)
|
|
|
|
async def post_job(self,payload,current_user):
|
|
try:
|
|
channel_id,service=await self._resolve_target(payload)
|
|
except (httpx.HTTPError,BufferError,RuntimeError) as e:
|
|
raise HTTPException(status_code=502,detail=f"Failed to resolve Buffer channel: {e}") from e
|
|
|
|
text=render_job_post(payload)
|
|
fields={
|
|
"title":payload.get("title"),
|
|
"employment_type":payload.get("employment_type"),
|
|
"location":payload.get("location"),
|
|
"experience_min":payload.get("experience_min"),
|
|
"experience_max":payload.get("experience_max"),
|
|
"requirements":list(payload.get("requirements") or []),
|
|
"optional_skills":list(payload.get("optional_skills") or []),
|
|
"salary":payload.get("salary") or "Anonymous",
|
|
"description":payload.get("description"),
|
|
"post_text":text,
|
|
"channel_id":channel_id,
|
|
"status":"draft",
|
|
"created_by":current_user["id"],
|
|
}
|
|
# Only set platform when it is actually known: passing None would override the
|
|
# column default and break the NOT NULL constraint. Buffer's channelService
|
|
# replaces this with the authoritative value once the post is created.
|
|
known_platform=service or normalize_platform(payload.get("platform"))
|
|
if known_platform:
|
|
fields["platform"]=known_platform
|
|
row=await JobPosts.insert_job_post(self.session,fields)
|
|
|
|
try:
|
|
post=await create_buffer_post(
|
|
text,
|
|
channel_id,
|
|
mode=payload.get("mode") or "addToQueue",
|
|
due_at=payload.get("due_at"),
|
|
)
|
|
except (httpx.HTTPError,BufferError,RuntimeError) as e:
|
|
|
|
await JobPosts.mark_failed(self.session,str(row.id),str(e))
|
|
raise HTTPException(status_code=502,detail=f"Failed to publish job post to Buffer: {e}") from e
|
|
|
|
saved=await JobPosts.mark_buffer_result(
|
|
self.session,
|
|
str(row.id),
|
|
buffer_post_id=post["id"],
|
|
status=local_status(post.get("status")),
|
|
external_link=post.get("externalLink"),
|
|
sent_at=parse_buffer_datetime(post.get("sentAt")),
|
|
platform=post.get("channelService"),
|
|
)
|
|
return serialize_job_post(saved)
|
|
|
|
async def list_channels(self):
|
|
try:
|
|
return await list_buffer_channels()
|
|
except (httpx.HTTPError,BufferError,RuntimeError) as e:
|
|
raise HTTPException(status_code=502,detail="Failed to list Buffer channels") from e
|
|
|
|
async def fetch_job_posts(self,search=None,top=None,skip=0,ids=None,active_only=True):
|
|
rows,total=await JobPosts.fetch_job_posts(
|
|
self.session,
|
|
search=search,
|
|
top=top,
|
|
skip=skip,
|
|
ids=ids,
|
|
active_only=active_only,
|
|
)
|
|
return [serialize_job_post(r) for r in rows],total
|