show/hide this revision's text 4 deleted 125 characters in body

Using FogBugz to Monitor an RSS Feed

It turns out that if you use the FogBugz API to submit a BugzScout request with the feed item's unique identifier as the "Scout Message" and set to flag to "Stop Reporting", you can send that API call as many times as you want, and only one case will ever be created for that feed item. This is still a very new process for us, so there are some kinks to be worked out. I'm going to use this as a placeholder to come and post the code, once it's checked in and not embarrassing. It's a pretty simple Python script that runs on top of the BeautifulSoup XML parsing codemuch what an RSS reader does.

Here's what it the script does, roughly:

  • It's not particularly good at managing Twitter conversations, where there's a string of @ replies in which your product is mentioned once. A future version will reconstruct conversations that included your product in the middle, will group conversations into one case, and will update the case should the conversation progress. We might even create a plug-in that will allow you to tweet back from the case.
  • The code relies on a slightly modified version of the library available at the bottom of the page here. My tweaked version allows you to create the FogBugz object with a token, rather than creating it and logging on to get a token. This allows you to put a token, and not a password into the code. Since it does rely on a couple of outside API's, though, you might not want to run it all that often, for fear of being throttled.

    The script is (effectively) idempotent. With the exception of the incrementing Occurrences field, the number of times you run it doesn't affect the result. Since it does rely on a couple of outside API's, though, you might not want to run it like crazy or you might get throttled.

    show/hide this revision's text 3 added 1567 characters in body; added 11 characters in body
  • Translates the tweet, if it's not English, so that this:diego! becomes this:
  • Submits the BugzScout case with the URL of the feed (and a "salt" salt value) as the sScoutDescription. This means that even if we keep sending the same request again and again, they'll all be aggregated under one case.
  • from __future__ import with_statement # This isn't required in Python 2.6# load values from files. We do this so we can make this code public without exposing# ourselves.# Here's the files that the script needs:# tweeter_ignore_list.txt - this is a list of people, including Fog Creek employees, whose tweets we will ignorenot create new cases# token.txt - for storing your FogBugz API token# salt.txt - for storing the "salt" you put into the ScoutMessage. This stops people from spamming you.with open("tweeter_ignore_list.txt") as ignore_file: ignore_list = [k.strip() for k in ignore_file.readlines()]with open("token.txt") as token_file: token_list = [k.strip() for k in token_file.readlines()]with open("salt.txt") as salt_file: salt_list = [k.strip() for k in salt_file.readlines()]if debug_flag: print "ignoring tweets from these users" print ignore_list# we do two runs, one for each product. "copilot" and "kiln" were wayyy too general.# note that this also gets replies to the @FogBugz account. If you have a dedicated account,# you should probably search for that name ('shneusk', 'richarmstrong', 'spolsky', e.g., 'michaelfogcreek')# one case will be created for each tweet, so don't worry about getting two cases for something# like "I love FogBugz from Fog Creek Software".fogbugz = FogBugz('https://your.fogbugz.com'FogBugz('https://our.fogbugz.com', "yourtokenhere") print tweet_translation if debug_flag: print tweet_translation.prettify() sScoutDescription='[twitbugz]-' sScoutDescription= salt_list[0] + tweet.link['href'],

    The script is (effectively) idempotent, with . With the exception of the incrementing Occurrences field. Basically, the number of times you run it doesn't affect the result.

    At some point, I'll check this code into a publicly available repo. If you have comments on it, let me know.

    show/hide this revision's text 2 added 1944 characters in body
    from xgoogle.translate import urllib2LanguageDetector, DetectionError, Translatordetect = LanguageDetector().detecttranslate = Translator().translatedebug_flag = False # call 'python twitbugz.py debug' to make this verboseif len(sys.argv) > 1: if sys.argv[1] == 'debug': debug_flag = True# this is a list of people, including Fog Creek employees, # whose tweets we will ignore 'spolsky') # some names removed to protect the innocent. 'michaelfogcreek')#gross, encoding the parameter literallyfogbugz = FogBugz('http://foo.fogbugz.com'FogBugz('https://your.fogbugz.com', "tokenremovedforsecurity") fogbugz_tweets = BeautifulSoup( twitter_search.open( 'http://search.twitter.com/search.atom?q=%s'% product BeautifulSoup(twitter_search.open('http://search.twitter.com/search.atom?q=%s'% product)) exceptURLError: TwitterConnectionError("Could not find Twitter.") if debug_flag: print tweet.prettify() print tweet_translation continue # move on if this should be ignored tweet_title = BeautifulSoup(tweet.title.renderContents(),convertEntities=BeautifulSoup.HTML_ENTITIES) tweet_translation = '' try: tweet_translation = BeautifulSoup(translate(tweet.title.string, lang_to="en"),convertEntities=BeautifulSoup.HTML_ENTITIES) except: # if anything goes wrong, we'll just put the original tweet in there. tweet_translation = tweet_title # This turns the translation into an ascii-only string. # The odd emoticons in this tweet seriously broke the script # http://twitter.com/fault0d/statuses/21386323580 # I tried a bunch of .decode() and .encode() stuff I got off teh Internets # but in the end, just pulling out any character above code 128 is fine tweet_translation_stripped = "".join([x if ord(x) < 128 else '?' for x in tweet_translation.string]) # Create the case in FogBugz. The title is the actual tweet. The content is a link and a translation. # If the language is already English, it leaves it untouched. resp = fogbugz.new(sTitle='[tweet] ' + tweet.title.stringtweet_title.string, sScoutDescription='my_secret_salt-sScoutDescription='[twitbugz]-' + tweet.link['href'], sEvent=tweet.link['href']sEvent="""%(translation)s%(link)s""" % {'link' : tweet.link['href'],'translation': tweet_translation_stripped}, #if debug_flag:#uncomment this line to get the case number back

    The code relies on a slightly modified version of the library available at the bottom of the page here. My tweaked version allows you to create the FogBugz object with a token, rather than creating it and logging on to get a token. This allows you to put a token, and not a password into the code. Since it does rely on a couple of outside API's, though, you might not want to run it all that often, for fear of being throttled.

    The script is idempotent, with the exception of the incrementing Occurrences field. Basically, the number of times you run it doesn't affect the result. At some point, I'll check this into a publicly available repo. If you have comments on it, let me know.

    show/hide this revision's text 1