Dec 14, 2022 Tags: lifestyle, meta
TL;DR: I’ve decided to wean myself off of Twitter by going “write-only”: my posts there will just be updates to this blog, bridged from Mastodon via Moa Bridge. I’ll be making those posts automatically, using a tool I made for exactly this purpose. Every post I make automatically will also therefore be available on Mastodon, and I encourage you (dear reader) to follow me there if you’d like.
Going forwards, you can find me (as in, actually talk to me) on Mastodon
at @yossarian@infosec.exchange
1.
I’m unlikely to respond to Twitter DMs anymore2.
Update: As I regularly forget to mention: this blog also has an IRC
room at #enosuchblog
on libera.chat
.
It looks like the “TL;DR” to this post might end up being almost as long as the actual post.
I don’t think I need to do much explaining here: Twitter is not the right place for me anymore. Others have said it just fine already, so I’ll keep it brief: I don’t have confidence in the site’s leadership, and I’ve been concerned by a visible upwelling of reactionary content on the site.
I’m also aware that I’m part of multiple communities that provide value to the site, beyond just the value in advertising to me. Most of those communities have left or are in the process of leaving, and the value I get out of the fragments that remain do not balance out against the continued value that Twitter gets from my active account.
Still, I can’t just delete my account and be done with it: deleting my account means that the username gets recycled, and I’d prefer if that didn’t happen. I also still have friends who would like to stay (which I don’t blame them for) and people who I know read my blog by discovering my posts on Twitter.
The best solution for both of these groups (and myself) is to keep the account, but in a “write-only” state: updates to this blog will be Tweeted out automatically, but the account will otherwise cease to be active.
autopost
As mentioned above: my only remaining presence on Twitter will be automated posts, which will happen whenever a new post on this blog comes out.
These posts will look a lot like previous ones, which I’ve been doing manually:
…except that they’ll be coming from a program called
autopost
.
Here’s the entirety of how I use autopost
to automate publishing my blog posts to various
sites, including Twitter:
1
autopost atom https://blog.yossarian.net/feed.xml
Internally, that:
It’s all Python, so here’s what the Twitter backend looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Twitter(Backend):
def __init__(self, config: TwitterConfig):
self._config = config
self._client = twitter.Api(
consumer_key=self._config.api_key.get_secret_value(),
consumer_secret=self._config.api_key_secret.get_secret_value(),
access_token_key=self._config.access_token.get_secret_value(),
access_token_secret=self._config.access_token_secret.get_secret_value(),
)
def health_check(self) -> Result[None, str]:
return Err("unimplemented")
def post(self, content: str, url: str, *, tags: list[str] = []) -> Result[Url, str]:
tags = [f"#{tag}" for tag in tags]
status = dedent(
f"""
{content}
{url}
{' '.join(tags)}
"""
)
try:
resp = self._client.PostUpdate(status)
url = f"https://twitter.com/{resp.user.screen_name}/status/{resp.id_str}"
return Ok(Url(url))
except Exception as e:
return Err(str(e))
…except that I can’t currently use it, because Twitter has neglected to give me a developer token for over a month3.
Instead, I’m using the Mastodon backend:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Mastodon(Backend):
def __init__(self, config: MastodonConfig):
self._config = config
self._client = MastodonClient(
api_base_url=self._config.server,
client_id=self._config.client_key.get_secret_value(),
client_secret=self._config.client_secret.get_secret_value(),
access_token=self._config.access_token.get_secret_value(),
)
def health_check(self) -> Result[None, str]:
try:
ok = self._client.instance_health()
if ok:
return Ok()
return Err(f"{self._config.server} failed instance health check")
except Exception as e:
return Err(str(e))
def post(self, content: str, url: str, *, tags: list[str] = []) -> Result[Url, str]:
tags = [f"#{tag}" for tag in tags]
status = dedent(
f"""
{content}
{url}
{' '.join(tags)}
"""
)
try:
resp = self._client.status_post(status)
return Ok(Url(resp["url"]))
except Exception as e:
return Err(str(e))
…and mirroring my posts via moa.party, which is a public instance of Moa Bridge.
Configuring autopost
is also pretty easy, it’s just a TOML file, listing
each “backend” to post to and how to get its credentials (in this case,
they’re expected to be in the listed environment variables):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[[backend]]
type = "Reddit"
name = "reddit:/r/enosuchblog"
subreddit = "enosuchblog"
client_id = { type = "Environment", variable = "REDDIT_CLIENT_ID" }
client_secret = { type = "Environment", variable = "REDDIT_CLIENT_SECRET" }
username = "yossarian_flew_away"
password = { type = "Environment", variable = "REDDIT_PASSWORD" }
[[backend]]
type = "Mastodon"
name = "mastodon:@yossarian@infosec.exchange"
server = "https://infosec.exchange"
client_key = { type = "Environment", variable = "INFOSEC_EXCHANGE_CLIENT_KEY" }
client_secret = { type = "Environment", variable = "INFOSEC_EXCHANGE_CLIENT_SECRET" }
access_token = { type = "Environment", variable = "INFOSEC_EXCHANGE_ACCESS_TOKEN" }
(As implied by the format, there can be multiple backends of each type, e.g. multiple subreddits to post to. I’m not currently using that functionality, but it might be useful to myself or others in the future.)
On its own, all autopost
does is make a post to a bunch of services.
It isn’t entirely sufficient for my purposes: there needs to be a little more state, to ensure that I don’t accidentally make duplicate posts each time I use GitHub Actions to deploy my blog.
To accomplish that, I have a little bit of CI glue:
autopost
.Here’s what that actually looks like, in CI:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
jobs:
pre-deploy:
runs-on: ubuntu-latest
outputs:
title: ${{ steps.latest-blog-title.outputs.title }}
steps:
- name: dependencies
run: |
# omitted for brevity
- name: get the most recent blog post title
id: latest-blog-title
run: |
title=$(curl https://blog.yossarian.net/feed.xml | \
xq -r '.feed.entry[0].title."#text"')
echo "title=${title}" | tee -a "${GITHUB_OUTPUT}"
# deploy omitted for brevity
autopost:
needs: [pre-deploy, deploy]
runs-on: ubuntu-latest
steps:
- name: dependencies
run: |
# omitted for brevity
- name: check for a new post
env:
LAST_POST_TITLE: ${{ needs.pre-deploy.outputs.title }}
run: |
latest_title=$(curl https://blog.yossarian.net/feed.xml | \
xq -r '.feed.entry[0].title."#text"')
if [[ "${latest_title}" = "${LAST_POST_TITLE}" ]]; then
echo "no new post, not autoposting"
exit 0
fi
echo "autoposting for: ${latest_title}"
autopost atom https://blog.yossarian.net/feed.xml
…and with that, updates only go out when the blog is updated with a new post (and not whenever I fix a typo or re-deploy for any other reason).
As I am wont, autopost
is open source and available for anybody else to use
via PyPI:
1
2
$ python -m pip install autopost
$ autopost --help
My hope is that it’ll help others make the move away from Twitter as well.
Among many other places. ↩
This is my opportunity to say “thanks” to those of you who did reach out to me via Twitter over the years. You might find it interesting to know that you were in the majority, followed by e-mail, followed by Keybase and Reddit. ↩
I can’t really blame them for this, though. It’s hard to imagine that there’s anybody even looking at developer applications at the moment. ↩