How do I use FogBugz to manage my Twitter presence? - FogBugz Knowledge Exchange most recent 30 from http://fogbugz.stackexchange.com 2013-05-22T02:40:07Z http://fogbugz.stackexchange.com/feeds/question/3628 http://www.creativecommons.org/licenses/by-nc/2.5/rdf http://fogbugz.stackexchange.com/questions/3628/how-do-i-use-fogbugz-to-manage-my-twitter-presence How do I use FogBugz to manage my Twitter presence? FogBugz FAQ 2010-07-15T15:16:49Z 2011-09-09T07:27:59Z <p>I'd like to be able to put all of my customer service requests in one place, including mentions of my company/product name on Twitter, and @ replies to my company twitter account. Can FogBugz do this?</p> http://fogbugz.stackexchange.com/questions/3628/how-do-i-use-fogbugz-to-manage-my-twitter-presence/3629#3629 Answer by Rich Armstrong for How do I use FogBugz to manage my Twitter presence? Rich Armstrong 2010-07-15T15:45:47Z 2010-08-19T19:38:46Z <h2>Using FogBugz to Monitor an RSS Feed</h2> <p>FogBugz's <a href="http://fogbugz.stackexchange.com/questions/315/submit-cases-from-the-field-bugzscout" rel="nofollow">BugzScout</a> capability actually makes this pretty easy. RSS feeds, in order to work correctly, have to uniquely identify each entry in the feed. That way, feed readers can periodically load the feed and sort out which items are new.</p> <p>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 pretty much what an RSS reader does.</p> <p>Here's what the script does, roughly:</p> <ol> <li>Loads the Twitter Search RSS feed for a list of words, here they are "FogBugz" and "Fog Creek". (Copilot and Kiln are too general to find in Twitter through this method.)</li> <li>Creates a fake correspondent address for the twitter account. This is a personal choice that helps other internal processes we have.</li> <li>Translates the tweet, if it's not English, so that this: <img src="https://developers.fogbugz.com/default.asp?pg=pgDownload&amp;pgType=pgWikiAttachment&amp;ixAttachment=439&amp;sFileName=diego.png" alt="diego!"> becomes this: <img src="https://developers.fogbugz.com/default.asp?pg=pgDownload&amp;pgType=pgWikiAttachment&amp;ixAttachment=440&amp;sFileName=diego-translated.png" alt="go diego!"></li> <li>Submits the BugzScout case with the URL of the feed (and a 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.</li> </ol> <p>Known Issues:</p> <ul> <li>The Occurrences field will increment every time the script is run. Just ignore it.</li> <li>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.</li> <li>I always end up clicking on the link to the status update and losing my original window due to the link not opening in a new window. This is a FogBugz design choice that I generally support, but for some reason, it breaks the user model here.</li> </ul> <p>Here's the code:</p> <pre><code>#!/usr/local/bin/python from __future__ import with_statement # This isn't required in Python 2.6 import re, string, sys from fogbugz import FogBugz from xgoogle.translate import LanguageDetector, DetectionError, Translator from BeautifulSoup import BeautifulSoup, CData import urllib import urllib2 detect = LanguageDetector().detect translate = Translator().translate class TwitterConnectionError(Exception): pass debug_flag = False # call 'python twitbugz.py debug' to make this verbose if len(sys.argv) &gt; 1: if sys.argv[1] == 'debug': debug_flag = True # 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 will not 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 (e.g., 'zappos_service'). # 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". product_list = ('fogbugz', '%22fog+creek%22') fogbugz = FogBugz('https://our.fogbugz.com', token_list[0]) twitter_search = urllib2.build_opener() for product in product_list: try: fogbugz_tweets = BeautifulSoup(twitter_search.open('http://search.twitter.com/search.atom?q=%s'% product)) except: raise for tweet in fogbugz_tweets.findAll('entry'): if debug_flag: print tweet.prettify() # get the twitter profile name so we can build a fake correspondent address. author = tweet.author.contents[1].string.partition(' ')[0] if author.lower() in ignore_list: continue # move on if this should be ignored author = author + "@twitbugz.fake" 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 if debug_flag: print tweet_translation.prettify() # 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) &lt; 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.string, sProject="Inbox", sArea="Support", sScoutDescription= salt_list[0] + tweet.link['href'], fScoutStopReporting=1, sEvent="""%(translation)s %(link)s""" % {'link' : tweet.link['href'],'translation': tweet_translation_stripped}, sCategory="Inquiry", sCustomerEmail=author) if debug_flag: print resp.prettify() </code></pre> <p>The code relies on a slightly modified version of the library available at the bottom of the page <a href="https://our.fogbugz.com/default.asp?W1048" rel="nofollow">here</a>. 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. </p> <p>The script is (effectively) <a href="http://en.wikipedia.org/wiki/Idempotence" rel="nofollow">idempotent</a>. 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.</p> <p>At some point, I'll check this code into a publicly available repo. If you have comments on it, let me know.</p>