Skip to content

Instantly share code, notes, and snippets.

@ttscoff
Last active April 28, 2023 04:58
Show Gist options
  • Save ttscoff/3f545a4e74b46cc80eca2b887ee364a6 to your computer and use it in GitHub Desktop.
Save ttscoff/3f545a4e74b46cc80eca2b887ee364a6 to your computer and use it in GitHub Desktop.
Scans RSS feed for latest post and creates Mastodon toot
#!/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