Skip to content

Commit

Permalink
Merge pull request #33 from radiovisual/list-feature
Browse files Browse the repository at this point in the history
Feeds From Twitter List Feature
  • Loading branch information
radiovisual authored Jun 3, 2018
2 parents b2a5995 + 9fe5776 commit b023983
Show file tree
Hide file tree
Showing 9 changed files with 10,151 additions and 62 deletions.
5 changes: 3 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
sudo: false
language: node_js
node_js:
- '5'
- '4'
- '8'
- '9'
- '10'
after_success: npm run coveralls
55 changes: 45 additions & 10 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default class Birdwatch {
/**
* Initialize a new Birdwatch
*
* @param {Object} options
* @param {Object} options - the Birdwatch options
* @api public
*/
constructor(options) {
Expand Down Expand Up @@ -50,12 +50,13 @@ export default class Birdwatch {
/**
* Get or set the feeds to monitor
*
* @param {String} screenname
* @param {Object} options
* @param {String} screenname - the Twitter screenname
* @param {Object} options - the feed options
* @returns {Object} the Birdwatch instance
* @api public
*/
feed(screenname, options) {
if (!arguments.length) {
if (arguments.length === 0) {
return this._feed;
}

Expand All @@ -71,10 +72,34 @@ export default class Birdwatch {
return this;
}

/**
* Get feeds from a Twitter list
*
* @param {String} listName - the name of the Twitter list
* @param {String} listOwner - the list owner's screename
* @returns {Object} the Birdwatch instance
* @api public
*/
feedsFromList(listName, listOwner) {
if (arguments.length === 0) {
return this._feedLists;
}

this._feedLists = this._feedLists || [];

this._feedLists.push({
listName,
listOwner
});

return this;
}

/**
* Start the birdwatch process.
*
* @api public
* @returns {*} Promise
*/
async start() {
const self = this;
Expand All @@ -85,13 +110,15 @@ export default class Birdwatch {
}
});

if (!this.feed() || this.feed().length === 0) {
throw new Error('You must supply at least one feed to Birdwatch');
if ((!this.feed() && !this.feedsFromList()) || ((this.feed().length === 0 && this.feedsFromList().length === 0))) {
throw new Error('You must supply at least one feed or list to Birdwatch');
}

const feedsFromList = await self.processFeedsFromList(self.feedsFromList());

report.logStartMessage(self.options.refreshTime, self);

await Promise.all(this.feed().map(feed => {
await Promise.all(self.feed().map(feed => {
const options = objectAssign({}, self.options, feed.options);

if (!feed.screenname) {
Expand All @@ -102,7 +129,7 @@ export default class Birdwatch {
if (Array.isArray(options.filterTags)) {
options.filterTags = hashRegex(options.filterTags);
} else if (!isRegexp(options.filterTags)) {
throw new Error(`Invalid regex: ${options.filterTags} for ${feed.screenname}`);
throw new TypeError(`Invalid regex: ${options.filterTags} for ${feed.screenname}`);
}
}
self.feeds.push({
Expand All @@ -112,8 +139,16 @@ export default class Birdwatch {
return Promise.resolve();
}));

await self.processFeeds(self.feeds);
await self.startTimer(self.feeds);
await self.processFeeds(self.feeds, feedsFromList);

// Now setup the interval that will incrementally update the cache.
setInterval(async () => {
const feedsFromList = await self.processFeedsFromList(self.feedsFromList());

await self.processFeeds(self.feeds, feedsFromList);
await self.sortTweets();
await self.saveToCacheFile();
}, self.options.refreshTime * 1000);

if (self.options.server) {
await self.startServer();
Expand Down
6 changes: 2 additions & 4 deletions lib/report.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
const chalk = require('chalk');
const version = require('./../package').version;
const {version} = require('./../package');

module.exports.logStartMessage = function (refreshTime, birdwatch) {
const {server, port, url} = birdwatch.options;
const feeds = birdwatch.feed().length;
const refreshSeconds = birdwatch.options.refreshTime;
const refreshMinutes = Math.round(refreshSeconds / 60);
const cache = `${birdwatch.options.cacheDir}/cached_tweets.json`;
const server = birdwatch.options.server;
const port = birdwatch.options.port;
const url = birdwatch.options.url;
const vers = `v${version}`;

console.log('\n\n');
Expand Down
124 changes: 100 additions & 24 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable prefer-promise-reject-errors */
import fs from 'fs';
import path from 'path';

Expand Down Expand Up @@ -37,22 +38,34 @@ server.use((req, res, next) => {
* Process the feeds by starting the promise chain.
*
* @param {Array} feeds - the list of feeds to process
* @param {Array} feedsFromList - the members of a Twitter list
*/
export async function processFeeds(feeds) {
export async function processFeeds(feeds, feedsFromList) {
const self = this;

self.filteredTweets = [];
self.allTweets = [];

// Let's go Birdwatching!
// Let's go Birdwatching!
if (self.options.logReports) {
report.processBirdwatchingMessage();
}

await Promise.all(feeds.map((feed, index) => {
let allFeeds = [
...feeds
];

if (feedsFromList && Array.isArray(feedsFromList) && feedsFromList.length > 0) {
allFeeds = [
...feeds,
...Array.prototype.concat(...feedsFromList)
];
}

await Promise.all(allFeeds.map((feed, index) => {
return new Promise(async (resolve, reject) => {
const options = objectAssign({}, self.options, feed.options);
const screenname = feed.screenname;
const {screenname} = feed;

await self.getTwitterData(screenname, options).then(async () => {
await self.filterTweets(screenname, options);
Expand All @@ -69,12 +82,53 @@ export async function processFeeds(feeds) {
}));
}

/**
* Get a list of members from a public Twitter list
*
* @param {String} listName - the Twitter list name
* @param {String} listOwner - the screename of the Twitter list owner
* @returns {*} Promise
*/
export async function fetchFeedsFromTwitterList(listName, listOwner) {
const api = `https://api.twitter.com/1.1/lists/members.json?slug=${listName}&owner_id=${listOwner}&owner_screen_name=${listOwner}&count=200`;

return new Promise((resolve, reject) => {
const credentials = getCredentials();
const oauth = new OAuth.OAuth(
'https://api.twitter.com/oauth/request_token',
'https://api.twitter.com/oauth/access_token',
credentials.consumerKey,
credentials.consumerSecret,
'1.0A',
null,
'HMAC-SHA1'
);

oauth.get(
api,
credentials.accessToken,
credentials.accessTokenSecret,
(err, data) => {
if (err) {
if (err.statusCode === 404) {
reject('404 Error');
} else {
reject(new OAuthError(err));
}
} else {
resolve(data);
}
}
);
});
}

/**
* Get tweets straight from the Twitter REST API.
*
* @param screenname - Screenname of the requested Twitter user.
* @param options - Birdwatch options
* @returns {Promise}
* @param {String} screenname - Screenname of the requested Twitter user.
* @param {Object} options - Birdwatch options
* @returns {*} Promise
*/
export function getTwitterData(screenname, options) {
const self = this;
Expand All @@ -84,7 +138,7 @@ export function getTwitterData(screenname, options) {
}

return new Promise((resolve, reject) => {
// are we using testData, or need real Twitter data?
// A we using testData, or need real Twitter data?
if (options.testData) {
self.allTweets = options.testData;
resolve(self.allTweets);
Expand Down Expand Up @@ -129,11 +183,11 @@ export function getTwitterData(screenname, options) {
*
* @param {String} screenname - the screenname of the current feed
* @param {Object} options - the feed options
* @returns {Promise}
* @returns {*} Promise
*/
export function filterTweets(screenname, options) {
const self = this;
const limit = options.limit;
const {limit} = options;
let count = 0;

const tweetdata = self.allTweets;
Expand Down Expand Up @@ -171,7 +225,7 @@ export function filterTweets(screenname, options) {
* Sort the tweets
*
* @note: Defaults to chronological order if no custom function is supplied.
* @returns {Promise}
* @returns {*} Promise
*/
export function sortTweets() {
const self = this;
Expand All @@ -183,25 +237,28 @@ export function sortTweets() {
}

/**
* Start the interval timer based on this.refreshTime
* Sort the tweets
*
* @note: seconds are converted to milliseconds
* @param {Array} feeds - feeds to pass to this.processFeeds()
* @note: Defaults to chronological order if no custom function is supplied.
* @param {Object} listMembers - the raw list members data that comes from the Twitter API.
* @returns {*} Promise
*/
export function startTimer(feeds) {
const self = this;

setInterval(async () => {
await self.processFeeds(feeds);
await self.sortTweets();
await self.saveToCacheFile();
return Promise.resolve();
}, self.options.refreshTime * 1000);
export function listMembersToFeedEntries(listMembers) {
const listObj = JSON.parse(listMembers);

return Promise.all(listObj.users.map(user => {
return Promise.resolve({
screenname: user.screen_name,
options: {
limit: 20
}
});
}));
}

/**
* Save the processed tweets to the on-disk cache
*
* @returns {undefined} side effect: saves file to disk
*/
export function saveToCacheFile() {
const self = this;
Expand All @@ -219,6 +276,24 @@ export function saveToCacheFile() {
});
}

/**
* Check to see if we need to find list members to add to the feed
* @param {Array} feedsFromList - the list of members to turn into feeds
* @returns {*} Promise
*/
export async function processFeedsFromList(feedsFromList) {
if (feedsFromList && feedsFromList.length > 0) {
return Promise.all(feedsFromList.map(async listData => {
const {listName, listOwner} = listData;
const listMembers = await fetchFeedsFromTwitterList(listName, listOwner);
const members = await listMembersToFeedEntries(listMembers);

return Promise.resolve(members);
}));
}
return Promise.resolve('No feeds from list to process');
}

/**
* Start the birdwatch server
*
Expand All @@ -238,6 +313,7 @@ export function startServer() {
*
* @returns {object}
* @api private
* @returns {Object} credentials object
*/
export function getCredentials() {
return {
Expand Down
Loading

0 comments on commit b023983

Please sign in to comment.