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