home *** CD-ROM | disk | FTP | other *** search
/ Maximum CD 2011 October / maximum-cd-2011-10.iso / DiscContents / digsby_setup.exe / lib / plugins / twitter / res / twitter.js < prev    next >
Encoding:
JavaScript  |  2011-01-31  |  90.8 KB  |  2,752 lines

  1. var TRENDS_UPDATE_MINS = 60;
  2. var IDLE_THRESHOLD_MS = 10 * 60 * 1000;
  3.  
  4. var INVITE_TEXT = "Hey, I've been using Digsby and thought you might find it useful. Check out the demo video: ";
  5. var INVITE_URL  = "http://bit.ly/clMDW5";
  6.  
  7. // javascript's RegExp \w character class doesn't include common extended characters. this string
  8. // includes some common latin characters, at least.
  9. var wordchars = '\\w└╚╠╥┘αΦ∞≥∙┴╔═╙┌▌ßΘφ≤·²┬╩╬╘█ΓΩε⌠√├╤╒π±⌡─╦╧╓▄Σδ∩÷ⁿí┐τ╟▀╪°┼σ╞µ▐■╨≡';
  10.  
  11. /* linkify @usernames */
  12. var atify_regex = new RegExp('(?:(?:^@([\\w]+))|(?:([^\\w])@([' + wordchars + ']+)))(/[' + wordchars + ']+)?', 'g');
  13.  
  14. var atifyOnClick = 'onclick="javascript:return tweetActions.openUser(this);"';
  15.  
  16. function atify(s) {
  17.     return s.replace(atify_regex, function(m, sn1, extra, sn2, listName) {
  18.         var screenname = (sn1 || sn2) + (listName ? listName : '');
  19.         return (extra || '') + '@<a class="userLink" href="http://twitter.com/' + screenname + '"' +
  20.             ' ' + atifyOnClick +
  21.             '>' + screenname + '</a>';
  22.     });
  23. }
  24.  
  25. /* linkify #hastags */
  26. var hashifyRegex = new RegExp('(?:(?:^)|(?:([^/&' + wordchars + '])))(#([' + wordchars + ']{2,}))', 'g');
  27.  
  28. function hashify(s) {
  29.     return s.replace(hashifyRegex, function(m, extra, outerSearch, search) {
  30.          return (extra || '') + '<a href="http://search.twitter.com/search?q=%23' + search + '">' + outerSearch + '</a>';
  31.     });
  32. }
  33.  
  34. function twitterLinkify(s) {
  35.     return atify(hashify(linkify(s)));
  36. }
  37.  
  38. var infoColumns = [
  39.     ['key', 'TEXT'],
  40.     ['value', 'TEXT']
  41. ];
  42.  
  43. // detect malformed databases and ask the account to delete all local data if we do
  44. function executeSql(tx, query, args, success, error) {
  45.     function _error(tx, err) {
  46.         if (err.message === 'database disk image is malformed')
  47.             D.notify('corrupted_database');
  48.         else
  49.             return error(tx, err);
  50.     }
  51.  
  52.     return tx.executeSql(query, args, success, _error);
  53. }
  54.  
  55. var tweetColumns = [
  56.     // [JSON name, sqlite datatype]
  57.     ['id',         'TEXT UNIQUE'],
  58.     ['created_at', 'TEXT'],
  59.     ['text',       'TEXT'],
  60.     ['source',     'TEXT'],
  61.     ['truncated',  'TEXT'],
  62.     ['in_reply_to_status_id', 'TEXT'],
  63.     ['in_reply_to_user_id',   'INTEGER'],
  64.     ['favorited',  'BOOLEAN'],
  65.     ['in_reply_to_screen_name', 'TEXT'],
  66.     ['user',       'INTEGER'],
  67.  
  68.     // custom
  69.     ['read',       'INTEGER'], // is read
  70.     ['mention',    'BOOLEAN'], // has @username in text
  71.     ['search',     'TEXT'],    // came from a search
  72.  
  73.     // search tweets are returned with just the username, not an ID, so for these
  74.     // tweets we only have from_user and profile_image_url
  75.     ['from_user',  'TEXT'],
  76.     ['profile_image_url', 'TEXT']
  77. ];
  78.  
  79. var directColumns = [
  80.     ['id',           'TEXT UNIQUE'],
  81.     ['sender_id',    'INTEGER'],
  82.     ['text',         'TEXT'],
  83.     ['recipient_id', 'INTEGER'],
  84.     ['created_at',   'TEXT'],
  85.  
  86.     ['read',         'INTEGER'],
  87. ];
  88.  
  89. var userColumns = [
  90.     ['id',          'INTEGER UNIQUE'],
  91.     ['name',        'TEXT'],
  92.     ['screen_name', 'TEXT'],
  93.     ['location',    'TEXT'],
  94.     ['description', 'TEXT'],
  95.     ['profile_image_url', 'TEXT'],
  96.     ['url',         'TEXT'],
  97.     ['protected',   'TEXT'],
  98.     ['followers_count', 'INTEGER'],
  99.     ['profile_background_color', 'TEXT'],
  100.     ['profile_text_color', 'TEXT'],
  101.     ['profile_link_color', 'TEXT'],
  102.     ['profile_sidebar_fill_color', 'TEXT'],
  103.     ['profile_sidebar_border_color', 'TEXT'],
  104.     ['friends_count',  'INTEGER'],
  105.     ['created_at',     'TEXT'],
  106.     ['favourites_count', 'INTEGER'],
  107.     ['utc_offset',     'INTEGER'],
  108.     ['time_zone',      'TEXT'],
  109.     ['profile_background_image_url', 'TEXT'],
  110.     ['profile_background_tile', 'TEXT'],
  111.     ['statuses_count', 'INTEGER'],
  112.     ['notifications',  'TEXT'],
  113.     ['following',      'TEXT']
  114. ];
  115.  
  116. var tweetsByTime = cmpByKey('created_at_ms');
  117. var tweetsById = cmpByKey('id');
  118.  
  119. function twitterPageLoaded(window)
  120. {
  121.     guard(function() {
  122.     // TODO: account should not be a global on the window.
  123.     var account = window.opener.account;
  124.     account.window = window;
  125.     window.db = window.opener.db;
  126.     window.timeline.account = account;
  127.     var timeline = account.timeline = window.timeline;
  128.     timeline.dataMap = account.tweets;
  129.  
  130.     // update timestamps every minute.
  131.     window.timestampTimer = window.setInterval(function() { timeline.updateTimestamps(); }, 1000 * 60);
  132.  
  133.     // use a temporary function on the window here hardcoded into feed.html's <body onunload> handler
  134.     // until http://dev.jquery.com/ticket/4791 is fixed (jquery child window unloads get registered
  135.     // on parent windows)
  136.     window.feedUnload = function() {
  137.         console.log('unload handler called');
  138.         account.recordReadTweets();
  139.     };
  140.  
  141.     var feedToOpen = window.opener.feedToOpen || 'timeline';
  142.  
  143.     //console.warn('calling changeView from twitterPageLoaded');
  144.     account.changeView(feedToOpen);
  145.     account.notifyFeeds();
  146.     delete window.opener.feedToOpen;
  147.     });
  148. }
  149.  
  150. function TwitterAccount(username, password, opts) {
  151.     if (!username)
  152.         throw "Need username";
  153.  
  154.     //console.warn('TwitterAccount(' + username + ', ' + password + ', ' + oauthToken + ', ' + oauthConsumerSecret + ')');
  155.  
  156.     var self = this;
  157.     this.showAllRealtime = false;
  158.     this.clients = [];
  159.     this.username = username;
  160.     //this.selfScreenNameLower = username.toLowerCase();
  161.     this.password = password;
  162.  
  163.     // OAUTH
  164.     if (opts.oauthTokenKey)
  165.         this.oauth = {
  166.             tokenKey: opts.oauthTokenKey,
  167.             tokenSecret: opts.oauthTokenSecret,
  168.             consumerKey:  opts.oauthConsumerKey,
  169.             consumerSecret: opts.oauthConsumerSecret
  170.         };
  171.  
  172.     this.users = {};   // {user id: user object}
  173.  
  174.     this.sql = {
  175.         'tweets':  new SQLDesc('Tweets',  tweetColumns),
  176.         'users':   new SQLDesc('Users',   userColumns),
  177.         'info':    new SQLDesc('Info',    infoColumns),
  178.         'directs': new SQLDesc('Directs', directColumns)
  179.     };
  180. }
  181.  
  182. function dropAllTables() {
  183.     var tables = objectKeys(window.account.sql);
  184.  
  185.     window.db.transaction(function(tx) {
  186.         console.log('dropping tables: ' + tables.join(', '));
  187.         $.each(tables, function (i, table) {
  188.             executeSql(tx, 'drop table if exists ' + table);
  189.         });
  190.     }, function(error) { console.error(error.message); });
  191. }
  192.  
  193.  
  194. function tweetsEqual(t1, t2) { return t1.id === t2.id; }
  195.  
  196. function errorFunc(msg) {
  197.     return function (err) {
  198.         var extra = '';
  199.         if (err !== undefined && err.message !== undefined)
  200.             extra = ': ' + err.message;
  201.         console.error(msg + extra);
  202.     };
  203. }
  204.  
  205. TwitterAccount.prototype = {
  206.     timerMs: 1000 * 60,
  207.     webRoot: 'http://twitter.com/',
  208.     apiRoot: 'https://api.twitter.com/1/',
  209.     searchApiRoot: 'http://search.twitter.com/',
  210.  
  211.     showTimelinePopup: function(n) {
  212.         var items = this.feeds.timeline.items;
  213.         if (n !== undefined)
  214.             items = items.slice(0, n);
  215.  
  216.         showPopupsForTweets(this, items);
  217.     },
  218.  
  219.     stopTimers: function(reason) {
  220.         console.warn('stopTimers:' + reason);
  221.         if (this.updateTimer)
  222.             clearInterval(this.updateTimer);
  223.     },
  224.  
  225.     maybeUpdate: function(forceUpdate, sources) {
  226.         var self = this;
  227.         var now = new Date().getTime();
  228.         var srcs;
  229.         var checkIdle = false;
  230.  
  231.         if (sources) {
  232.             srcs = sources;
  233.         } else if (forceUpdate) {
  234.             console.warn('forcing update');
  235.             srcs = $.map(self.sources, function(s) { return s.feed.autoUpdates ? s : null; });
  236.         } else {
  237.             checkIdle = true;
  238.             // only update sources whose needsUpdate returns true
  239.             srcs = $.map(self.sources, function(s) { return s.needsUpdate(now) ? s : null; });
  240.         }
  241.  
  242.         if (srcs.length === 0)
  243.             return;
  244.  
  245.         function _doUpdate() {
  246.             if (self.returnFromIdleTimer) {
  247.                 clearInterval(self.returnFromIdleTimer);
  248.                 self.returnFromIdleTimer = undefined;
  249.             }
  250.  
  251.             // any successful update triggers online state
  252.             function success() {
  253.                 self.didUpdate = true;
  254.                 self.notifyClients('didReceiveUpdates');
  255.             }
  256.             console.debug('UpdateNotification being created with ' + srcs.length);
  257.             var updateNotification = new UpdateNotification(self, srcs.length, {
  258.                 success: success,
  259.                 onDone: function(tweets) {
  260.                     self.notifyClients('didReceiveWholeUpdate');
  261.                 },
  262.             });
  263.  
  264.             var markRead = false;
  265.             if (self.firstUpdate) {
  266.                 self.firstUpdate = false;
  267.                 markRead = true;
  268.             }
  269.  
  270.             for (var c = 0; c < srcs.length; ++c) {
  271.                 var src = srcs[c];
  272.                 if (markRead)
  273.                     src.markNextUpdateAsRead();
  274.                 src.update(updateNotification);
  275.             }
  276.         }
  277.  
  278.         if (!checkIdle) {
  279.             _doUpdate();
  280.             return;
  281.         }
  282.  
  283.         D.rpc('get_idle_time', {}, function(args) {
  284.             if (args.idleTime > IDLE_THRESHOLD_MS) {
  285.                 if (!self.returnFromIdleTimer) {
  286.                     // once idle, check idle time every 10 seconds so that
  287.                     // we can update soon after you come back.
  288.                     self.returnFromIdleTimer = setInterval(function() {
  289.                         self.maybeUpdate();
  290.                     }, 10 * 1000);
  291.                 }
  292.             } else _doUpdate();
  293.         }, _doUpdate);
  294.     },
  295.  
  296.     discardOldTweets: function() {
  297.         var self = this,
  298.             idsToDelete = [],
  299.             usersToKeep = {};
  300.  
  301.         var doc, toInsertMap;
  302.         if (this.timeline) {
  303.             doc = this.timeline.container.ownerDocument;
  304.             console.debug('** discardOldTweets this.timeline.toInsert: ' + this.timeline.toInsert);
  305.             if (this.timeline.toInsertMap)
  306.                 toInsertMap = this.timeline.toInsertMap;
  307.         }
  308.  
  309.         doc = this.timeline ? this.timeline.container.ownerDocument : undefined;
  310.  
  311.         //console.warn('** discardOldTweets toInsertMap: ' + toInsertMap);
  312.  
  313.         function shouldDelete(tweet) {
  314.             if (!doc || !doc.getElementById(tweet.id))
  315.                 if (!tweet.feeds || !objectKeys(tweet.feeds).length)
  316.                     if (!toInsertMap || !(tweet.id in toInsertMap))
  317.                         return true;
  318.         }
  319.  
  320.         function discard(items) {
  321.             // discard references to all tweets not in a feed
  322.             $.each(items, function (i, tweet) {
  323.                 if (shouldDelete(tweet))
  324.                     idsToDelete.push(tweet.id);
  325.                 else {
  326.                     $.each(['user', 'recipient_id', 'sender_id'], function (i, attr) {
  327.                         if (tweet[attr])
  328.                             usersToKeep[tweet[attr]] = true;
  329.                     });
  330.                 }
  331.             });
  332.  
  333.             $.each(idsToDelete, function (i, id) {
  334.                 //console.log('DELETING ' + id + ' ' + items[id].text + ' ' + objectLength(items[id].feeds))
  335.                 delete items[id];
  336.             });
  337.         }
  338.  
  339.         discard(this.tweets);
  340.  
  341.         // discard all users not referenced by tweets or directs or groups
  342.  
  343.         $.each(this.getUsersToKeep(), function(i, id) {
  344.             usersToKeep[id] = true;
  345.         });
  346.  
  347.         var usersToDelete = [];
  348.         $.each(this.users, function (id, user) {
  349.             if (user.screen_name.toLowerCase() !== this.selfScreenNameLower)
  350.                 if (!(id in usersToKeep))
  351.                     usersToDelete.push(id);
  352.         });
  353.  
  354.         if (usersToDelete.length) {
  355.             $.each(usersToDelete, function (i, id) { delete self.users[id]; });
  356.         }
  357.     },
  358.  
  359.     forceUpdate: function() {
  360.         this.maybeUpdate(true);
  361.     },
  362.  
  363.     setupTimers: function() {
  364.         var self = this;
  365.         this.updateTimer = setInterval(function() { self.maybeUpdate(); }, this.timerMs);
  366.         this.maybeUpdate();
  367.  
  368.         // update trends periodically.
  369.         this.trendsTimer = setInterval(function() { self.getTrends(); },
  370.                                        1000 * 60 * TRENDS_UPDATE_MINS);
  371.     },
  372.  
  373.     initialize: function(feeds, accountopts) {
  374.         var self = this;
  375.  
  376.         if (accountopts) {
  377.             console.warn('apiRoot: ' + accountopts.apiRoot);
  378.             console.debug(JSON.stringify(accountopts));
  379.             if (accountopts.timeCorrectionSecs !== undefined) {
  380.                 var diffMs = accountopts.timeCorrectionSecs * 1000;
  381.                 var d = new Date(new Date().getTime() - diffMs);
  382.                 receivedCorrectTimestamp(d, true);
  383.             }
  384.  
  385.             if (accountopts.webRoot) self.webRoot = accountopts.webRoot;
  386.             if (accountopts.apiRoot) self.apiRoot = accountopts.apiRoot;
  387.             if (accountopts.searchApiRoot) self.searchApiRoot = accountopts.searchApiRoot;
  388.  
  389.         }
  390.  
  391.         function success(tx) {
  392.             self.initializeFeeds(tx, feeds, accountopts, function() {
  393.                 if (self.offlineMode) {
  394.                     self.didUpdate = true;
  395.                     self.notifyClients('didReceiveUpdates');
  396.                     self.notifyFeeds();
  397.                     self.doNotifyUnread();
  398.                 } else {
  399.                     self.getFollowing(function() {
  400.                         // we don't know which users to discard until we know who we're following.
  401.                         self.discardOldUsers();
  402.                     });
  403.                     self.getTrends();
  404.                     self.setupTimers();
  405.                     self.notifyFeeds();
  406.                 }
  407.             }, function() {
  408.                 self.connectionFailed();
  409.             });
  410.         }
  411.  
  412.         function error(tx, error) {
  413.             console.error('error creating tables: ' + error.message);
  414.         }
  415.  
  416.         account.setupSql(success, error);
  417.     },
  418.  
  419.     refreshFeedItems: function(feed) {
  420.         feed.removeAllItems();
  421.         this.addAllSorted(feed, this.timeline && this.timeline.feed === feed);
  422.     },
  423.  
  424.     addAllSorted: function(feed, deleted) {
  425.         var tweets = objectValues(this.tweets);
  426.         tweets.sort(tweetsByTime);
  427.  
  428.         console.debug('##########\nsorting all tweets for addSorted on ' + feed + ' ' + tweets.length + ' tweets');
  429.         var scroll = this.timeline && this.timeline.feed === this.feeds.timeline;
  430.         feed.addSorted(tweets, undefined, true, {
  431.             scroll: scroll,
  432.             setSorted: deleted,
  433.             account: this
  434.         });
  435.     },
  436.  
  437.     deleteFeed: function(opts) {
  438.         var self = this, name = opts.feedName;
  439.         if (!name) return;
  440.  
  441.         console.log('deleteFeed: ' + name);
  442.  
  443.         var didFind = false;
  444.         $.each(this.customFeeds, function (i, feed) {
  445.             if (feed.name !== name) return;
  446.             didFind = true;
  447.  
  448.             if (feed.source) {
  449.                 if (!arrayRemove(self.sources, feed.source))
  450.                     console.error('did not remove source ' + feed.source);
  451.             }
  452.  
  453.             self.customFeeds.splice(i, 1);
  454.  
  455.             // when deleting a custom feed, the timeline may change
  456.             // b/c of filtering and merging
  457.             var timeline = self.feeds.timeline;
  458.             if (timeline && feed !== timeline) {
  459.                 var deleted = timeline.feedDeleted(feed.serialize(), self.customFeeds);
  460.                 self.addAllSorted(timeline, deleted);
  461.             }
  462.  
  463.             delete self.feeds[name];
  464.             self.notifyFeeds();
  465.             self.doNotifyUnread();
  466.             return false;
  467.         });
  468.  
  469.         if (!didFind) console.warn('deleteFeed ' + name + ' did not find a feed to delete');
  470.     },
  471.  
  472.     uniqueFeedName: function(type) {
  473.         var name;
  474.         var i = 1;
  475.         do { name = type + ':' + i++;
  476.         } while (name in this.feeds);
  477.         return name;
  478.     },
  479.  
  480.     editFeed: function(feedDesc) {
  481.         var name = feedDesc.name; assert(name);
  482.         var feed = this.feeds[name]; assert(feed);
  483.         var timeline = this.feeds.timeline;
  484.  
  485.         timeline.feedDeleted(feed.serialize(), this.customFeeds);
  486.  
  487.         var needsUpdate = feed.updateOptions(feedDesc);
  488.  
  489.         timeline.feedAdded(this, feed.serialize());
  490.  
  491.         if (needsUpdate) {
  492.             this.refreshFeedItems(feed);
  493.             this.addAllSorted(timeline, true);
  494.         }
  495.  
  496.         this.notifyFeeds();
  497.         if (needsUpdate)
  498.             this.doNotifyUnread();
  499.     },
  500.  
  501.     findFeed: function(feedDesc) {
  502.         console.log('findFeed: ' + JSON.stringify(feedDesc));
  503.  
  504.         if (feedDesc['type'] === 'search' && feedDesc['query']) {
  505.             var foundFeed;
  506.             $.each(this.feeds, function(i, feed) {
  507.                 if (feed.query && feed.query === feedDesc['query']) {
  508.                     foundFeed = feed;
  509.                     return false;
  510.                 }
  511.             });
  512.  
  513.             if (foundFeed)
  514.                 return foundFeed;
  515.         }
  516.     },
  517.  
  518.     getAllGroupIds: function() {
  519.         var allIds = [];
  520.         $.each(this.customFeeds, function (i, feed) {
  521.             var userIds = feed.userIds;
  522.             if (userIds)
  523.                 for (var i = 0; i < userIds.length; ++i)
  524.                     allIds.push(userIds[i]);
  525.         });
  526.         return allIds;
  527.     },
  528.  
  529.     addFeed: function(feedDesc, shouldChangeView) {
  530.         console.debug('addFeed(' + JSON.stringify(feedDesc) + ')');
  531.         console.log(JSON.stringify(feedDesc));
  532.  
  533.         var existingFeed = this.findFeed(feedDesc);
  534.         if (existingFeed) {
  535.             console.log('returning existing feed ' + existingFeed);
  536.             if (this.timeline.feed !== existingFeed)
  537.                 this.changeView(existingFeed['name']);
  538.             return existingFeed;
  539.         }
  540.  
  541.         if (shouldChangeView === undefined)
  542.             shouldChangeView = true;
  543.  
  544.         // assign user created feeds a unique name
  545.         if ((feedDesc.type === 'group' || feedDesc.type === 'search' || feedDesc.type === 'user') &&
  546.             feedDesc.name === undefined)
  547.             feedDesc.name = this.uniqueFeedName(feedDesc.type);
  548.  
  549.         if (feedDesc.type === 'search' && !feedDesc.save)
  550.             D.notify('hook', 'digsby.statistics.twitter.new_search')
  551.  
  552.         var self = this,
  553.             feed = createFeed(this.tweets, feedDesc);
  554.  
  555.         if (shouldChangeView)
  556.             this.addAllSorted(feed);
  557.  
  558.         var source = feed.makeSource(self);
  559.         if (source) {
  560.             this.sources.push(source);
  561.             if (shouldChangeView) {
  562.                 var done = function() { self.timeline.finish(); };
  563.                 var obj = {success: done, error: done};
  564.                 console.log('in addFeed, updating source ' + source + ' now');
  565.                 source.update(obj);
  566.             }
  567.         }
  568.  
  569.         this.feeds[feed.name] = feed;
  570.         this.customFeeds.push(feed);
  571.  
  572.         if (shouldChangeView) {
  573.             this.notifyFeeds();
  574.             this.changeView(feed['name']);
  575.         }
  576.  
  577.         if (feed !== this.feeds.timeline) {
  578.             //console.debug('calling feedAdded: ' + JSON.stringify(feedDesc));
  579.             this.feeds.timeline.feedAdded(this, feedDesc);
  580.         } else
  581.             console.warn('feed is timeline, skipping feedAdded');
  582.  
  583.         return feed;
  584.     },
  585.  
  586.     addGroup: function(feedDesc, shouldChangeView) {
  587.         return this.addFeed(feedDesc, shouldChangeView);
  588.     },
  589.  
  590.     // map of {feedName: [account option name, default minutes to update]}
  591.     account_opts_times: {timeline: ['friends_timeline', 2],
  592.                          directs:  ['direct_messages', 10],
  593.                          mentions: ['replies', 2]},
  594.  
  595.     setAccountOptions: function(opts) {
  596.         if (!this.feeds)
  597.             return;
  598.  
  599.         console.warn('setAccountOptions: apiRoot ' + opts.apiRoot);
  600.         if (opts.apiRoot)
  601.             this.apiRoot = opts.apiRoot;
  602.  
  603.         var invite_url = opts.demovideo_link ? opts.demovideo_link : INVITE_URL;
  604.         this.invite_message = INVITE_TEXT + invite_url;
  605.  
  606.         // opts may have values for the updateFrequency of the main feeds.
  607.         var self = this;
  608.         console.debug('setAccountOptions');
  609.         $.each(this.feeds, function (name, feed) {
  610.             if (feed.name in self.account_opts_times) {
  611.                 var feedopts = self.account_opts_times[feed.name];
  612.                 var optName = feedopts[0];
  613.                 var defaultUpdateMins = feedopts[1];
  614.                 var minutes = optName in opts ? opts[optName] : defaultUpdateMins;
  615.  
  616.                 console.debug('feed: ' + feed.name + ' ' + minutes);
  617.                 feed.source.setUpdateFrequency(1000 * 60 * minutes);
  618.             }
  619.         });
  620.  
  621.         this.searchUpdateFrequency = 1000 * 60 * get(opts, 'search_updatefreq', 2);
  622.  
  623.         settings.autoscroll_when_at_bottom = get(opts, 'autoscroll_when_at_bottom', true);
  624.  
  625.         this.offlineMode = opts.offlineMode;
  626.         if (this.offlineMode)
  627.             console.warn('twitter is in offline mode');
  628.     },
  629.  
  630.     initializeFeeds: function(tx, userFeeds, opts, success, error) {
  631.         var self = this;
  632.  
  633.         if (!this.selfUser && !this.selfScreenNameLower) {
  634.             // if we haven't looked up our own id yet, then do so first
  635.             this.verifyCredentials(function() { self.initializeFeeds(undefined, userFeeds, opts, success, error); },
  636.                                    error);
  637.             return;
  638.         }
  639.  
  640.         var tweets = this.tweets = {};
  641.  
  642.         // the base set of all feeds
  643.         this.customFeeds = [];
  644.         var feeds = this.feeds = {
  645.             favorites: new TwitterFavoritesFeedModel(tweets),
  646.             history: new TwitterHistoryFeedModel(tweets, {screen_name: this.selfScreenNameLower})
  647.         };
  648.  
  649.         self.sources = $.map(objectValues(feeds), function(f) { return f.makeSource(self) || null; });
  650.         console.warn('created ' + self.sources.length + ' self.sources');
  651.  
  652.         if (userFeeds.length) {
  653.             // Construct feeds here. Take care to construct the main timeline
  654.             // view first, since it needs to be around when we construct the other
  655.             // feeds, which may affect it via filtering and merging.
  656.             var userFeedsRearranged = Array.apply(null, userFeeds);
  657.             userFeedsRearranged.sort(function(a, b) {
  658.                 // timeline goes first
  659.                 return -cmp(a.type==='timeline', b.type==='timeline');
  660.             });
  661.             $.each(userFeedsRearranged, function (i, feedOpts) { self.addFeed(feedOpts, false); });
  662.  
  663.             // Rearrange the feeds back into their original user order.
  664.             this.setFeeds(userFeeds);
  665.         }
  666.  
  667.         this.setAccountOptions(opts);
  668.  
  669.         var successCount = 0;
  670.         function _success(tablename, tweets) {
  671.             console.log('loaded ' + tweets.length + ' cached tweets');
  672.             assert(feeds);
  673.             self.newTweets(tweets);
  674.  
  675.             var toDeleteIds = [];
  676.  
  677.             assert(tweets);
  678.  
  679.             // discard any tweets that we pulled out of the cache, but
  680.             // not added to any timelines
  681.             $.each(tweets, function (i, tweet) {
  682.                 if (!tweet.feeds || !objectKeys(tweet.feeds).length)
  683.                     toDeleteIds.push(tweet.id);
  684.             });
  685.  
  686.             console.debug('discarding ' + toDeleteIds.length + ' ' + tablename);
  687.             self.discard(tablename, toDeleteIds);
  688.             if (++successCount === 2 && success)
  689.                 success();
  690.         }
  691.  
  692.         function _error(err) {
  693.             console.error('error: ' + err.message);
  694.             if (error) error(err);
  695.         }
  696.  
  697.         self.loadAllCached({tx: tx,
  698.                             success: _success,
  699.                             error: _error});
  700.     },
  701.  
  702.     setFeeds: function(newFeeds) {
  703.         // rearrange feeds
  704.         var feeds = this.feeds;
  705.         this.customFeeds = $.map(newFeeds, function(f) {
  706.             return feeds[f.name];
  707.         });
  708.         this.notifyFeeds();
  709.     },
  710.  
  711.     changeView: function (feed) {
  712.         console.warn('changeView(' + JSON.stringify(feed) + ')');
  713.         console.warn('  typeof feed is ' + (typeof feed));
  714.  
  715.         if (typeof feed !== 'string') {
  716.             // can be a new feed description
  717.             console.log('got a feed description, calling addFeed');
  718.             feed = this.addFeed(feed, true);
  719.             return;
  720.         }
  721.  
  722.         var self = this;
  723.  
  724.         var timeline = this.timeline;
  725.         timeline.pauseMarkAsRead(true);
  726.         timeline.viewChanged(feed);
  727.  
  728.         timeline.pauseMarkAsRead(false, function() {
  729.             if (self.feeds[feed].scrollToBottom)
  730.                 // history and favorites start scrolled to the bottom
  731.                 timeline.scrollToBottom();
  732.             else
  733.                 timeline.scrollToNew();
  734.         });
  735.  
  736.         console.warn('changeView: ' + feed);
  737.  
  738.         // switching away from a non-saved search feed deletes it.
  739.         var toDelete = [];
  740.         $.each(this.feeds, function (i, f) {
  741.             if (f.query && !f.save && f.name !== feed)
  742.                 toDelete.push(f.name);
  743.         });
  744.  
  745.         if (toDelete.length)
  746.             $.each(toDelete, function (i, name) { self.deleteFeed({feedName: name}); });
  747.  
  748.         this.notifyClients('viewChanged', feed);
  749.     },
  750.  
  751.     timelineClosing: function() {
  752.         console.log('timelineClosing');
  753.         var self = this;
  754.         var feed = this.timeline.feed;
  755.         if (feed.query && !feed.save) {
  756.             console.log('yes');
  757.             setTimeout(function() { self.deleteFeed({feedName: feed.name}); }, 50);
  758.         }
  759.     },
  760.  
  761.     setupSql: function (success, error) {
  762.         function _success(tx) {
  763.             executeSql(tx,
  764.                 'create table if not exists info_keyed (' +
  765.                 'key text primary key, ' +
  766.                 'value text)', [],
  767.                 success, error);
  768.         }
  769.  
  770.         var self = this;
  771.         var makeTables = function (tx, sqls) {
  772.             if (sqls.length === 0)
  773.                 _success(tx);
  774.             else
  775.                 sqls[0].ensureTableExists(tx, function (tx, result) {
  776.                     makeTables(tx, sqls.slice(1));
  777.                 }, error);
  778.         };
  779.  
  780.         function didCheckForExistingTweets(tx) {
  781.             makeTables(tx, objectValues(self.sql));
  782.         }
  783.  
  784.         // Assume that if the "Tweets" table does not exist, this is a new account. We'll mark
  785.         // tweets from the first update as already read.
  786.         window.db.transaction(function (tx) {
  787.             executeSql(tx, "select * from Tweets limit 1", [],
  788.                 didCheckForExistingTweets,
  789.                 function (tx, err) { self.firstUpdate = true; didCheckForExistingTweets(tx); }
  790.             );
  791.         });
  792.     },
  793.  
  794.     isSelfTweet: function(t) {
  795.         var user = this.users[t.user];
  796.         if (user) {
  797.             if (this.selfUser)
  798.                 return user.id === this.selfUser.id;
  799.             else
  800.                 return user.screen_name.toLowerCase() === this.selfScreenNameLower;
  801.         }
  802.     },
  803.  
  804.     isMention: function(t) {
  805.         // consider tweets with your @username not at the beginning
  806.         // still a mention
  807.         return (
  808.             t.text.toLowerCase().search('@' + this.selfScreenNameLower) !== -1)
  809.             ? true : false;
  810.     },
  811.  
  812.     isDirect: function(t) {
  813.         return t.recipient_id ? true : false;
  814.     },
  815.  
  816.     verifyCredentials: function(done, onError, retried) {
  817.         var self = this;
  818.  
  819.         loadServerTime(function() {
  820.  
  821.             function onJSON(json) {
  822.                 self.selfUser = self.users[json.id] = self.sql.users.fromJSON(json);
  823.                 self.selfScreenNameLower = self.selfUser.screen_name.toLowerCase();
  824.                 if (done) done();
  825.             }
  826.  
  827.             function cacheCredentials(data) {
  828.                 var json = JSON.stringify(data);
  829.                 console.log('caching credentials');
  830.                 window.db.transaction(function(tx) {
  831.                     executeSql(tx, "insert or replace into info_keyed values (?, ?)",
  832.                                   [self.username, json]);
  833.                 }, errorFunc('error caching credentials'));
  834.             }
  835.  
  836.             function doVerifyRequest() {
  837.                 var url = self.apiRoot + 'account/verify_credentials.json';
  838.                 self.urlRequest(url,
  839.                     function (data) {
  840.                         console.log('verify_credentials call returned successfully');
  841.                         cacheCredentials(data);
  842.                         onJSON(data);
  843.                     },
  844.                     function (error) {
  845.                         console.error('could not verify credentials: ' + error.message);
  846.                         if (retried) {
  847.                             console.error('onError is ' + onError);
  848.                             if (onError) onError();
  849.                         } else {
  850.                             console.error('retrying in 1min.');
  851.                             setTimeout(function() {
  852.                                 self.verifyCredentials(done, onError, true);
  853.                             }, 1000 * 60);
  854.                         }
  855.                     }
  856.                 );
  857.             }
  858.  
  859.             console.log('checking for credentials in database.');
  860.             window.db.transaction(function(tx) {
  861.                 function success(tx, result) {
  862.                     if (result.rows.length) {
  863.                         onJSON(JSON.parse(result.rows.item(0).value));
  864.                     } else {
  865.                         doVerifyRequest();
  866.                     }
  867.                 }
  868.                 executeSql(tx, "select value from info where key == ?",
  869.                     [self.username], success, doVerifyRequest);
  870.             });
  871.         });
  872.     },
  873.  
  874.     getTweetType: function(t) {
  875.         var tweet_type = 'timeline';
  876.  
  877.         if (this.isSelfTweet(t))
  878.             tweet_type = 'sent';
  879.         else if (t.mention)
  880.             tweet_type = 'mention';
  881.         else if (t.search)
  882.             tweet_type = 'search';
  883.         else if (t.sender_id)
  884.             tweet_type = 'direct';
  885.         return tweet_type;
  886.     },
  887.  
  888.     getFollowingUsers: function() {
  889.         var self = this,
  890.             url = this.apiRoot + 'statuses/friends.json';
  891.         this.pagedUrlRequest(url, {
  892.             itemsKey: 'users',
  893.             success: function (users) {
  894.                 if (!users.length)
  895.                     console.warn('no users returned in success callback for getFollowingUsers');
  896.                 else
  897.                     // save users out to database.
  898.                     window.db.transaction(function (tx) {
  899.                         $.each(users, function(i, user) {
  900.                             self.sql.users.insertOrReplace(tx, user);
  901.                         });
  902.                     }, function (err) {
  903.                         console.error('error caching users following: ' + err);
  904.                     });
  905.             },
  906.             error: function (xhr, error) {
  907.                 console.error('error getting users we are following: ' + error);
  908.             }
  909.         });
  910.     },
  911.  
  912.     getFollowing: function(opts) {
  913.         var opts = opts || {};
  914.  
  915.         var self = this;
  916.         if (!this.didGetFollowing) {
  917.             this.didGetFollowing = true;
  918.             var followingUrl = this.apiRoot + 'friends/ids.json';
  919.             function success(data) {
  920.                 self.notifyClients('didReceiveFollowing', data);
  921.                 var following = JSON.parse('['+data+']');
  922.                 self.maybeGetAllUsers(following);
  923.                 self.following_ids = set(following);
  924.                 if (opts.success)
  925.                     opts.success();
  926.             }
  927.             this.urlRequest(followingUrl, success, errorFunc('retreiving IDs the user is following'),
  928.                             {dataType: undefined});
  929.         }
  930.     },
  931.  
  932.     maybeGetAllUsers: function(following) {
  933.         // if there are any IDs in following that we don't have users for, go
  934.         // get them from the network.
  935.         var self = this, missing = false;
  936.         $.each(following, function (i, id) {
  937.             if (!(id in self.users)) {
  938.                 self.getFollowingUsers();
  939.                 return false;
  940.             }
  941.         });
  942.     },
  943.  
  944.     getTrends: function() {
  945.         var self = this;
  946.         function success(data) { self.notifyClients('didReceiveTrends', data); }
  947.         var url = this.searchApiRoot + 'trends/current.json';
  948.         this.urlRequest(url, success, errorFunc('error retrieving trends'));
  949.     },
  950.  
  951.     onRealTimeTweet: function(tweetjson) {
  952.         var self = this;
  953.  
  954.         if ('delete' in tweetjson)
  955.             return this.onRealTimeDeleteTweet(tweetjson);
  956.  
  957.         // realtime tweet stream also includes @replys to people in the following
  958.         // list, so filter out any tweets from users not in our following_ids set
  959.         if (this.showAllRealtime ||
  960.             (this.following_ids && tweetjson.user.id in this.following_ids)) {
  961.  
  962.             // filter out replies from people we're following to people we are
  963.             // NOT following, to match the website. (settting?)
  964.             var reply_to = tweetjson.in_reply_to_user_id;
  965.             if (!reply_to || reply_to in this.following_ids) {
  966.                 var tweet = this.makeTweetFromNetwork(tweetjson);
  967.                 assert(tweet !== undefined);
  968.                 self.appendTweet(tweet);
  969.                 if (!this.isSelfTweet(tweet))
  970.                     showPopupsForTweets(this, [tweet]);
  971.                 this.cacheTweets([tweet]);
  972.                 this.doNotifyUnread();
  973.             }
  974.         }
  975.     },
  976.  
  977.     onRealTimeDeleteTweet: function(tweetjson) {
  978.         console.log('TODO: streaming API sent delete notice for status ' + tweetjson.status.id);
  979.     },
  980.  
  981.     getUsers: function(opts, justFollowing) {
  982.         if (justFollowing === undefined)
  983.             justFollowing = true;
  984.  
  985.         // if justFollowing is true (the default), only return users with ids
  986.         // in this.following_ids
  987.         var usersMap;
  988.         if (justFollowing && this.following_ids) {
  989.             usersMap = {};
  990.             var followingIds = this.following_ids;
  991.             $.each(this.users, function (id, user) {
  992.                 if (id in followingIds) usersMap[id] = user;
  993.             });
  994.         } else
  995.             usersMap = this.users;
  996.  
  997.         if (opts.success)
  998.             opts.success(usersMap);
  999.     },
  1000.  
  1001.     addClient: function(client) {
  1002.         this.clients.push(client);
  1003.     },
  1004.  
  1005.     changeState: function(state) {
  1006.         console.warn('changeState: ' + state);
  1007.  
  1008.         if (state === 'oautherror' && this.state !== 'oautherror') {
  1009.             this.state = state;
  1010.             this.notifyClients('stateChanged', 'oautherror');
  1011.             this.stopTimers('oautherror');
  1012.         } else if (state === 'autherror') {
  1013.             this.state = state;
  1014.             this.notifyClients('stateChanged', 'autherror');
  1015.             this.stopTimers('autherror');
  1016.         } else if (state == 'connfail') {
  1017.             if (this.state !== 'autherror') { // do not go from autherror to connfail
  1018.                 this.state = state;
  1019.                 this.notifyClients('stateChanged', 'connfail');
  1020.                 this.stopTimers('connection fail');
  1021.             }
  1022.         } else if (state == 'online') {
  1023.             if (this.state === undefined) {
  1024.                 this.state = 'online';
  1025.                 this.notifyClients('stateChanged', 'online');
  1026.             }
  1027.         } else {
  1028.             console.error('changeState got unknown state: ' + state);
  1029.         }
  1030.     },
  1031.  
  1032.     notifyClients: function(funcName/*, *args */) {
  1033.         var args = Array.apply(null, arguments);
  1034.         args.shift();
  1035.  
  1036.         $.each(this.clients, function(i, client) {
  1037.             client[funcName].apply(client, args);
  1038.         });
  1039.     },
  1040.  
  1041.     notifyFeeds: function(name) {
  1042.         var feedsJSON = $.map(this.customFeeds, function (feed) {
  1043.             return feed.serialize ? feed.serialize() : null;
  1044.         });
  1045.         this.notifyClients(name || 'feedsUpdated', feedsJSON);
  1046.     },
  1047.  
  1048.     getTweet: function(id, success, error) {
  1049.         var self = this;
  1050.  
  1051.         // first, try loading the tweet from the cache
  1052.         var cacheSuccess = function(tx, tweets) {
  1053.             if (tweets.length !== 1) {
  1054.                 console.log('cacheSuccess expected one tweet, got ' + tweets.length);
  1055.                 return loadFromNetwork();
  1056.             }
  1057.             success(tweets[0]);
  1058.         };
  1059.  
  1060.         var cacheError = function(err) {
  1061.             console.error('could not retreive tweet ' + id + ' from the database: ' + err.message);
  1062.             loadFromNetwork();
  1063.         };
  1064.  
  1065.         // if that fails, grab it from the network
  1066.         var loadFromNetwork = function() {
  1067.             function urlSuccess(data) {
  1068.                 var tweet = self.makeTweetFromNetwork(data);
  1069.                 self.cacheTweets([tweet], function() { success(tweet); });
  1070.             }
  1071.  
  1072.             var url = self.apiRoot + 'statuses/show/' + id + '.json';
  1073.             self.urlRequest(url, urlSuccess, error);
  1074.         };
  1075.  
  1076.         this.loadCachedTweets({id: id, limit: 1, success: cacheSuccess, error: cacheError});
  1077.     },
  1078.  
  1079.     /**
  1080.      * Handles X-RateLimit- HTTP response headers indicating API request
  1081.      * limits.
  1082.      */
  1083.     handleRateLimits: function(xhr, textStatus) {
  1084.         var r = {limit:     xhr.getResponseHeader('X-RateLimit-Limit'),
  1085.                  remaining: xhr.getResponseHeader('X-RateLimit-Remaining'),
  1086.                  reset:     xhr.getResponseHeader('X-RateLimit-Reset')};
  1087.  
  1088.         var dateStr = xhr.getResponseHeader('Date');
  1089.         if (dateStr) {
  1090.             var date = new Date(dateStr);
  1091.             if (date) receivedCorrectTimestamp(date, true);
  1092.         }
  1093.  
  1094.  
  1095.         if (r.limit && r.remaining && r.reset) {
  1096.             this.rateLimit = r;
  1097.             if (parseInt(r.remaining, 10) < 20)
  1098.                 console.log('remaining API requests: ' + r.remaining);
  1099.         }
  1100.     },
  1101.  
  1102.     /**
  1103.      * Sends a tweet or direct.
  1104.      *   replyTo   can be an id that the tweet is in reply to
  1105.      *   success   is called with the tweet object
  1106.      *   error     is called with an exception object
  1107.      */
  1108.     tweet: function(text, replyTo, success, error) {
  1109.         // make "d screen_name message" a direct message
  1110.         var direct = text.match(/^d\s+(\S+)\s+(.+)/);
  1111.         if (direct) {
  1112.             var screen_name = direct[1];
  1113.             if (screen_name.charAt(screen_name.length-1) === ':')
  1114.                 screen_name = screen_name.slice(0, screen_name.length-1);
  1115.             text = direct[2];
  1116.             return this.direct(screen_name, text, success, error);
  1117.         }
  1118.  
  1119.         var self = this;
  1120.         var opts = {status: text,
  1121.                     source: 'digsby'};
  1122.         if (replyTo)
  1123.             opts.in_reply_to_status_id = replyTo;
  1124.  
  1125.         var url = this.apiRoot + 'statuses/update.json';
  1126.  
  1127.         function _success(data) {
  1128.             self.notifyClients('statusUpdated');
  1129.             var tweet = self.makeTweetFromNetwork(data);
  1130.             self.appendTweet(tweet, {ignoreIds: true});
  1131.             self.cacheTweets([tweet], function() {
  1132.                 var t = notifyTweet(self, tweet);
  1133.                 self.notifyClients('selfTweet', t);
  1134.                 if (success) success(t);
  1135.             });
  1136.         }
  1137.  
  1138.         this.urlRequest(url, _success, error, {type: 'POST', data: opts});
  1139.     },
  1140.  
  1141.     direct: function(screen_name, text, success, error) {
  1142.         var self = this,
  1143.             opts = {text: text, screen_name: screen_name},
  1144.             url = this.apiRoot + 'direct_messages/new.json';
  1145.  
  1146.         var _success = function(data) {
  1147.             self.notifyClients('directSent');
  1148.             var direct = self.makeDirectFromNetwork(data);
  1149.             self.appendTweet(direct);
  1150.             self.cacheDirects([direct], function() {
  1151.                 if (success) success(notifyTweet(self, direct));
  1152.             });
  1153.         };
  1154.  
  1155.         this.urlRequest(url, _success, extractJSONError('Error sending direct message', error), {type: 'POST', data: opts});
  1156.     },
  1157.  
  1158.     deleteTweet: function(opts) {
  1159.         return this.deleteItem('tweets', 'statuses/destroy/', opts);
  1160.     },
  1161.  
  1162.     deleteDirect: function(opts) {
  1163.         return this.deleteItem('directs', 'direct_messages/destroy/', opts);
  1164.     },
  1165.  
  1166.     deleteItem: function(sql, urlPart, opts) {
  1167.         var self = this;
  1168.  
  1169.         function success(tweet) {
  1170.             useStringIdentifiers(tweet, ['id', 'in_reply_to_status_id']);
  1171.             console.log('successfully deleted ' + sql + ' ' + tweet.id);
  1172.             self.discard(sql, [tweet.id]); // remove from sql cache
  1173.             delete self.tweets[tweet.id];  // remove from memory cache
  1174.             // remove from any feeds.
  1175.             $.each(self.feeds, function (i, feed) { feed.removeItem(tweet); });
  1176.  
  1177.             if (opts.success) {
  1178.                 // scroll to bottom if necessary.
  1179.                 if (self.timeline)
  1180.                     self.timeline.scrollToBottomIfAtBottom(function() { opts.success(tweet); });
  1181.                 else
  1182.                     opts.success(tweet);
  1183.             }
  1184.         }
  1185.  
  1186.         return this.urlRequest(this.apiRoot + urlPart + opts.id + '.json',
  1187.                                success,
  1188.                                opts.error,
  1189.                                {type: 'POST'});
  1190.     },
  1191.  
  1192.     follow: function(opts) {
  1193.         console.warn('follow: ' + JSON.stringify(opts));
  1194.  
  1195.         if (opts.screen_name) {
  1196.             function success(data) {
  1197.                 console.warn('follow success:');
  1198.                 console.warn(JSON.stringify(data));
  1199.                 if (opts.success)
  1200.                     opts.success();
  1201.             }
  1202.  
  1203.             function error(err) {
  1204.                 console.error('error following' + opts.screen_name);
  1205.                 printException(err);
  1206.                 if (opts.error) opts.error(err);
  1207.             }
  1208.  
  1209.             this.urlRequest(this.apiRoot + 'friendships/create.json',
  1210.                             success, opts.error || errorFunc('error following'),
  1211.                             {type: 'POST', data: {screen_name: opts.screen_name}});
  1212.         }
  1213.     },
  1214.  
  1215.     appendTweet: function(tweet, opts) {
  1216.         if (this.timeline) {
  1217.             var self = this;
  1218.             this.timeline.scrollToBottomIfAtBottom(function() {
  1219.                 opts = opts || {};
  1220.                 opts.scroll = false;
  1221.                 self.newTweets([tweet], undefined, true, opts);
  1222.             });
  1223.         }
  1224.     },
  1225.  
  1226.     newTweets: function (tweets, source, updateNow, opts) {
  1227.         if (opts === undefined) opts = {};
  1228.         opts.account = this;
  1229.         callEach(this.feeds, 'addSorted', tweets, source, updateNow, opts);
  1230.         var timeline = this.feeds.timeline;
  1231.         if (this.lastNotifiedMaxId === undefined ||
  1232.             this.lastNotifiedMaxId !== timeline.source.maxId) {
  1233.             var recentTweets = timeline.items.slice(-200);
  1234.             this.notifyClients('recentTimeline', recentTweets);
  1235.         }
  1236.     },
  1237.  
  1238.     /**
  1239.      * twitter API has paged requests using a "cursor" parameter
  1240.      */
  1241.     pagedUrlRequest: function(url, opts) {
  1242.         if (opts.itemsKey === undefined)
  1243.             throw 'pagedUrlRequest: must provide "itemsKey" in opts'
  1244.  
  1245.         var self = this,
  1246.             nextCursor = -1;
  1247.             allData = [];
  1248.  
  1249.         function _success(data) {
  1250.             // append data
  1251.             arrayExtend(allData, data[opts.itemsKey]);
  1252.             nextCursor = data.next_cursor;
  1253.  
  1254.             if (nextCursor === 0 || nextCursor === undefined) // next_cursor 0 means we hit the end
  1255.                 return opts.success(allData);
  1256.             else
  1257.                 nextRequest();
  1258.         }
  1259.  
  1260.         function nextRequest() {
  1261.             opts.data = {cursor: nextCursor};
  1262.             self.urlRequest(url, _success, opts.error, opts);
  1263.         }
  1264.  
  1265.         nextRequest();
  1266.     },
  1267.  
  1268.     /* currently unused */
  1269.     getFollowedBy: function(_success) {
  1270.         var self = this;
  1271.         var url = this.apiRoot + 'followers/ids.json';
  1272.         this.pagedUrlRequest(url, {
  1273.             itemsKey: 'ids',
  1274.             success: function (ids) {
  1275.                 self.followers = set(ids);
  1276.                 if (_success)
  1277.                     _success(ids);
  1278.             },
  1279.             error: errorFunc('retreiving followers')
  1280.         });
  1281.     },
  1282.  
  1283.     urlRequest: function(url, success, error, opts) {
  1284.         var self = this;
  1285.         opts = opts || {};
  1286.  
  1287.         function _error(xhr, textStatus, errorThrown) {
  1288.             if (xhr.status === 401) {
  1289.                 console.error('authentication error');
  1290.  
  1291.                 if (!self.didUpdate) {
  1292.                     if (self.oauth)
  1293.                         self.changeState('oautherror');
  1294.                     else
  1295.                         // ignore auth errors after already connected
  1296.                         self.changeState('autherror');
  1297.                 }
  1298.             }
  1299.  
  1300.             if (error)
  1301.                 error(xhr, textStatus, errorThrown);
  1302.         }
  1303.  
  1304.         var httpType = opts.type || 'GET';
  1305.         console.log(httpType + ' ' + url);
  1306.         if (opts.data)
  1307.             console.log(JSON.stringify(opts.data));
  1308.  
  1309.         var basicAuthUsername, basicAuthPassword;
  1310.         var authHeader;
  1311.  
  1312.         if (url.substr(0, this.searchApiRoot.length) !== this.searchApiRoot) {
  1313.             if (this.oauth) {
  1314.                 if (!opts.data) opts.data = {};
  1315.                 authHeader = this.getOAuthHeader({url: url, method: httpType, data: opts.data});
  1316.             } else {
  1317.                 basicAuthUsername = encodeURIComponent(this.username);
  1318.                 basicAuthPassword = encodeURIComponent(this.password);
  1319.             }
  1320.         }
  1321.  
  1322.         return $.ajax({
  1323.             url: url,
  1324.             success: success,
  1325.             error: _error,
  1326.             type: httpType,
  1327.             data: opts.data,
  1328.  
  1329.             dataType: opts.dataType || 'json',
  1330.             dataFilter: function (data, type) {
  1331.                 var obj;
  1332.                 if (type.toLowerCase() === 'json') {
  1333.                     try {
  1334.                         obj = JSON.parse(data);
  1335.                     } catch (err) {
  1336.                         // show what the bad data was, if it wasn't JSON
  1337.                         console.error('ERROR parsing JSON response, content was:');
  1338.                         console.error(data);
  1339.                         throw err;
  1340.                     }
  1341.  
  1342.                     return obj;
  1343.                 } else
  1344.                     return data;
  1345.             },
  1346.  
  1347.             username: basicAuthUsername,
  1348.             password: basicAuthPassword,
  1349.  
  1350.             complete: this.handleRateLimits,
  1351.             beforeSend: function(xhr) {
  1352.                 xhr.setRequestHeader('User-Agent', 'Digsby');
  1353.                 if (authHeader)
  1354.                     xhr.setRequestHeader('Authorization', authHeader);
  1355.             }
  1356.         });
  1357.     },
  1358.  
  1359.     getOAuthHeader: function(opts) {
  1360.         var paramList = [];
  1361.         for (var k in opts.data)
  1362.             paramList.push([k, opts.data[k]]);
  1363.  
  1364.         paramList.sort(function(a, b) {
  1365.             if (a[0] < b[0]) return -1;
  1366.             if (a[0] > b[0]) return 1;
  1367.             return 0;
  1368.         });
  1369.  
  1370.         var message = {
  1371.             action: opts.url,
  1372.             method: opts.method,
  1373.             parameters: paramList
  1374.         };
  1375.  
  1376.         var accessor = {
  1377.             consumerKey: this.oauth.consumerKey,
  1378.             consumerSecret: this.oauth.consumerSecret,
  1379.  
  1380.             token: this.oauth.tokenKey,
  1381.             tokenSecret: this.oauth.tokenSecret
  1382.         };
  1383.  
  1384.         OAuth.completeRequest(message, accessor);
  1385.         return OAuth.getAuthorizationHeader(undefined, message.parameters)
  1386.     },
  1387.  
  1388.     favorite: function(tweet, onSuccess, onError) {
  1389.         var self = this;
  1390.         var url, favoriting;
  1391.         if (tweet.favorited) {
  1392.             url = this.apiRoot + 'favorites/destroy/' + tweet.id + '.json';
  1393.             favoriting = false;
  1394.         } else {
  1395.             url = this.apiRoot + 'favorites/create/' + tweet.id + '.json';
  1396.             favoriting = true;
  1397.         }
  1398.  
  1399.         // TODO: this is the same as getTweet
  1400.         function urlSuccess(data) {
  1401.             var tweet = self.makeTweetFromNetwork(data);
  1402.             tweet.favorited = favoriting;
  1403.             self.cacheTweets([tweet], function() { if (onSuccess) {onSuccess(tweet);} });
  1404.         }
  1405.  
  1406.         this.urlRequest(url, urlSuccess, onError, {type: 'POST'});
  1407.     },
  1408.  
  1409.     cacheFavorited: function(tweetIds, favorited) {
  1410.         var ids = '(' + joinSingleQuotedStringArray(tweetIds, ', ') + ')';
  1411.         var sql = 'update or ignore tweets set favorited = ' + (favorited ? '1' : '0') + ' where id in ' + ids;
  1412.         window.db.transaction(function(tx) {
  1413.             executeSql(tx, sql, [], null, errorFunc('error unfavoriting items'));
  1414.         });
  1415.     },
  1416.  
  1417.     makeTweetFromNetwork: function(item, markRead) {
  1418.         useStringIdentifiers(item, ['id', 'in_reply_to_status_id']);
  1419.  
  1420.         if (item.id in this.tweets)
  1421.             return this.tweets[item.id];
  1422.  
  1423.         item = transformRetweet(item);
  1424.  
  1425.         var tweet = this.makeTweet(item);
  1426.         var user = tweet.user;
  1427.         // tweets from network have full User JSON
  1428.         this.users[user.id] = user;
  1429.         // but we'll just store ID in memory and on disk
  1430.         tweet.user = user.id;
  1431.         // categorize mentions as such if they contain @username
  1432.         tweet.mention = this.isMention(tweet);
  1433.  
  1434.         if (this.isSelfTweet(tweet)) {
  1435.             this.maybeNotifySelfTweet(tweet);
  1436.             tweet.read = 1;
  1437.         } else if (markRead) {
  1438.             tweet.read = 1;
  1439.         } else {
  1440.             tweet.read = 0;
  1441.         }
  1442.  
  1443.         return tweet;
  1444.     },
  1445.  
  1446.     makeDirect: function(item, override) {
  1447.         var direct = this.sql.directs.fromJSON(item, override);
  1448.         this.tweets[direct.id] = direct;
  1449.         direct.toString = directToString;
  1450.         direct.created_at_ms = new Date(direct.created_at).getTime();
  1451.         direct.user = direct.sender_id;
  1452.         return direct;
  1453.     },
  1454.  
  1455.     makeDirectFromNetwork: function(item, markRead) {
  1456.         useStringIdentifiers(item, ['id']);
  1457.         if (item.id in this.tweets)
  1458.             return this.tweets[item.id];
  1459.         var direct = this.makeDirect(item);
  1460.  
  1461.         this.users[direct.user] = item.sender;
  1462.         direct.read = (markRead || this.isSelfTweet(direct)) ? 1 : 0;
  1463.  
  1464.         return direct;
  1465.     },
  1466.  
  1467.     makeTweet: function(item, override) {
  1468.         var tweet = this.sql.tweets.fromJSON(item, override);
  1469.         this.tweetIn(tweet);
  1470.         return tweet;
  1471.     },
  1472.  
  1473.     tweetIn: function(tweet) {
  1474.         var oldTweet = this.tweets[tweet.id];
  1475.         if (oldTweet && oldTweet.read !== undefined)
  1476.             tweet.read = oldTweet.read;
  1477.         this.tweets[tweet.id] = tweet;
  1478.         tweet.toString = tweetToString;
  1479.         tweet.created_at_ms = new Date(tweet.created_at).getTime();
  1480.     },
  1481.  
  1482.     cacheTweets: function (tweets, ondone) {
  1483.         return this.cacheItems(tweets, ondone, this.sql.tweets);
  1484.     },
  1485.  
  1486.     cacheDirects: function (directs, ondone) {
  1487.         return this.cacheItems(directs, ondone, this.sql.directs);
  1488.     },
  1489.  
  1490.     cacheItems: function (tweets, ondone, sql) {
  1491.         if (tweets.length === 0) {
  1492.             if (ondone) guard(ondone);
  1493.             return;
  1494.         }
  1495.  
  1496.         console.log("cacheItems() is saving " + tweets.length + " items to " + sql);
  1497.  
  1498.         var self = this;
  1499.         window.db.transaction(function(tx) {
  1500.             // insert tweets
  1501.             $.each(tweets, function (i, tweet) {
  1502.  
  1503.                 sql.insertOrReplace(tx, tweet);
  1504.             });
  1505.  
  1506.             // update users
  1507.             $.each(tweets, function (i, tweet) {
  1508.                 if (tweet.user !== null)
  1509.                     self.sql.users.insertOrReplace(tx, self.users[tweet.user]);
  1510.             });
  1511.  
  1512.         }, function (error) {
  1513.             console.error('Failed to cache tweets to database: ' + error.message);
  1514.             if (ondone) ondone();
  1515.         }, function () {
  1516.             if (ondone) ondone();
  1517.         });
  1518.     },
  1519.  
  1520.     discard: function(tablename, ids) {
  1521.         if (ids.length === 0) return;
  1522.         ids = '(' + joinSingleQuotedStringArray(ids, ', ') + ')';
  1523.         var deleteStatement = 'delete from ' + tablename + ' where id in ' + ids;
  1524.         window.db.transaction(function (tx) {
  1525.             executeSql(tx, deleteStatement, [], null, errorFunc('discarding ' + tablename));
  1526.         });
  1527.     },
  1528.  
  1529.     markAllAsRead: function() {
  1530.         this.markTweetsAsRead(this.tweets);
  1531.     },
  1532.  
  1533.     markFeedAsRead: function(name) {
  1534.         if (name in this.feeds)
  1535.             this.markTweetsAsRead(this.feeds[name].items);
  1536.     },
  1537.  
  1538.     toggleAddsToCount: function(name) {
  1539.         var feed = this.feeds[name];
  1540.         if (feed) {
  1541.             feed.noCount = !feed.noCount;
  1542.             this.notifyFeeds();
  1543.             this.doNotifyUnread();
  1544.         }
  1545.     },
  1546.  
  1547.     markTweetsAsRead: function(tweets) {
  1548.         var self = this;
  1549.         $.each(tweets, function (i, t) { self.markAsRead(t); });
  1550.     },
  1551.  
  1552.     markAsRead: function(item) {
  1553.         var self = this;
  1554.  
  1555.         if (!item || item.read) return;
  1556.         var id = item.id;
  1557.  
  1558.         //console.log('markAsRead: ' + item.text);
  1559.         item.read = 1;
  1560.         if (item.feeds)
  1561.             $.each(item.feeds, function(i, feed) { feed.markedAsRead(item); });
  1562.  
  1563.         self.doNotifyUnread();
  1564.  
  1565.         if (this.markAsReadLater === undefined)
  1566.             this.markAsReadLater = {};
  1567.  
  1568.         this.markAsReadLater[id] = true;
  1569.  
  1570.         if (this.markAsReadTimer === undefined) {
  1571.             var unreadTimerCb = function () {
  1572.                 self.recordReadTweets();
  1573.             };
  1574.             this.markAsReadTimer = this.ownerWindow.setTimeout(unreadTimerCb, 1000);
  1575.         }
  1576.     },
  1577.  
  1578.     connectionFailed: function() {
  1579.         this.notifyClients('connectionFailed');
  1580.         if (!this.didUpdate)
  1581.             // if this is the first login attempt, just stop timers and die
  1582.             this.stopTimers('connectionFailed');
  1583.     },
  1584.     /**
  1585.      * calls onRead on all clients with {name: unreadCount, ...} for
  1586.      * each feed.
  1587.      */
  1588.     doNotifyUnread: function(force) {
  1589.         var self = this;
  1590.         if (this.notifyUnreadTimer === undefined)
  1591.             this.notifyUnreadTimer = setTimeout(function() {
  1592.                 self._notifyUnread(self);
  1593.             }, 200);
  1594.     },
  1595.  
  1596.     _notifyUnread: function(self) {
  1597.         delete self.notifyUnreadTimer;
  1598.         var unread = {};
  1599.         var feeds = $.map(self.customFeeds, function (feed) {
  1600.             if (!feed.addsToUnreadCount())
  1601.                 // don't include tweets from unsaved searches
  1602.                 return null;
  1603.  
  1604.             $.each(feed.items, function (i, tweet) {
  1605.                 if (!tweet.read)
  1606.                     unread[tweet.id] = true;
  1607.             });
  1608.  
  1609.             return feed.serialize();
  1610.         });
  1611.  
  1612.         self.notifyClients('onUnread', {
  1613.             feeds: feeds,
  1614.             total: objectKeys(unread).length
  1615.         });
  1616.     },
  1617.  
  1618.     recordReadTweets: function() {
  1619.         delete this.markAsReadTimer;
  1620.  
  1621.         var ids = [];
  1622.         for (var id in this.markAsReadLater) {
  1623.             if (this.markAsReadLater[id])
  1624.                 ids.push(id);
  1625.         }
  1626.  
  1627.         this.markAsReadLater = [];
  1628.  
  1629.         if (ids.length) {
  1630.             var idsSql = joinSingleQuotedStringArray(ids, ', ');
  1631.             var sql1 = 'UPDATE Tweets SET read=1 WHERE Tweets.id IN (' + idsSql + ')';
  1632.             // TODO: hack. we're using two tables, don't do this
  1633.             var sql2 = 'UPDATE Directs SET read=1 WHERE Directs.id IN (' + idsSql + ')';
  1634.             window.db.transaction(function(tx) {
  1635.                 executeSql(tx, sql1);
  1636.                 executeSql(tx, sql2);
  1637.             }, function (error) {
  1638.                 console.error('failed to mark as read: ' + error.message);
  1639.                 console.error('sql was:');
  1640.                 console.error(sql1);
  1641.                 console.error(sql2);
  1642.             });
  1643.         }
  1644.     },
  1645.  
  1646.     loadAllCached: function(opts) {
  1647.         var self = this, allTweets = [];
  1648.  
  1649.             self.loadCachedUsers({tx: opts.tx, error: opts.error, success: function(tx) {
  1650.                 self.loadCachedTweets({tx: tx, error: opts.error, success: function(tx, tweets) {
  1651.                     opts.success('tweets', tweets);
  1652.  
  1653.                     // immediately notify the GUI of the newest self tweet we know about.
  1654.                     self.possibleSelfTweets(tweets);
  1655.  
  1656.                     self.loadCachedDirects({tx: tx, error: opts.error, success: function (tx, directs) {
  1657.                         opts.success('directs', directs);
  1658.                     }});
  1659.                 }});
  1660.             }})
  1661.     },
  1662.  
  1663.     possibleSelfTweets: function (tweets) {
  1664.         var selfTweet = this.findNewestSelfTweet(tweets);
  1665.         if (selfTweet) this.maybeNotifySelfTweet(selfTweet);
  1666.     },
  1667.  
  1668.     maybeNotifySelfTweet: function(tweet) {
  1669.         if (!this.newestSelfTweet || this.newestSelfTweet.created_at_ms < tweet.created_at_ms) {
  1670.             this.newestSelfTweet = notifyTweet(this, tweet);
  1671.             this.notifyClients('selfTweet', this.newestSelfTweet);
  1672.         }
  1673.     },
  1674.  
  1675.     findNewestSelfTweet: function (tweets) {
  1676.         var self = this, newest;
  1677.         $.each(tweets, function (i, tweet) {
  1678.             if (self.isSelfTweet(tweet) && tweet.created_at_ms &&
  1679.                 (!newest || tweet.created_at_ms > newest.created_at_ms))
  1680.                 newest = tweet;
  1681.         });
  1682.  
  1683.         if (newest)
  1684.             return newest;
  1685.     },
  1686.  
  1687.     addGroupsFromLists: function() {
  1688.         var self = this;
  1689.         this.getLists(function (groups) {
  1690.             $.each(groups, function (i, group) {
  1691.                 self.addGroup(group, false);
  1692.             });
  1693.         });
  1694.     },
  1695.  
  1696.     getLists: function(success) {
  1697.         var self = this,
  1698.             urlPrefix = this.apiRoot + this.selfScreenNameLower + '/';
  1699.  
  1700.         var getListsSuccess = success;
  1701.         var groups = [];
  1702.  
  1703.         self.pagedUrlRequest(urlPrefix + 'lists.json', {
  1704.             itemsKey: 'lists',
  1705.             error: errorFunc('could not get list ids'),
  1706.             success: function (lists) {
  1707.                 var i = 0;
  1708.  
  1709.                 function nextGroup() {
  1710.                     var list = lists[i++];
  1711.                     if (!list) return getListsSuccess(groups);
  1712.                     var group = {type: 'group', groupName: list.name,
  1713.                                  filter: false, popups: false, ids: []};
  1714.                     var url = urlPrefix + list.slug + '/members.json';
  1715.                     self.pagedUrlRequest(url, {
  1716.                         itemsKey: 'users',
  1717.                         error: errorFunc('error retrieving ' + url),
  1718.                         success: function (users) {
  1719.                             $.each(users, function (i, user) { group.ids.push(user.id); });
  1720.                             groups.push(group);
  1721.                             nextGroup();
  1722.                         }
  1723.                     });
  1724.                 }
  1725.  
  1726.                 nextGroup();
  1727.             }
  1728.         });
  1729.     },
  1730.  
  1731.     cachedDirectColumnNames: 'directs.id as direct_id, users.id as user_id, *',
  1732.  
  1733.     loadCachedDirects: function(opts) {
  1734.         var self = this;
  1735.  
  1736.         function success(tx, result) {
  1737.             var directs = [];
  1738.             for (var i = 0; i < result.rows.length; ++i) {
  1739.                 var row = result.rows.item(i);
  1740.                 var direct_id = row.direct_id, user_id = row.user_id;
  1741.                 if (user_id)
  1742.                     self.users[user_id] = self.sql.users.fromJSON(row, {id: user_id});
  1743.                 var direct = self.makeDirect(row, {id: direct_id});
  1744.                 directs.push(direct);
  1745.             }
  1746.             directs.reverse();
  1747.             if (opts.success) opts.success(tx, directs);
  1748.         }
  1749.  
  1750.         function error(tx, err) {
  1751.             if (opts.error) opts.error(err);
  1752.             else console.error('could not load cached directs: ' + err.message);
  1753.         }
  1754.  
  1755.         return this.loadCachedItems(opts, this.cachedDirectColumnNames, 'Directs', 'sender_id', success, error);
  1756.     },
  1757.  
  1758.     cachedTweetColumnNames: 'tweets.id as tweet_id, users.id as user_id, tweets.profile_image_url as tweet_image, users.profile_image_url as user_image, *',
  1759.  
  1760.     loadCachedTweets: function(opts) {
  1761.         var self = this;
  1762.  
  1763.         function success(tx, result) {
  1764.             var tweets = [];
  1765.             for (var i = 0; i < result.rows.length; ++i) {
  1766.                 var row = result.rows.item(i);
  1767.  
  1768.                 // avoid id name clash
  1769.                 var tweet_id = row.tweet_id,
  1770.                     user_id = row.user_id,
  1771.                     user_image = row.user_image,
  1772.                     tweet_image = row.tweet_image;
  1773.  
  1774.                 if (user_id)
  1775.                     self.users[user_id] = self.sql.users.fromJSON(row, {id: user_id, profile_image_url: user_image});
  1776.                 var tweet = self.makeTweet(row, {id: tweet_id, profile_image_url: tweet_image});
  1777.  
  1778.                 tweets.push(tweet);
  1779.             }
  1780.  
  1781.             tweets.reverse();
  1782.  
  1783.             if (opts.success)
  1784.                 opts.success(tx, tweets);
  1785.         }
  1786.  
  1787.         function error(tx, err) {
  1788.             if (opts.error) opts.error(err);
  1789.             else console.error('could not load cached tweets: ' + err.message);
  1790.         }
  1791.  
  1792.         return this.loadCachedItems(opts, this.cachedTweetColumnNames, 'Tweets', 'user', success, error);
  1793.     },
  1794.  
  1795.     loadCachedItems: function(opts, columnNames, tableName, userColumn, success, error) {
  1796.         /* id: adds 'WHERE tweet.id == <id>'
  1797.            limit: adds 'LIMIT <limit>' */
  1798.  
  1799.         var self = this;
  1800.  
  1801.         var args = [];
  1802.         var innerQuery = "SELECT " + columnNames + " FROM " + tableName +
  1803.                          " LEFT OUTER JOIN Users ON " + tableName + "." + userColumn + " = Users.id";
  1804.  
  1805.         // WHERE tweet_id
  1806.         if (opts.id) {
  1807.             innerQuery += ' WHERE tweet_id==?';
  1808.             args.push(opts.id);
  1809.         }
  1810.  
  1811.         innerQuery += " ORDER BY " + tableName + ".id DESC";
  1812.  
  1813.         // LIMIT clause
  1814.         if (opts.limit) {
  1815.             innerQuery += ' LIMIT ?';
  1816.             args.push(opts.limit);
  1817.         }
  1818.  
  1819.         var query = "SELECT * FROM (" + innerQuery + ")";
  1820.  
  1821.         console.log(query);
  1822.         useOrCreateTransaction(opts.tx, query, args, success, error);
  1823.     },
  1824.  
  1825.     getUsersToKeep: function() {
  1826.         var excludedUserIds = [];
  1827.         if (this.following_ids)
  1828.             arrayExtend(excludedUserIds, objectKeys(this.following_ids));
  1829.         var groupIds = this.getAllGroupIds();
  1830.         if (groupIds)
  1831.             arrayExtend(excludedUserIds, groupIds);
  1832.         if (this.selfUser)
  1833.             excludedUserIds.push(this.selfUser.id);
  1834.         return excludedUserIds;
  1835.     },
  1836.  
  1837.     discardOldUsers: function(opts) {
  1838.         // discards all users not referenced by tweets or directs
  1839.         opts = opts || {};
  1840.  
  1841.         var excludedUserIds = this.getUsersToKeep();
  1842.  
  1843.         // don't discard users we're following either
  1844.         var appendClause = '';
  1845.         if (excludedUserIds.length) {
  1846.             var usersFollowing = excludedUserIds.join(', ');
  1847.             appendClause = 'and id not in (' + usersFollowing + ')';
  1848.         }
  1849.  
  1850.         var query = 'delete from users where id not in (select sender_id from directs union select user from tweets union select recipient_id from directs)' + appendClause;
  1851.         useOrCreateTransaction(opts.tx, query, [], opts.success, opts.error);
  1852.     },
  1853.  
  1854.     loadCachedUsers: function(opts) {
  1855.         var self = this,
  1856.             query = 'SELECT * FROM Users';
  1857.  
  1858.         function success(tx, result) {
  1859.             var added = 0;
  1860.             for (var i = 0; i < result.rows.length; ++i) {
  1861.                 var row = result.rows.item(i);
  1862.                 if (!(row.id in self.users)) {
  1863.                     self.users[row.id] = self.sql.users.fromJSON(row);
  1864.                     added += 1;
  1865.                 }
  1866.             }
  1867.  
  1868.             console.log('loadCachedUsers loaded ' + added + ' users');
  1869.             if (opts.success)
  1870.                 opts.success(tx);
  1871.         }
  1872.  
  1873.         function error(err) {
  1874.             console.error('error loading cached users: ' + err);
  1875.             if (opts.error)
  1876.                 opts.error(err);
  1877.         }
  1878.  
  1879.         useOrCreateTransaction(opts.tx, query, [], success, error);
  1880.     },
  1881.  
  1882.     inviteFollowers: function() {
  1883.         var self = this;
  1884.         function cb(followers) {
  1885.             followers = $.grep(followers, function() { return true; });
  1886.             console.log('followers:');
  1887.             console.log(JSON.stringify(followers));
  1888.  
  1889.             if (!followers.length)
  1890.                 return;
  1891.  
  1892.             // pick up to 200 random
  1893.             arrayShuffle(followers);
  1894.             var f2 = [];
  1895.             for (var i = 0; i < 200 && i < followers.length; ++i)
  1896.                 f2.push(followers[i]);
  1897.             followers = f2;
  1898.  
  1899.             var idx = 0, errCount = 0;
  1900.             var errCount = 0;
  1901.  
  1902.             function _err() { if (++errCount < 5) _direct(); }
  1903.  
  1904.             function _direct() {
  1905.                 var id = followers[idx++];
  1906.                 if (id === undefined) return;
  1907.                 var data = {text: self.invite_message, user_id: id};
  1908.                 self.urlRequest(self.apiRoot + 'direct_messages/new.json',
  1909.                                 _direct, _err, {type: 'POST', data: data});
  1910.             }
  1911.  
  1912.             _direct();
  1913.         }
  1914.  
  1915.         if (this.followers)
  1916.             cb(this.followers);
  1917.         else
  1918.             this.getFollowedBy(cb);
  1919.     }
  1920. };
  1921.  
  1922. function tweetUserImage(account, tweet) {
  1923.     var userId = tweet.user;
  1924.     if (userId) {
  1925.         var user = account.users[userId];
  1926.         if (user)
  1927.             return user.profile_image_url;
  1928.     }
  1929.  
  1930.     return tweet.profile_image_url;
  1931. }
  1932.  
  1933. function tweetUserName(account, tweet) {
  1934.     var userId = tweet.user;
  1935.     if (userId) {
  1936.         var user = account.users[userId];
  1937.         if (user) return user.name;
  1938.     }
  1939.  
  1940.     return tweet.from_user;
  1941. }
  1942.  
  1943. function tweetScreenName(account, tweet) {
  1944.     var userId = tweet.user;
  1945.     if (userId) {
  1946.         var user = account.users[userId];
  1947.         if (user) return user.screen_name;
  1948.     }
  1949.  
  1950.     return tweet.from_user;
  1951. }
  1952.  
  1953. function directTargetScreenName(account, direct) {
  1954.     var userId = direct.recipient_id;
  1955.     if (userId) {
  1956.         var user = account.users[userId];
  1957.         if (user) return user.screen_name;
  1958.     }
  1959. }
  1960.  
  1961. function TimelineSource() {
  1962.     this.maxId = '0';
  1963. }
  1964.  
  1965. TimelineSource.prototype = {
  1966.     toString: function() {
  1967.         return '<' + this.constructor.name + ' ' + this.url + '>';
  1968.     }
  1969. };
  1970.  
  1971. function TwitterTimelineSource(account, url, data) {
  1972.     if (account === undefined) // for inheritance
  1973.         return;
  1974.  
  1975.     this.account = account;
  1976.     this.url = url;
  1977.     this.data = data;
  1978.     this.count = 100;
  1979.     this.lastUpdateTime = 0;
  1980.     this.lastUpdateStatus = undefined;
  1981.     this.limitById = true;
  1982.     this._updateFrequency = 1000 * 60;
  1983. }
  1984.  
  1985. TwitterTimelineSource.prototype = new TimelineSource();
  1986. TwitterTimelineSource.prototype.constructor = TwitterTimelineSource;
  1987.  
  1988. TwitterTimelineSource.prototype.updateFrequency = function() {
  1989.     return this._updateFrequency;
  1990. };
  1991.  
  1992. TwitterTimelineSource.prototype.setUpdateFrequency = function(updateFreqMs) {
  1993.     this._updateFrequency = updateFreqMs;
  1994. };
  1995.  
  1996. TwitterTimelineSource.prototype.shouldMarkIncomingAsRead = function() {
  1997.     var markReadNow = this.markRead;
  1998.     if (this.markRead)
  1999.         this.markRead = false;
  2000.     return markReadNow;
  2001. };
  2002.  
  2003. TwitterTimelineSource.prototype.markNextUpdateAsRead = function() {
  2004.     this.markRead = true;
  2005. }
  2006.  
  2007. TwitterTimelineSource.prototype.ajaxSuccess = function(data, status, success, error, since_id) {
  2008.     try {
  2009.         var self = this;
  2010.         data.reverse();
  2011.  
  2012.         var newTweets = [];
  2013.  
  2014.         var markRead = self.shouldMarkIncomingAsRead();
  2015.         $.each(data, function (i, item) {
  2016.             var tweet = self.account.makeTweetFromNetwork(item, markRead);
  2017.             if (since_id && compareIds(tweet.id, since_id) < 0) {
  2018.                 console.warn('IGNRORING TWEET WITH ID ' + tweet.id + ' LESS THAN since_id ' + since_id);
  2019.             } else {
  2020.                 if (self.feed.alwaysMentions)
  2021.                     tweet.mention = 1;
  2022.                 self.updateMinMax(tweet.id);
  2023.                 newTweets.push(tweet);
  2024.             }
  2025.         });
  2026.  
  2027.         console.log("requestTweets loaded " + data.length + " tweets from the network");
  2028.  
  2029.         if (success) guard(function() {
  2030.             success(newTweets);
  2031.         });
  2032.     } catch (err) {
  2033.         if (error) {
  2034.             printStackTrace();
  2035.             error(err);
  2036.         }
  2037.         else throw err;
  2038.     }
  2039. };
  2040.  
  2041. /**
  2042.  * returns a simple jsonable object that gets passed to fire()
  2043.  */
  2044. var notifyTweet = function(account, t) {
  2045.     return {text: t.text,
  2046.             user: {screen_name: tweetScreenName(account, t),
  2047.                    profile_image_url: tweetUserImage(account, t)},
  2048.             id: t.id,
  2049.             favorited: t.favorited,
  2050.             created_at: t.created_at,
  2051.             created_at_ms: t.created_at_ms,
  2052.             tweet_type:account.getTweetType(t),
  2053.  
  2054.             // directs
  2055.             sender_id: t.sender_id,
  2056.             recipient_id: t.recipient_id};
  2057. };
  2058.  
  2059. function UpdateNotification(account, count, opts)
  2060. {
  2061.     assert(account);
  2062.     assert(count !== undefined);
  2063.  
  2064.     this.account = account;
  2065.     this.count = count;
  2066.     this.tweets = [];
  2067.     this.opts = opts || {};
  2068.     this.successCount = 0;
  2069. }
  2070.  
  2071. UpdateNotification.prototype = {
  2072.     success: function(source, tweets) {
  2073.         this.successCount++;
  2074.  
  2075.         assert(source && tweets);
  2076.         arrayExtend(this.tweets, tweets);
  2077.         this.onDone();
  2078.  
  2079.         if (this.opts.success)
  2080.             this.opts.success(source, tweets);
  2081.     },
  2082.  
  2083.     error: function(source) {
  2084.         this.onDone();
  2085.     },
  2086.  
  2087.     onDone: function() {
  2088.         //console.warn('UpdateNotification.onDone: ' + (this.count-1));
  2089.         var account = this.account;
  2090.         if (--this.count === 0) {
  2091.             if (!this.successCount)
  2092.                 account.connectionFailed();
  2093.             else {
  2094.                 showPopupsForTweets(account, this.tweets);
  2095.                 if (account.timeline)
  2096.                     account.timeline.finish(false);
  2097.  
  2098.                 if (this.opts.onDone)
  2099.                     this.opts.onDone(this.tweets);
  2100.  
  2101.                 account.discardOldTweets();
  2102.                 account.doNotifyUnread();
  2103.             }
  2104.         }
  2105.     }
  2106. };
  2107.  
  2108. function uniqueSortedTweets(tweets, timeline) {
  2109.     function tweetCmp(a, b) {
  2110.         // directs first, then mentions, then by id, then if it's in the timeline
  2111.         return (-cmp(account.isDirect(a), account.isDirect(b))
  2112.                 || -cmp(account.isMention(a), account.isMention(b))
  2113.                 || -cmp(timeline.hasTweet(a), timeline.hasTweet(b))
  2114.                 || cmp(a.created_at_ms, b.created_at_ms)
  2115.                 || compareIds(a.id, b.id));
  2116.     }
  2117.  
  2118.     tweets.sort(tweetCmp);
  2119.     return uniquify(tweets, function(a) { return a.id; });
  2120. }
  2121.  
  2122. var shownPopupIds = {};
  2123. var lastPopupIdTrim = 0;
  2124. var ONE_HOUR_MS = 60 * 1000 * 60;
  2125.  
  2126. function maybeTrimPopupIds(now) {
  2127.  
  2128.     if (now - lastPopupIdTrim < ONE_HOUR_MS)
  2129.         return;
  2130.  
  2131.     lastPopupIdTrim = now;
  2132.  
  2133.     var toDelete = [];
  2134.     $.each(shownPopupIds, function (id, time) {
  2135.         if (now - time > ONE_HOUR_MS)
  2136.             toDelete.push(id);
  2137.     });
  2138.  
  2139.     $.each(toDelete, function (i, id) { delete shownPopupIds[id]; });
  2140. }
  2141.  
  2142. function firePopup(tweets, opts) {
  2143.     if (opts === undefined)
  2144.         opts = {};
  2145.  
  2146.     opts.topic = 'twitter.newtweet';
  2147.     opts.tweets = tweets;
  2148.  
  2149.     D.rpc('fire', opts);
  2150. }
  2151.  
  2152. var _postfix_count = 0;
  2153. function showPopupForTweet(account, tweet) {
  2154.     _postfix_count++;
  2155.     return firePopup([notifyTweet(tweet)], {popupid_postfix: _postfix_count});
  2156. }
  2157.  
  2158. function showPopupsForTweets(account, tweets) {
  2159.     var feedsFilteringPopups = [];
  2160.  
  2161.     // collect feeds with popups: false
  2162.     $.each(account.feeds, function (name, feed) {
  2163.         if (feed.popups !== undefined && !feed.popups)
  2164.             feedsFilteringPopups.push(feed);
  2165.     });
  2166.  
  2167.     var notifyTweets = $.map(tweets, function (t) {
  2168.         // never show self tweets
  2169.         if (account.isSelfTweet(t))
  2170.             return null;
  2171.  
  2172.         // don't show repeats
  2173.         if (t.id in shownPopupIds)
  2174.             return null;
  2175.  
  2176.         if (t.read)
  2177.             return null;
  2178.  
  2179.         // exclude popups for feeds with popups: false
  2180.         for (var j = 0; j < feedsFilteringPopups.length; ++j) {
  2181.             if (feedsFilteringPopups[j].hasTweet(t, account))
  2182.                 return null;
  2183.         }
  2184.  
  2185.         return notifyTweet(account, t);
  2186.     });
  2187.  
  2188.     if (!notifyTweets.length)
  2189.         return;
  2190.  
  2191.     notifyTweets = uniqueSortedTweets(notifyTweets, account.feeds.timeline);
  2192.  
  2193.     var now = new Date().getTime();
  2194.     $.each(notifyTweets, function (i, tweet) { shownPopupIds[tweet.id] = now; });
  2195.     maybeTrimPopupIds(now);
  2196.  
  2197.     return firePopup(notifyTweets);
  2198. }
  2199.  
  2200. TwitterTimelineSource.prototype.cache = function(tweets) {
  2201.     this.account.cacheTweets(tweets);
  2202. };
  2203.  
  2204. TwitterTimelineSource.prototype.update = function(updateNotification) {
  2205.     var self = this, account = this.account;
  2206.  
  2207.     if (this.updating) {
  2208.         console.warn('WARNING: ' + this + ' is already updating');
  2209.         this.updating.error(self);
  2210.     }
  2211.  
  2212.     this.updating = updateNotification;
  2213.  
  2214.     this.loadNewer(function (tweets, info) {
  2215.         if (info) {
  2216.             console.warn('url request got ' + tweets.length + ' tweets: ' + info.url);
  2217.         }
  2218.         var extraUpdate = (self.feed.extraUpdate || function(t, d) { d(t); });
  2219.         extraUpdate.call(self, tweets, function(tweets) {
  2220.             self.cache(tweets);
  2221.             account.newTweets(tweets, self);
  2222.             if (self.onUpdate)
  2223.                 guard(function() { self.onUpdate(tweets); });
  2224.             if (updateNotification)
  2225.                 updateNotification.success(self, tweets);
  2226.         });
  2227.         self.updating = undefined;
  2228.     }, function (error) {
  2229.         console.error('error updating source: ' + error.message + ' ' + error);
  2230.         self.updating = undefined;
  2231.  
  2232.         if (updateNotification)
  2233.             updateNotification.error(self, error);
  2234.     });
  2235. };
  2236.  
  2237. TwitterTimelineSource.prototype.didUpdate = function(status) {
  2238.     this.lastUpdateTime = new Date().getTime();
  2239.     this.lastUpdateStatus = status;
  2240. };
  2241.  
  2242. TwitterTimelineSource.prototype.needsUpdate = function(now) {
  2243.     if (!this.feed.autoUpdates ||
  2244.         !this.updateFrequency()) // updateFrequency may be 0 (never update this feed)
  2245.         return false;
  2246.  
  2247.     return now - this.lastUpdateTime >= this.updateFrequency();
  2248. };
  2249.  
  2250. TwitterTimelineSource.prototype.loadNewer = function(success, error, opts) {
  2251.     var self = this, account = this.account, url = this.url;
  2252.  
  2253.     function ajaxError(xhr, textStatus, errorThrown) {
  2254.         self.didUpdate('error');
  2255.  
  2256.         /*
  2257.         * search.twitter.com servers can sometimes return 403s if they don't like the since_id you're sending. see
  2258.         * http://groups.google.com/group/twitter-development-talk/browse_thread/thread/ed72429eef055cb3/23cf597ef030ca62?lnk=gst&q=search+403#23cf597ef030ca62
  2259.         *
  2260.         * if we get a 403 from search.twitter.com, try clearing maxId and trying again.
  2261.         */
  2262.         if (!self._didSearchHack && xhr.status === 403) {
  2263.             var searchUrl = 'http://search.twitter.com/search.json';
  2264.             if (url.substr(0, searchUrl.length) === searchUrl) {
  2265.                 self.maxId = '0';
  2266.                 self._didSearchHack = true;
  2267.                 console.warn('clearing max id and restarting search');
  2268.                 return self.loadNewer(success, error, opts);
  2269.             }
  2270.         }
  2271.  
  2272.         console.log('error for url: ' + url);
  2273.         console.log(' xhr.status: ' + xhr.status);
  2274.         console.log(' xhr.statusText: ' + xhr.statusText);
  2275.         if (errorThrown)
  2276.             console.log("error exception: " + errorThrown.message);
  2277.         if (textStatus)
  2278.             console.log("error with status: " + textStatus);
  2279.  
  2280.         if (error)
  2281.             error(xhr, textStatus, errorThrown);
  2282.     }
  2283.  
  2284.     var oldMaxId = self.maxId;
  2285.     console.info('requesting tweets, maxId is ' + oldMaxId + ': ' + url);
  2286.  
  2287.     function _success(data) {
  2288.         self.didUpdate('success');
  2289.         success(data, {url: url});
  2290.     }
  2291.  
  2292.     this.request = account.urlRequest(url, function(data, status) {
  2293.         //for (var i = 0; i < data.length; ++i)
  2294.             //if (compareIds(data[i].id_str, oldMaxId) < 0)
  2295.                 //console.warn('OLDER ID: ' + data[i].id_str + ' older than old max ' + oldMaxId);
  2296.         return self.ajaxSuccess(data, status, _success, error, data.since_id);
  2297.     }, ajaxError, {data: this.makeData()});
  2298. };
  2299.  
  2300. TwitterTimelineSource.prototype.makeData = function() {
  2301.     var args = shallowCopy(this.data);
  2302.     args.count = this.count;
  2303.     if (this.limitById && compareIds(this.maxId, '0') > 0)
  2304.         args.since_id = this.maxId;
  2305.  
  2306.     return args;
  2307. };
  2308.  
  2309. TwitterTimelineSource.prototype.updateMinMax = function(id) {
  2310.     if (compareIds(id, this.maxId) > 0)
  2311.         this.maxId = id;
  2312. };
  2313.  
  2314. function TwitterTimelineDirectSource(account) {
  2315.     TwitterTimelineSource.call(this, account, account.apiRoot + 'direct_messages.json');
  2316. }
  2317.  
  2318. TwitterTimelineDirectSource.inheritsFrom(TwitterTimelineSource, {
  2319.     cache: function(directs) {
  2320.         this.account.cacheDirects(directs);
  2321.     },
  2322.  
  2323.     ajaxSuccess: function(data, status, success, error) {
  2324.         var self = this;
  2325.         var newDirects = [];
  2326.         data.reverse();
  2327.  
  2328.         var markRead = self.shouldMarkIncomingAsRead();
  2329.         function onData(data) {
  2330.             $.each(data, function (i, item) {
  2331.                 var direct = self.account.makeDirectFromNetwork(item, markRead);
  2332.                 self.updateMinMax(direct.id);
  2333.                 newDirects.push(direct);
  2334.             });
  2335.         }
  2336.  
  2337.         onData(data);
  2338.  
  2339.         try {
  2340.  
  2341.             // 2nd, load send directs. this is a hack and needs to be abstracted into the idea of feeds having multiple sources.
  2342.             var _interim_success = function(data, status) {
  2343.                 try {
  2344.                     onData(data);
  2345.                     console.log("loaded " + data.length + " directs from the network");
  2346.                     newDirects.sort(tweetsByTime);
  2347.  
  2348.                     if (success)
  2349.                         guard(function() { success(newDirects); });
  2350.                 } catch (err) {
  2351.                     if (error) {
  2352.                         printException(err);
  2353.                         error(err);
  2354.                     } else throw err;
  2355.                 }
  2356.             };
  2357.  
  2358.             var url2 = self.account.apiRoot + 'direct_messages/sent.json';
  2359.             var data = {};
  2360.             if (this.maxId && this.maxId !== '0')
  2361.                 data.since_id = this.maxId;
  2362.             this.request = self.account.urlRequest(url2, _interim_success, error, {data: data});
  2363.  
  2364.         } catch (err) {
  2365.             if (error) {
  2366.                 printException(err);
  2367.                 error(err);
  2368.             } else throw err;
  2369.         }
  2370.     }
  2371. });
  2372.  
  2373. function TwitterTimelineSearchSource(account, searchQuery) {
  2374.     this.searchQuery = searchQuery;
  2375.     TwitterTimelineSource.call(this, account, account.searchApiRoot + 'search.json');
  2376. }
  2377.  
  2378. var searchTweetAttributes = ['id', 'text', 'from_user', 'source', 'profile_image_url', 'created_at'];
  2379.  
  2380.  
  2381. TwitterTimelineSearchSource.prototype = new TwitterTimelineSource();
  2382.  
  2383. TwitterTimelineSearchSource.prototype.updateFrequency = function() {
  2384.     // all searches get their update frequency from the account
  2385.     return this.account.searchUpdateFrequency;
  2386. }
  2387.  
  2388. TwitterTimelineSearchSource.prototype.makeData = function() {
  2389.     var args = shallowCopy(this.data);
  2390.     args.rpp = Math.min(100, this.count);
  2391.     args.q = this.searchQuery;
  2392.     if (compareIds(this.maxId, '0') > 0)
  2393.         args.since_id = this.maxId;
  2394.  
  2395.     return args;
  2396. };
  2397.  
  2398. TwitterTimelineSearchSource.prototype.ajaxSuccess = function(_data, status, success, error) {
  2399.     var self = this, data = _data.results, tweets = [];
  2400.     var account = self.account;
  2401.  
  2402.     data.reverse();
  2403.     //console.log('TwitterTimelineSearchSource.ajaxSuccess ' + data.length + ' results');
  2404.  
  2405.     window.db.transaction(function (tx) {
  2406.         function recurseBuild(i) {
  2407.             if (i === data.length) {
  2408.                 //console.log('search retreived ' + i + ' tweets, calling success');
  2409.                 if (success) success(tweets);
  2410.                 return;
  2411.             }
  2412.  
  2413.             // TODO: use account.makeTweet here
  2414.  
  2415.             var searchTweet = data[i];
  2416.             useStringIdentifiers(searchTweet, ['id', 'in_reply_to_status_id']);
  2417.             var tweet = {truncated: null,
  2418.                          in_reply_to: null,
  2419.                          favorited: null,
  2420.                          in_reply_to_status_id: null,
  2421.                          in_reply_to_user_id: null,
  2422.                          in_reply_to_screen_name: null,
  2423.  
  2424.                          read: 0,
  2425.                          mention: account.isMention(searchTweet),
  2426.                          search: self.searchQuery};
  2427.  
  2428.             var username = searchTweet.from_user;
  2429.             var userId = null;
  2430.  
  2431.             executeSql(tx, 'SELECT * FROM Users WHERE Users.screen_name == ?', [username], function (tx, result) {
  2432.                 if (result.rows.length) {
  2433.                     // if we already know about the user, use its real id.
  2434.                     var row = result.rows.item(0);
  2435.                     userId = row.id;
  2436.                     if (!account.users[userId])
  2437.                         account.users[row.user] = account.sql.users.fromJSON(row);
  2438.                 }
  2439.  
  2440.                 $.each(searchTweetAttributes, function (i, attr) { tweet[attr] = searchTweet[attr]; });
  2441.                 tweet.user = userId;
  2442.                 if (userId === null)
  2443.                     tweet.from_user = username;
  2444.  
  2445.                 self.updateMinMax(tweet.id);
  2446.                 account.tweetIn(tweet);
  2447.                 tweets.push(tweet);
  2448.  
  2449.                 return recurseBuild(i + 1);
  2450.             }, function (tx, errorObj) {
  2451.                 console.error('error retrieving user w/ screen_name ' + username);
  2452.                 if (error)
  2453.                     error(errorObj);
  2454.             });
  2455.         }
  2456.  
  2457.         recurseBuild(0);
  2458.     });
  2459. };
  2460.  
  2461. function TwitterHTTPClient(account) {
  2462.     this.account = account;
  2463. }
  2464.  
  2465. TwitterHTTPClient.prototype = {
  2466.     didFirstUpdate: false,
  2467.  
  2468.     didReceiveFollowing: function(following_json) {
  2469.         D.notify('following', {following: following_json});
  2470.     },
  2471.  
  2472.     didReceiveTrends: function(trends) {
  2473.         D.notify('trends', {trends: trends});
  2474.     },
  2475.  
  2476.     serverMessage: function(url, opts) {
  2477.         D.ajax({url: urlQuery('digsby://' + url, opts),
  2478.                 type: 'JSON'});
  2479.     },
  2480.  
  2481.     onReply: function(tweet) {
  2482.         this.serverMessage('reply',
  2483.             {id: tweet.id,
  2484.              screen_name: tweetScreenName(this.account, tweet),
  2485.              text: tweet.text});
  2486.     },
  2487.  
  2488.     onRetweet: function(tweet) {
  2489.         this.serverMessage('retweet',
  2490.             {id: tweet.id,
  2491.              screen_name: tweetScreenName(this.account, tweet),
  2492.              text: tweet.text});
  2493.     },
  2494.  
  2495.     onDirect: function(tweet) {
  2496.         this.serverMessage('direct',
  2497.             {screen_name: tweetScreenName(this.account, tweet)});
  2498.     },
  2499.  
  2500.     onUnread: function(info)  {
  2501.         D.notify('unread', info);
  2502.     },
  2503.  
  2504.     selfTweet: function(tweet) {
  2505.         D.notify('selfTweet', {tweet: tweet});
  2506.     },
  2507.  
  2508.     feedsUpdated: function(feeds) {
  2509.         D.notify('feeds', feeds);
  2510.     },
  2511.  
  2512.     viewChanged: function(feedName) {
  2513.         D.notify('viewChanged', {feedName: feedName});
  2514.     },
  2515.  
  2516.     stateChanged: function(state) {
  2517.         D.notify('state', {state: state});
  2518.     },
  2519.  
  2520.     didReceiveUpdates: function() {
  2521.         if (this.didFirstUpdate)
  2522.             return;
  2523.  
  2524.         this.didFirstUpdate = true;
  2525.         this.account.changeState('online');
  2526.     },
  2527.  
  2528.     didReceiveWholeUpdate: function() {
  2529.         D.notify('received_whole_update');
  2530.     },
  2531.  
  2532.     connectionFailed: function() {
  2533.         if (this.didFirstUpdate)
  2534.             // connection problems after first update are ignored.
  2535.             return;
  2536.  
  2537.         this.account.changeState('connfail');
  2538.     },
  2539.  
  2540.     statusUpdated: function() {
  2541.         D.notify('hook', 'digsby.twitter.status_updated');
  2542.     },
  2543.  
  2544.     directSent: function() {
  2545.         D.notify('hook', 'digsby.twitter.direct_sent');
  2546.     },
  2547.  
  2548.     editFeed: function(feed) { D.notify('edit_feed', feed); },
  2549.  
  2550.     recentTimeline: function(tweets) {
  2551.         var acct = this.account;
  2552.         D.notify('recentTimeline', {
  2553.             tweets: $.map(tweets, function(t) { return notifyTweet(acct, t); })
  2554.         });
  2555.     }
  2556. };
  2557.  
  2558. var returnFalse = function() { return false; };
  2559.  
  2560. var tweetActions = {
  2561.  
  2562.     container: function(elem) { return $(elem).parents('.container')[0]; },
  2563.  
  2564.     favorite: function(elem) {
  2565.         var node = this.container(elem);
  2566.         var skin = account.timeline.skin;
  2567.         skin.setFavorite(node, 'pending');
  2568.  
  2569.         var tweet = account.timeline.nodeToElem(node);
  2570.         var originalFavorite = tweet.favorited;
  2571.         account.favorite(tweet, function (tweet) {
  2572.             skin.setFavorite(node, tweet.favorited);
  2573.         }, function (error) {
  2574.             console.log('error setting favorite');
  2575.             skin.setFavorite(node, originalFavorite);
  2576.         });
  2577.     },
  2578.  
  2579.     trash: function (elem) {
  2580.         var self = this;
  2581.         guard(function() {
  2582.         var node = self.container(elem);
  2583.         var tweet = account.timeline.nodeToElem(node);
  2584.         var skin = account.timeline.skin;
  2585.         var opts = {
  2586.             id: tweet.id,
  2587.             success: function (tweet) {
  2588.                 // remove it visually
  2589.                 node.parentNode.removeChild(node); // TODO: animate this
  2590.             },
  2591.             error: function(err) {
  2592.                 printException(err);
  2593.                 console.log('error deleting tweet');
  2594.                 skin.setDelete(node);
  2595.             },
  2596.         };
  2597.  
  2598.         skin.setDelete(node, 'pending');
  2599.         if (tweet.sender_id)
  2600.             account.deleteDirect(opts);
  2601.         else
  2602.             account.deleteTweet(opts);
  2603.         });
  2604.     },
  2605.  
  2606.     action: function(action, elem) {
  2607.         account.notifyClients(action, account.timeline.nodeToElem(this.container(elem)));
  2608.     },
  2609.  
  2610.     reply: function(elem) { this.action('onReply', elem); },
  2611.     retweet: function(elem) { this.action('onRetweet', elem); },
  2612.     direct: function(elem) { this.action('onDirect', elem); },
  2613.  
  2614.     popupInReplyTo: function(id) {
  2615.         id = this.tweets[id].in_reply_to_status_id;
  2616.         if (!id) return;
  2617.         account.getTweet(id, function (tweet) {
  2618.             showPopupForTweet(tweet);
  2619.         });
  2620.     },
  2621.  
  2622.     inReplyTo: function(a) {
  2623.         var self = this;
  2624.  
  2625.         guard(function() {
  2626.             var timeline = account.timeline;
  2627.             var container = self.container(a);
  2628.             var tweet = timeline.nodeToElem(container);
  2629.             var id = tweet.in_reply_to_status_id;
  2630.  
  2631.             // if the CSS transformations here change, please also modify
  2632.             // feedSwitch in timeline2.js
  2633.             a.style.display = 'none';
  2634.  
  2635.             account.getTweet(id, function(tweet) {
  2636.                 container.className += ' reply';
  2637.                 timeline.insertAsParent(tweet, container);
  2638.                 //spinner.parentNode.removeChild(spinner);
  2639.             }, function (error) {
  2640.                 console.log('error retreiving reply tweet');
  2641.                 printException(error);
  2642.             });
  2643.  
  2644.         });
  2645.     },
  2646.  
  2647.     openUser: function(a) {
  2648.         if (!settings.user_feeds)
  2649.             return true;
  2650.  
  2651.         guard(function() {
  2652.             var screen_name = a.innerHTML;
  2653.             console.log('openUser ' + screen_name);
  2654.             var feedDesc = {type: 'user', screen_name: screen_name};
  2655.             account.addFeed(feedDesc, true);
  2656.         });
  2657.  
  2658.         return false;
  2659.     },
  2660. };
  2661.  
  2662. function tweetToString() { return '<Tweet ' + this.id + '>'; }
  2663. function directToString() { return '<Direct ' + this.id + '>'; }
  2664.  
  2665. /**
  2666.  * twitter error responses come with nicely formatted error messages in
  2667.  * a JSON object's "error" key. this function returns an AJAX error handler
  2668.  * that passes that error message to the given function.
  2669.  */
  2670. function extractJSONError(errorMessage, errorFunc) {
  2671.     function error(xhr, textStatus, errorThrown) {
  2672.         console.error('xhr.repsonseText: ' + xhr.responseText);
  2673.  
  2674.         try {
  2675.             var err = JSON.parse(xhr.responseText).error;
  2676.             if (err) errorMessage += ': ' + err;
  2677.         } catch (err) {}
  2678.  
  2679.         console.error(errorMessage);
  2680.         errorFunc(errorMessage);
  2681.     }
  2682.     return error;
  2683. }
  2684.  
  2685. function transformRetweet(tweet) {
  2686.     var rt = tweet.retweeted_status;
  2687.     if (rt && rt.user && rt.user.screen_name)
  2688.         tweet.text = 'RT @' + rt.user.screen_name + ': ' + rt.text;
  2689.  
  2690.     return tweet;
  2691. }
  2692.  
  2693. function useOrCreateTransaction(tx, query, values, success, error) {
  2694.     function execQuery(tx) { executeSql(tx, query, values, success, error); }
  2695.     if (tx) execQuery(tx);
  2696.     else db.transaction(execQuery);
  2697. }
  2698.  
  2699. var didCacheTimestamp = false;
  2700.  
  2701. // called with the Twitter API server's current time
  2702. function receivedCorrectTimestamp(date, cache) {
  2703.     if (cache || !didCacheTimestamp) {
  2704.         OAuth.correctTimestamp(Math.floor(date.getTime() / 1000));
  2705.         cacheDiffMs(new Date().getTime() - date.getTime(), true);
  2706.     }
  2707. }
  2708.  
  2709. // caches the difference between our time and Twitter server time
  2710. lastCachedToDisk = null;
  2711.  
  2712. function cacheDiffMs(diffMs, cache) {
  2713.     diffMs = parseInt(diffMs, 10);
  2714.  
  2715.     // don't recache to disk if unnecessary.
  2716.     if (!cache && lastCachedToDisk !== null && Math.abs(lastCachedToDisk - diffMs) < 1000*60*2)
  2717.         return;
  2718.  
  2719.     console.log('cacheDiffMs(' + diffMs + ')');
  2720.  
  2721.     window.db.transaction(function(tx) {
  2722.         executeSql(tx, 'insert or replace into info_keyed values (?, ?)', ['__servertimediff__', diffMs], function() {
  2723.             didCacheTimestamp = true;
  2724.             lastCachedToDisk = diffMs;
  2725.         },
  2726.         function(e) {
  2727.             console.warn('cacheDiffMs DATABASE ERROR: ' + e);
  2728.         });
  2729.     }, errorFunc('error caching server time diff'));
  2730. }
  2731.  
  2732. function loadServerTime(done) {
  2733.     window.db.transaction(function(tx) {
  2734.         function success(tx, result) {
  2735.             if (result.rows.length) {
  2736.                 var diffMs = result.rows.item(0).value;
  2737.                 if (diffMs) {
  2738.                     diffMs = parseInt(diffMs, 10);
  2739.                     if (diffMs) {
  2740.                         var d = new Date(new Date().getTime() - diffMs);
  2741.                         receivedCorrectTimestamp(d);
  2742.                     }
  2743.                 }
  2744.             } else
  2745.                 console.warn('**** no server time cached');
  2746.             done();
  2747.         }
  2748.         executeSql(tx, "select value from info_keyed where key == ?", ['__servertimediff__'], success, done);
  2749.     });
  2750. }
  2751.  
  2752.