96 lines
3.3 KiB
Python
96 lines
3.3 KiB
Python
"""Slack sink: upload the parsed Markdown as a file via the external-upload flow.
|
|
|
|
Slack deprecated ``files.upload`` (retired) in favour of a three-step external
|
|
upload. Delivery follows that official path:
|
|
|
|
1. ``files.getUploadURLExternal`` — reserve an upload URL + file id for the
|
|
given filename and byte length.
|
|
2. ``POST`` the raw bytes to the returned upload URL.
|
|
3. ``files.completeUploadExternal`` — finalize the upload, attach it to the
|
|
target channel, and post an initial comment.
|
|
|
|
Images are *not* embedded: Markdown is uploaded as a single ``.md`` file.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import urllib.parse
|
|
|
|
from . import _http, _md
|
|
from .base import ParsedDoc, Sink, SinkError, SinkResult, register
|
|
|
|
|
|
@register
|
|
class SlackSink(Sink):
|
|
name = "slack"
|
|
requires = ("SLACK_BOT_TOKEN", "SLACK_CHANNEL")
|
|
label = "Slack channel (file upload)"
|
|
|
|
def deliver(self, doc: ParsedDoc) -> SinkResult:
|
|
token = self.env("SLACK_BOT_TOKEN")
|
|
channel = self.env("SLACK_CHANNEL")
|
|
auth = {"Authorization": f"Bearer {token}"}
|
|
|
|
content = doc.markdown.encode("utf-8")
|
|
filename = _md.slugify(doc.title) + ".md"
|
|
|
|
# Step 1: reserve an external upload URL + file id. This endpoint wants
|
|
# form-encoded data, so use http_request and parse the JSON response.
|
|
form = urllib.parse.urlencode({
|
|
"filename": filename,
|
|
"length": len(content),
|
|
}).encode("utf-8")
|
|
status, raw = _http.http_request(
|
|
"POST",
|
|
"https://slack.com/api/files.getUploadURLExternal",
|
|
headers={**auth, "Content-Type": "application/x-www-form-urlencoded"},
|
|
data=form,
|
|
)
|
|
parsed = _parse_json(raw)
|
|
if not parsed.get("ok"):
|
|
raise SinkError(parsed.get("error") or f"Slack getUploadURLExternal failed (HTTP {status})")
|
|
upload_url = parsed.get("upload_url")
|
|
file_id = parsed.get("file_id")
|
|
if not upload_url or not file_id:
|
|
raise SinkError("Slack did not return an upload URL / file id")
|
|
|
|
# Step 2: upload the raw bytes to the reserved URL.
|
|
up_status, _up_body = _http.http_request(
|
|
"POST", upload_url,
|
|
headers={"Content-Type": "application/octet-stream"},
|
|
data=content,
|
|
)
|
|
if up_status != 200:
|
|
raise SinkError(f"Slack file upload failed (HTTP {up_status})")
|
|
|
|
# Step 3: finalize the upload into the channel.
|
|
status, parsed = _http.request_json(
|
|
"POST",
|
|
"https://slack.com/api/files.completeUploadExternal",
|
|
headers=auth,
|
|
payload={
|
|
"files": [{"id": file_id, "title": doc.title}],
|
|
"channel_id": channel,
|
|
"initial_comment": f"Parsed: {doc.title}",
|
|
},
|
|
)
|
|
if not parsed.get("ok"):
|
|
raise SinkError(parsed.get("error") or f"Slack completeUploadExternal failed (HTTP {status})")
|
|
|
|
files = parsed.get("files") or [{}]
|
|
url = files[0].get("permalink")
|
|
return SinkResult(
|
|
sink=self.name, ok=True, url=url,
|
|
detail="uploaded .md file (images not embedded)",
|
|
)
|
|
|
|
|
|
def _parse_json(raw):
|
|
import json
|
|
if not raw:
|
|
return {}
|
|
try:
|
|
return json.loads(raw.decode("utf-8"))
|
|
except (ValueError, UnicodeDecodeError):
|
|
return {}
|