fb_posts: edit and repost through the publisher's signed reschedule
POST /fbposts/{id}/reschedule {message, scheduled_for}. The site derives
the per-post signature from the stored cancel link and asks the publisher
to delete the post on Facebook and redraft the queue file; the row goes
to drafted with no Facebook id and no cancel link until the publisher's
next cycle reports the new one. Times must be at least 15 minutes out,
with an offset. An explicit null now clears fb_post_id on upsert; an
absent one still keeps it. tests/smoke_admin.py 160 -> 162.
This commit is contained in:
@@ -1043,3 +1043,59 @@ def cancel_fb_post(request: Request, pid: str, x_admin_token: str = Header(None)
|
||||
out = store.mark_fb_cancelled(pid, actor)
|
||||
_log(request, "fbpost.cancelled", pid)
|
||||
return out
|
||||
|
||||
|
||||
@router.post("/fbposts/{pid:path}/reschedule")
|
||||
def reschedule_fb_post(request: Request, pid: str, payload: dict = Body(...), x_admin_token: str = Header(None)):
|
||||
"""Edit and repost a scheduled post: body {message, scheduled_for}. The
|
||||
site asks the publisher's control server, signed with the same per-post
|
||||
signature as the cancel link, to delete the post on Facebook and redraft
|
||||
the queue file; the publisher schedules it again on its next cycle
|
||||
(within about 15 minutes) and reports the new id. Until then the row is
|
||||
drafted with no cancel link."""
|
||||
actor = _auth(request, x_admin_token, "fbposts:read")
|
||||
rec = store.get_fb_post(pid)
|
||||
if not rec:
|
||||
raise HTTPException(404, "no such post")
|
||||
if rec["state"] != "scheduled":
|
||||
raise HTTPException(409, "post is %s, not scheduled" % rec["state"])
|
||||
if not rec.get("cancel_url") or not rec.get("fb_post_id"):
|
||||
raise HTTPException(409, "no signed link was recorded for this post")
|
||||
message = (payload.get("message") or "").strip()
|
||||
if not message:
|
||||
raise HTTPException(422, "message is required")
|
||||
if len(message) > 5000:
|
||||
raise HTTPException(422, "message is over 5000 characters")
|
||||
when = (payload.get("scheduled_for") or "").strip() or None
|
||||
if when:
|
||||
try:
|
||||
import datetime as _dt
|
||||
d = _dt.datetime.fromisoformat(when.replace("Z", "+00:00"))
|
||||
if d.tzinfo is None:
|
||||
raise ValueError
|
||||
if d <= _dt.datetime.now(_dt.timezone.utc) + _dt.timedelta(minutes=15):
|
||||
raise HTTPException(422, "scheduled_for must be at least 15 minutes from now")
|
||||
except ValueError:
|
||||
raise HTTPException(422, "scheduled_for must be an ISO timestamp with an offset")
|
||||
import urllib.parse as _up, urllib.request as _ur, urllib.error as _ue, json as _json
|
||||
u = _up.urlparse(rec["cancel_url"]); q = _up.parse_qs(u.query)
|
||||
sig = (q.get("sig") or [""])[0]
|
||||
if not sig:
|
||||
raise HTTPException(409, "the recorded cancel link carries no signature")
|
||||
target = _up.urlunparse((u.scheme, u.netloc, "/reschedule", "", "", ""))
|
||||
body = _json.dumps({"id": rec["fb_post_id"], "sig": sig, "message": message, "publish_at": when}).encode()
|
||||
try:
|
||||
with _ur.urlopen(_ur.Request(target, data=body, method="POST",
|
||||
headers={"Content-Type": "application/json", "User-Agent": "scout-website"}), timeout=60) as resp:
|
||||
status = resp.status
|
||||
except _ue.HTTPError as e:
|
||||
raise HTTPException(502, "publisher answered %d on reschedule" % e.code)
|
||||
except Exception as e:
|
||||
raise HTTPException(502, "publisher unreachable: %s" % e)
|
||||
if status >= 300:
|
||||
raise HTTPException(502, "publisher answered %d on reschedule" % status)
|
||||
out = store.upsert_fb_post({"id": pid, "status": "drafted", "message": message, "scheduled_for": when,
|
||||
"fb_post_id": None, "cancel_url": None, "unit": rec["unit"], "page_id": rec.get("page_id"),
|
||||
"image_ref": rec.get("image_ref"), "queue_file": rec.get("queue_file"), "link": rec.get("link")})
|
||||
_log(request, "fbpost.redrafted", "%s by %s%s" % (pid, actor, (" for " + when) if when else ""))
|
||||
return out
|
||||
|
||||
+3
-1
@@ -1158,7 +1158,9 @@ def upsert_fb_post(rec, image=None):
|
||||
fields["cancelled_at"] = rec.get("cancelled_at") or _now()
|
||||
fields["cancelled_by"] = rec.get("cancelled_by") or "publisher"
|
||||
if old:
|
||||
if fields.get("fb_post_id") is None:
|
||||
# A missing fb_post_id keeps the old one; an explicit null clears it
|
||||
# (a redraft has no Facebook id until the next cycle).
|
||||
if fields.get("fb_post_id") is None and "fb_post_id" not in rec:
|
||||
fields.pop("fb_post_id")
|
||||
sets = ", ".join("%s=?" % k for k in fields)
|
||||
con.execute("UPDATE fb_posts SET %s WHERE id=?" % sets, (*fields.values(), pid))
|
||||
|
||||
Reference in New Issue
Block a user