5

1

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?

flag
1 
This is something we'd like to see too. In our case we're mostly interested in dealing with @replies and DMs to our support only Twitter. – jasonjwwilliams Jul 15 2010 at 19:14

1 Answer

4

Using FogBugz to Monitor an RSS Feed

FogBugz's BugzScout 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.

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.

Here's what the script does, roughly:

  1. 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.)
  2. Creates a fake correspondent address for the twitter account. This is a personal choice that helps other internal processes we have.
  3. Translates the tweet, if it's not English, so that this: diego! becomes this: go diego!
  4. 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.

Known Issues:

  • The Occurrences field will increment every time the script is run. Just ignore it.
  • 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.
  • 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.

Here's the 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) > 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) < 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()

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.

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.

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

link|flag

Your Answer

Not the answer you're looking for? Browse other questions tagged or ask your own question.