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