Last active
April 28, 2023 04:58
-
-
Save ttscoff/3f545a4e74b46cc80eca2b887ee364a6 to your computer and use it in GitHub Desktop.
Scans RSS feed for latest post and creates Mastodon toot
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby -W1 | |
# frozen_string_literal: true | |
# This script scans an RSS feed for the latest post, and if | |
# it hasn't already been posted to Mastodon, creates a new | |
# toot with a link to it. | |
require 'json' | |
require 'optparse' | |
require 'rss' | |
### Mastodon Setup | |
MASTODON_ENDPOINT = 'https://MASTODON_SERVER/api/v1/statuses' | |
MASTODON_AUTH = 'XXXXXXXXXX' | |
### Feed Setup | |
# If this is set to a JSON file or URL, it will be used to get the recent posts, set to nil to use XML | |
JSON_FEED = nil | |
# If this is set to an XML file or url and JSON_FEED is nil, it will be used to get recent posts | |
RSS_FEED = '~/Sites/dev/bt/public/atom.xml' | |
# Where to keep track of what's already posted | |
TOOTS_DB = '~/Sites/dev/bt/toots.json' | |
# Template for posting, use %title% and %url% | |
POST_TEMPLATE = "Lately on the blog: %title%\n\n%url%" | |
# Query string to add to posts for campaigns, etc. | |
QUERY_STRING = '?utm_campaign=blog_post&utm_source=mastodon' | |
### Front Matter Updating | |
# Skip adding front matter if false | |
ADD_FRONT_MATTER = true | |
# Where Markdown files are stored in the format YYYY-MM-DD-slug.md | |
POSTS_DIR = '~/Sites/dev/bt/source/_posts' | |
# Base url of the site (include trailing slash) | |
SITE_URL = 'https://brettterpstra.com/' | |
# BlogToot | |
class Toot | |
include REXML | |
attr_accessor :debug, :silent, :force, :ten | |
def initialize(endpoint, token) | |
@endpoint = endpoint | |
@token = token | |
@ten = false | |
@debug = false | |
@force = false | |
@silent = false | |
@last_toot_file = File.expand_path(TOOTS_DB) | |
end | |
def posts | |
@posts ||= if JSON_FEED.nil? | |
entries | |
else | |
json_entries | |
end | |
end | |
def entries | |
rss = if File.exist?(File.expand_path(RSS_FEED)) | |
RSS::Parser.parse(File.expand_path(RSS_FEED), false) | |
else | |
RSS::Parser.parse(RSS_FEED, false) | |
end | |
entries = [] | |
case rss.feed_type | |
when 'rss' | |
rss.items.each { |item| entries.push({ title: item.title, url: item.link.href }) } | |
when 'atom' | |
rss.items.each { |item| entries.push({ title: item.title.content, url: item.link.href }) } | |
end | |
entries | |
rescue StandardError => e | |
raise "Error retrieving XML feed: #{e}" | |
end | |
def json_entries | |
json = if File.exist?(File.expand_path(JSON_FEED)) | |
JSON.parse(IO.read(JSON_FEED)) | |
else | |
JSON.parse(`curl -SsL '#{JSON_FEED}'`.strip) | |
end | |
json['items'].each_with_object([]) { |i, arr| arr.push({ title: i['title'], url: i['url'] }) } | |
rescue StandardError => e | |
raise "Error retrieving JSON feed: #{e}" | |
end | |
def post_info(post) | |
[post[:title], post[:url]] | |
end | |
def toot_last_ten | |
posts[0..10].reverse.each do |item| | |
@title, @post_url = post_info(item) | |
toot | |
end | |
end | |
def most_recent_post | |
latest = posts[0] | |
post_info(latest) | |
end | |
def tooted | |
res = @toots.select { |toot| toot['post_url'] =~ /#{Regexp.escape(@post_url)}/ } | |
res.count.positive? | |
end | |
def record_toot | |
return if @debug | |
raise 'No toot URI to record' if @toot_uri.nil? | |
@toots.push({ | |
post_url: @post_url, | |
toot_url: @toot_uri | |
}) | |
warn "Tooted #{@title}: #{@toot_uri}" unless @silent | |
File.open(@last_toot_file, 'w') { |f| f.puts @toots.to_json } | |
end | |
def update_markdown | |
return if @debug | |
raise 'No toot URI to update Markdown' if @toot_uri.nil? | |
slug = @post_url.sub(%r{^https?://[^/]+/}, '').sub(/index\.html?$/, '').gsub(%r{/}, '-').sub(/-$/, '') | |
source = File.join(File.expand_path(POSTS_DIR), "#{slug}.md") | |
content = IO.read(source) | |
if content =~ /^mastodon: http/ | |
content.sub!(/^mastodon: http.*?$/, "mastodon: #{@toot_uri}") | |
else | |
content.sub!(/^---\n/, "---\nmastodon: #{@toot_uri}\n") | |
end | |
File.open(source, 'w') { |f| f.puts content } | |
warn "Updated #{source} with Mastodon URL" unless @silent | |
puts "NEW_TOOT : #{source}" unless @silent | |
end | |
def render_template | |
POST_TEMPLATE.sub(/%title%/, @title).sub(/%url%/, "#{@post_url}#{QUERY_STRING}") | |
end | |
def toot | |
@toots = JSON.parse(IO.read(@last_toot_file).strip) | |
@title, @post_url = most_recent_post | |
@toot_uri = nil | |
return if tooted && !@force | |
begin | |
auth = "Authorization: Bearer #{@token}" | |
res = `curl -SsL -H "#{auth}" -F "status=#{render_template}"#{@debug ? %( -F visibility=direct) : ''} #{@endpoint}` | |
@toot_uri = JSON.parse(res)['uri'] | |
rescue StandardError => e | |
raise "Error posting toot: #{e}" | |
end | |
warn "TOOTED: #{@toot_uri}" unless @silent | |
record_toot | |
update_markdown if ADD_FRONT_MATTER | |
end | |
end | |
toot = Toot.new(MASTODON_ENDPOINT, MASTODON_AUTH) | |
optparse = OptionParser.new do |opts| | |
opts.banner = "Usage: #{File.basename(__FILE__)} [OPTIONS] [ten]" | |
opts.on('-d', '--debug', 'Post as private') do | |
toot.debug = true | |
end | |
opts.on('-f', '--force', 'Force post even if already posted') do | |
toot.force = true | |
end | |
opts.on('-s', '--silent', 'No output to STDOUT or STDERR') do | |
toot.silent = true | |
end | |
opts.on('--last-ten', 'Toot last 10 articles') do | |
toot.ten = true | |
end | |
opts.on('-h', '--help', 'Display this screen') do | |
puts opts | |
Process.exit 0 | |
end | |
end | |
optparse.parse! | |
if toot.ten | |
toot.toot_last_ten | |
else | |
toot.toot | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment