Skip to content

Instantly share code, notes, and snippets.

@dmerejkowsky
Last active July 26, 2020 10:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dmerejkowsky/d105bbf82b7746bf60b044ebd1c5fef8 to your computer and use it in GitHub Desktop.
Save dmerejkowsky/d105bbf82b7746bf60b044ebd1c5fef8 to your computer and use it in GitHub Desktop.
"""
Code for https://dmerej.info/blog/post/my-blogging-flow-part-2-publishing/,
because some people asked.
Entry points are either `publish()`or `new_post()`.
"""
import contextlib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import subprocess
import smtplib
import sys
import feedparser
from path import Path
import ruamel.yaml
import twitter
from mastodon import Mastodon
import markdown
CFG_PATH = Path("~/.config/dmerej.info.yml").expanduser()
NEW_POST_EMAIL_SUBJECT = "[dmerej.info] New blog post"
NEW_POST_TEXT = """\
New #blog post
{post_title}: {post_url}
{hashtags}
"""
NEW_POST_EMAIL_MD = """\
### New blog post on dmerej.info/blog
[{post_title}]({post_url})
To unsubscribe, reply to this e-mail and tell me you want to opt out,
no questions asked :)
"""
NEW_POST_EMAIL_HTML = markdown.markdown(NEW_POST_EMAIL_MD)
def publish():
""" Synchronize local blog repo on the server """
blog_src_path = Path(read_conf()["blog"]["src"])
build(blog_src_path)
rsync(blog_src_path)
def new_post(tags):
""" Tell the world about the latest article """
title, url = fetch_latest_article()
notify_newsletter_subscribers(title, url)
notify_social_media(title, url, tags)
def read_conf():
parsed = ruamel.yaml.safe_load(CFG_PATH.read_text())
return parsed
class Tweeting:
""" Wraps the twitter library for a simpler API """
def __init__(self):
conf = read_conf()
auth_dict = conf["twitter"]["auth"]
keys = ["token", "token_secret", "api_key", "api_secret"]
auth_values = (auth_dict[key] for key in keys)
auth = twitter.OAuth(*auth_values)
self._api = twitter.Twitter(auth=auth)
def tweet(self, text):
self._api.statuses.update(status=text)
class Tooting:
""" Wraps the Mastodon library for a simple API """
def __init__(self):
conf = read_conf()
mastodon_conf = conf["mastodon"]
instance_url = mastodon_conf["instance_url"]
auth_conf = mastodon_conf["auth"]
client_id = auth_conf["client_id"]
client_secret = auth_conf["client_secret"]
token = auth_conf["token"]
self.mastodon = Mastodon(
client_id=client_id,
client_secret=client_secret,
access_token=token,
api_base_url=instance_url,
)
def toot(self, text):
self.mastodon.toot(text)
def build(blog_src_path):
""" Build the static blog pages, without the drafts """
cmd = ["hugo", "--buildDrafts=false"]
public_path = blog_src_path / "public"
public_path.rmtree_p()
subprocess.run(cmd, cwd=blog_src_path, check=True)
def rsync(blog_src_path, dry_run=False):
""" Synchronize the built HTML pages with the files on the server """
# fmt: off
cmd = [
"rsync",
"--recursive", "--itemize-changes", "--checksum", "--delete"
"public",
"ssh://...",
]
# fmt: on
subprocess.run(cmd, cwd=blog_src_path, check=True)
def fetch_latest_article():
""" Extract the latest title and url from the RSS feed """
feed = feedparser.parse("https://dmerej.info/blog/index.xml")
latest = feed.entries[0]
title = latest.title
url = latest.link
return title, url
def notify_newsletter_subscribers(title, url):
""" Send an email to the recipents listed in the config file """
conf = read_conf()
recipients = conf["newsletter"]["subscribers"]
author = conf["newsletter"]["from"]
subject = NEW_POST_EMAIL_SUBJECT
text = NEW_POST_EMAIL_MD.format(post_url=url, post_title=title)
html = NEW_POST_EMAIL_HTML.format(post_url=url, post_title=title)
send_email(
subject=subject, author=author, recipients=recipients, text=text, html=html
)
def notify_social_media(title, url, tags):
"""
Create a tweet on twitter and a toot on mastodon about the
latest blog post
"""
hashtags = " ".join(["#" + x for x in tags])
text = NEW_POST_TEXT.format(post_url=url, post_title=title, hashtags=hashtags)
tweeting = Tweeting()
tweeting.tweet(text)
tooting = Tooting()
tooting.toot(text)
@contextlib.contextmanager
def mail_server(conf):
""" Wrap SMTP class for a simler API """
smtp_conf = conf["smtp"]
server = smtplib.SMTP(smtp_conf["address"])
server.starttls()
auth_conf = conf["auth"]
server.login(auth_conf["login"], auth_conf["password"])
yield server
server.quit()
def send_email(*, subject, author, recipients, text, html):
""" Generic function to send emails using a SMTP server """
conf = read_conf()
with mail_server(conf["email"]) as server:
message = MIMEMultipart("alternative")
message["subject"] = subject
message["From"] = author
part1 = MIMEText(text, "plain")
part2 = MIMEText(html, "html")
message.attach(part1)
message.attach(part2)
server.sendmail(author, recipients, message.as_string())
blog:
src: /home/dmerej/src/blog
email:
smtp:
address: ...
auth:
login: ...
password: ...
newsletter:
from: ...
subscribers:
- ..@...
- ..@...
twitter:
auth:
api_key: ...
api_secret: ...
token: ...
token_secret: ...
mastodon:
instance_url: "https://mamot.fr"
auth:
client_id: ...
client_secret: ...
token: ...
[tool.poetry]
name = "dmerej.info"
version = "0.1.0"
description = "tools for dmerej.info"
# ...
[tool.poetry.dependencies]
python = "^3.7"
feedparser = "^5.2.1"
markdown = "^3.1.1"
"Mastodon.py" = "^1.5.0"
path = "^13.1.0"
"ruamel.yaml" = "^0.16.7"
twitter = "^1.18.0"
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment