Since adding syndication of link posts to Pinboard I’ve added syndication of photos to Flickr (in theory, since I’m still waiting to get last years’ films developed I can’t use it yet) and notes to Twitter. I wasn’t sure about doing notes to Twitter as I don’t really want the clutter on my site, but I’ve decided to take the same approach as I have with Pinboard: I can tweet via my website, but I don’t have to.
I’ll not talk about the Photo syndication as it’s not so interesting; it was all made pretty easy with the Flickraw gem. The Twitter stuff is a bit more interesting because I wanted to make the experience as frictionless as possible; still pretty easy thanks to the twitter gem.
In the Pinboard syndication post I cheated and just linked to the repository, but I’ll try and talk through this one a bit more. So not in the order that I wrote the code, but in an order that kind of makes sense:
I have a rake task that handles POSSEing to Twitter.
task :posse_twitter do
date = File.open("_deploy_date", &:readline)
netrc = Netrc.read
token, secret = netrc["twitter.com"]
puts "--> Looking for note posts to syndicate to Twitter"
posse_twitter = Jekyll_syndicate_twitter.new(token, secret, date)
posse_twitter.syndicate()
#Above should fail if no posts found, hence re-build below will only occur if changes made
Rake::Task["build"].invoke
Rake::Task["deploy"].invoke
end
This reads a file called _date_deploy
from the top of my Jekyll site directory. It’s a text file that contains one line only: the date and time that I last deployed my site. I store my user/access token and secret in a .netrc
file (the actual application/consumer token and secret in a _flickr_app
file). This rake task then creates a new instance of a Jekyll_syndicate_twitter
class and calls the syndicate method on it:
class Jekyll_syndicate_twitter
def initialize(token, secret, date)
@date = date
@twitter = Twitter::REST::Client.new
#The actual key and secret are stored with the app in a file called...
twitter_app = YAML.load_file("_twitter_app")
@twitter.consumer_key = twitter_app["consumer_key"]
@twitter.consumer_secret = twitter_app["consumer_secret"]
#Assumes authentication has already taken place
@twitter.access_token = token
@twitter.access_token_secret = secret
end
def syndicate
note_posts = find_posts_since("note", @date)
note_posts.each do |filename, title|
#Does stuff, we'll get to this later
end
end
end
This is fairly simple because the gem does all the hard work. The initialize
method just handles creating a new Twitter client given all the correct tokens and secrets. The syndicate
method finds all new note type posts since the last deploy date using find_posts_since
:
(I’ll cut some of this out and just discuss what changed since the Pinboard syndication).
def find_posts_since(post_type, last_deploy_date)
#This is looking in _posts not _site
posts_to_syndicate = []
#Need to do this differently for note posts and normal posts
post_directory = "_posts/"
if post_type == "note"
post_directory += "notes/"
end
#Skip sub-directories
all_posts = Dir[post_directory+'/*'].reject do |post|
File.directory?(post)
end
all_posts.each do |post|
filename = File.basename(post)
#Need to check date in file if available (for notes) if not use filename
yaml = YAML.load_file(post)
if yaml.has_key?("date")
#Seems to come as Time
publish_date = yaml["date"].to_datetime
else
publish_date = DateTime.parse(filename[0..9])
end
if publish_date > DateTime.parse(last_deploy_date)
#...Gets the actual bits and bobs required from the files
end
end
if posts_to_syndicate.empty?
fail NoPostsFoundError, "No #{post_type} posts found"
end
posts_to_syndicate
end
I’m storing my notes in a sub-directory of _posts
just to keep everything a bit cleaner so the method needs to look in the right place. Since notes have the potential to be posted more than once a day (I haven’t had that problem yet though) I can’t rely on the file name when comparing the deploy date, I need to check the actual date in the file if it is there. If no new posts are found then I raise a custom error so I can capture this in my Rakefile and still allow other things to carry on since it is an ok error.
Now I’ve got a list of note posts I want to syndicate and relevant data from those posts (which for notes is just the text of the note), going back to the syndicate class…
class Jekyll_syndicate_twitter
def initialize(token, secret, date)
#The stuff that was up above
end
def syndicate
note_posts = find_posts_since("note", @date)
note_posts.each do |filename, title|
#For twitter posts can't add any kind of POSSE backlink
#But should be possible to get the link of the twitter post
tweet = @twitter.update(title)
#Can build link from tweet.id, weird that doesn't seem to be a way to get full url
link_to_tweet = "https://twitter.com/#{tweet.user.name}/statuses/#{tweet.id}"
#Need to then update the post, as per flickr
add_link_to_blog_post("_posts/notes/"+filename, link_to_tweet)
end
end
end
…it then tweets the relevant bit (I’m using the title field of the yaml front matter to store the tweet/note text) and then edits the note post to add the link to the tweet in:
def add_link_to_blog_post(post, link)
#Manually do or use some aspect of to_yaml. I think safer to do manually can then quote the url
lines = File.readlines(post)
#insert before second ---
yaml_start_idx = lines.index{ |line| line.strip == "---" }
yaml_end_idx = lines[yaml_start_idx+1..-1].index{ |line| line.strip == "---" } + yaml_start_idx+1
lines.insert(yaml_end_idx, "u-syndication: \"#{link}\"\n")
#Write back out
f = File.open(post, "w")
lines.each do |line|
f << line
end
f.close
end
This simply opens the relevant note file for editing and adds a u-syndication
property to the yaml front matter which I can then make use of in my Jekyll templates using some Liquid magic to add a syndication link to the note. I’ve also included webactions, but only on the page permalink, not everywhere the note is displayed. I don’t have my Jekyll templates in a public repository anymore so I might go over how I’ve set this up in another post. Might.
Last of all then, and getting to the main point of this post, so I can make tweeting from the command line usable, I have a rake task set up for it:
task :note, [:text] do |t, args|
#Get argument from command line
#Can't use commas: http://stackoverflow.com/questions/7258148/how-can-i-use-a-comma-in-a-string-argument-to-a-rake-task
#Fix idea from: http://blog.stevenocchipinti.com/2013/10/18/rake-task-with-an-arbitrary-number-of-arguments/
text = args[:text]
if args.extras.length > 0
#rake strips white space as well. Will just have to assume I have used spaces after commas
text += ", "+args.extras.join(", ")
end
#Check length
if text.length > 140
fail NoteTooLongError, "Note is too long by #{text.length-140}"
end
#replace anything that's not a letter or number with a hyphen to make it pretty ish
filename = text.gsub(/[^0-9a-zA-Z]/, "-")
#Just to be on safe side in case I've missed something
filename = CGI.escape(filename)
#get date
date = Date.today.iso8601
datetime = DateTime.now.iso8601
#write out to file
File.open("_posts/notes/#{date}-#{filename}.markdown", "w") do |file|
file << "---\n"
file << "layout: page\n"
file << "type: note\n"
file << "title: \"#{text}\"\n"
file << "categories:\n"
file << "- note\n"
file << "date: #{datetime}\n"
file << "---\n"
end
Rake::Task["build"].invoke
#deploy, which will call posse_twitter
Rake::Task["deploy"].invoke
end
With this I can call rake note["Here's a tweet"]
on the command line and it’ll build my note post, build my site, syndicate the tweet out and deploy my site. Ok, there is a bit of a delay waiting for the site to build, but actually not too long.
The task looks complex because Rake has an issue with commas in arguments for tasks and will split any string on commas and consider these as separate arguments. So if I tried to tweet something like “All things being well, I’m now syndicating notes” all that I’d end up with would be “All things being well”. I’ve added a work-around to recombine any split string and add the commas back in. I might have to rethink this if I ever want to extend the work I’ve done and syndicate out twitter replies as I’ll need to include an intentional additional argument of the id of the tweet being replied to. But for the time being it’s unlikely I’m going to want to do this as I’ll just jump to TTYtter. I’m also checking that my note isn’t longer than 140 characters because I have no interest in syndicating out truncated notes.
For the most recent code refer, once again, to the repository