forked from GitHub/dbot
Initial rewrite of goodreads module using async/await
Covers original functionality plus adds a new ~reading command. There is probably a lot of duplication in the module itself that can be cleaned up with some additional metaprogramming but *eh* I'm tired tonight.
This commit is contained in:
parent
a574b7d2ed
commit
71c2c52d47
2
install
2
install
@ -14,7 +14,7 @@ if [[ $? -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
npm install googlemaps humanize feedparser node-units tvdb method-override 500px process async wordnik node-uuid underscore request sandbox express moment-timezone moment jade databank databank-redis ent passport passport-local password-hash connect-flash
|
||||
npm install googlemaps humanize feedparser node-units tvdb method-override 500px process async wordnik node-uuid underscore request request-promise-native sandbox express moment-timezone moment jade databank databank-redis ent passport passport-local password-hash connect-flash
|
||||
|
||||
cd public/
|
||||
wget https://github.com/twbs/bootstrap/releases/download/v3.3.2/bootstrap-3.3.2-dist.zip
|
||||
|
@ -1,162 +1,294 @@
|
||||
/**
|
||||
* Module Name: GoodReads
|
||||
* Description: Various goodreads.
|
||||
* Description: Interacts with the GoodReads API to provide book-oriented functionality to dbot
|
||||
*/
|
||||
|
||||
var _ = require('underscore')._,
|
||||
request = require('request'),
|
||||
async = require('async'),
|
||||
moment = require('moment'),
|
||||
parseString = require('xml2js').parseString;
|
||||
|
||||
var goodreads = function(dbot) {
|
||||
this.ApiRoot = 'https://www.goodreads.com/';
|
||||
|
||||
const util = require('util'),
|
||||
_ = require('underscore')._,
|
||||
rp = require('request-promise-native'),
|
||||
parseString = util.promisify(require('xml2js').parseString);
|
||||
|
||||
const GoodReads = function(dbot) {
|
||||
this.apiRoot = 'https://www.goodreads.com';
|
||||
|
||||
this.internalAPI = {
|
||||
'getGoodreads': function(server, nick, callback) {
|
||||
dbot.api.profile.getProfile(server, nick, function(err, user, profile) {
|
||||
if(user) {
|
||||
if(profile && _.has(profile.profile, 'goodreads')) {
|
||||
callback(user, profile.profile.goodreads);
|
||||
} else {
|
||||
callback(user, null);
|
||||
}
|
||||
} else {
|
||||
callback(null, null);
|
||||
}
|
||||
'outputError': (evt, e) => {
|
||||
switch(e) {
|
||||
case 'goodreads-error': evt.reply('Error talking to GoodReads.'); return;
|
||||
case 'book-not-found': evt.reply(dbot.t('gr_nobook')); return;
|
||||
case 'no-description': evt.reply('No description was found for the book you asked for.'); return;
|
||||
case 'author-not-found': evt.reply(dbot.t('gr_noauthor')); return;
|
||||
}
|
||||
|
||||
console.log(e);
|
||||
evt.reply('Something went wrong and I don\'t know what.');
|
||||
},
|
||||
|
||||
'formatProfile': profile => {
|
||||
var shelves = {};
|
||||
_.each(profile.user_shelves.user_shelf, shelf => {
|
||||
shelves[shelf.name] = shelf.book_count['_'];
|
||||
});
|
||||
profile.user_shelves = shelves;
|
||||
return profile;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.api = {
|
||||
'searchBook': function(term, callback) {
|
||||
request.get({
|
||||
'url': this.ApiRoot + 'search.xml',
|
||||
'qs': {
|
||||
'q': term,
|
||||
'key': this.config.api_key
|
||||
'findBook': async term => {
|
||||
//https://www.goodreads.com/search/index.xml
|
||||
const body = await rp({
|
||||
uri: this.apiRoot + '/search/index.xml',
|
||||
qs: {
|
||||
key: this.config.api_key,
|
||||
q: term.split(' ').join('+')
|
||||
}
|
||||
}, function(err, response, body) {
|
||||
if(!_.isUndefined(body) && !err) {
|
||||
parseString(body, function(err, result) {
|
||||
// This is why we don't use XML kids
|
||||
var result = result['GoodreadsResponse'].search[0].results[0];
|
||||
if(_.has(result, 'work')) {
|
||||
callback(null, {
|
||||
'id': result.work[0].best_book[0].id[0]['_'],
|
||||
'title': result.work[0].best_book[0].title[0],
|
||||
'author': result.work[0].best_book[0].author[0].name[0],
|
||||
'rating': result.work[0].average_rating[0]
|
||||
});
|
||||
} else {
|
||||
callback(true, null);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(true, null);
|
||||
});
|
||||
|
||||
const response = await parseString(body, { explicitArray: false });
|
||||
if(!_.has(response, 'GoodreadsResponse')) throw 'goodreads-error';
|
||||
|
||||
const result = response.GoodreadsResponse.search.results;
|
||||
if(!result || !_.has(result, 'work')) throw 'book-not-found';
|
||||
if(!result.work[0]) throw 'book-not-found';
|
||||
|
||||
return {
|
||||
id: result.work[0].best_book.id['_'],
|
||||
title: result.work[0].best_book.title,
|
||||
author: result.work[0].best_book.author.name,
|
||||
rating: result.work[0].average_rating
|
||||
};
|
||||
},
|
||||
|
||||
'getSummaryForBook': async id => {
|
||||
//https://www.goodreads.com/book/show.xml
|
||||
const body = await rp({
|
||||
uri: this.apiRoot + '/book/show.xml',
|
||||
qs: {
|
||||
key: this.config.api_key,
|
||||
id: id
|
||||
}
|
||||
}.bind(this));
|
||||
});
|
||||
|
||||
const response = await parseString(body, { explicitArray: false });
|
||||
if(!_.has(response, 'GoodreadsResponse')) throw 'goodreads-error';
|
||||
|
||||
const result = response.GoodreadsResponse.book;
|
||||
if(!result) throw 'book-not-found';
|
||||
if(!_.has(result, 'description')) throw 'no-description';
|
||||
|
||||
return result.description;
|
||||
},
|
||||
|
||||
'findAuthor': async term => {
|
||||
//https://www.goodreads.com/api/author_url/<ID>
|
||||
const body = await rp({
|
||||
url: this.apiRoot + '/api/author_url/' + term,
|
||||
qs: {
|
||||
key: this.config.api_key
|
||||
}
|
||||
});
|
||||
|
||||
const response = await parseString(body, {explicitArray: false });
|
||||
if(!_.has(response, 'GoodreadsResponse')) throw 'goodreads-error';
|
||||
|
||||
const result = response.GoodreadsResponse.author;
|
||||
if(!result) throw 'author-not-found';
|
||||
|
||||
return {
|
||||
id: result['$'].id,
|
||||
author: result.name
|
||||
};
|
||||
},
|
||||
|
||||
'getProfileById': async id => {
|
||||
//https://www.goodreads.com/user/show.xml
|
||||
try {
|
||||
var body = await rp({
|
||||
url: this.apiRoot + '/user/show.xml',
|
||||
qs: {
|
||||
key: this.config.api_key,
|
||||
id: id
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
if(e.statusCode && e.statusCode == 404) {
|
||||
throw 'user-not-found';
|
||||
return;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
const response = await parseString(body, { explicitArray: false });
|
||||
if(!_.has(response, 'GoodreadsResponse')) throw 'goodreads-error';
|
||||
|
||||
const result = response.GoodreadsResponse.user;
|
||||
if(!result) throw 'user-not-found';
|
||||
|
||||
return this.internalAPI.formatProfile(result);
|
||||
},
|
||||
|
||||
'getProfile': function(id, callback) {
|
||||
request.get({
|
||||
'url': this.ApiRoot + 'user/show.xml',
|
||||
'qs': {
|
||||
'username': id,
|
||||
'key': this.config.api_key
|
||||
'getProfileByName': async username => {
|
||||
//https://www.goodreads.com/user/show.xml
|
||||
try {
|
||||
var body = await rp({
|
||||
url: this.apiRoot + '/user/show.xml',
|
||||
qs: {
|
||||
key: this.config.api_key,
|
||||
username: username
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
if(e.statusCode && e.statusCode == 404) {
|
||||
throw 'user-not-found';
|
||||
return;
|
||||
}
|
||||
}, function(err, response, body) {
|
||||
if(!_.isUndefined(body) && !err) {
|
||||
parseString(body, function(err, result) {
|
||||
if(result && _.has(result, 'GoodreadsResponse')) {
|
||||
var shelves = {};
|
||||
result = result['GoodreadsResponse'].user[0].user_shelves[0].user_shelf;
|
||||
_.each(result, function(shelf) {
|
||||
shelves[shelf.name[0]] = shelf.book_count[0]['_'];
|
||||
});
|
||||
callback(null, shelves);
|
||||
} else {
|
||||
callback(true, null);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(true, null);
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
const response = await parseString(body, { explicitArray: false });
|
||||
if(!_.has(response, 'GoodreadsResponse')) throw 'goodreads-error';
|
||||
|
||||
const result = response.GoodreadsResponse.user;
|
||||
if(!result) throw 'user-not-found';
|
||||
|
||||
return this.internalAPI.formatProfile(result);
|
||||
},
|
||||
|
||||
'getShelfForUserId': async (id, shelf) => {
|
||||
//https://www.goodreads.com/review/list.xml?v=2
|
||||
var body = await rp({
|
||||
url: this.apiRoot + '/review/list.xml',
|
||||
qs: {
|
||||
v: '2',
|
||||
key: this.config.api_key,
|
||||
id: id,
|
||||
shelf: shelf
|
||||
}
|
||||
});
|
||||
|
||||
const response = await parseString(body, { explicitArray: false });
|
||||
if(!_.has(response, 'GoodreadsResponse')) throw 'goodreads-error';
|
||||
|
||||
let result = response.GoodreadsResponse.reviews.review;
|
||||
if(!result) return [];
|
||||
|
||||
if(!_.isArray(result)) {
|
||||
result = [result];
|
||||
}
|
||||
|
||||
return _.map(result, r => {
|
||||
return {
|
||||
id: r.book.id['_'],
|
||||
title: r.book.title_without_series
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.commands = {
|
||||
'~book': function(event) {
|
||||
this.api.searchBook(event.input[1], function(err, res) {
|
||||
if(!err) {
|
||||
event.reply(dbot.t('gr_book', {
|
||||
'author': res.author,
|
||||
'title': res.title,
|
||||
'rating': res.rating,
|
||||
'link': this.ApiRoot + 'book/show/' + res.id
|
||||
}));
|
||||
} else {
|
||||
event.reply(dbot.t('gr_nobook'));
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
'~books': function(event) {
|
||||
var user = event.rUser,
|
||||
gr = event.rProfile.goodreads;
|
||||
if(event.res[0]) {
|
||||
user = event.res[0].user;
|
||||
gr = event.res[0].gr;
|
||||
'~book' : async evt => {
|
||||
try {
|
||||
const book = await this.api.findBook(evt.input[1]);
|
||||
evt.reply(dbot.t('gr_book', {
|
||||
author: book.author,
|
||||
title: book.title,
|
||||
rating: book.rating,
|
||||
link: this.apiRoot + '/book/show/' + book.id
|
||||
}));
|
||||
}
|
||||
|
||||
this.api.getProfile(gr, function(err, profile) {
|
||||
if(!err) {
|
||||
event.reply(dbot.t('gr_books', {
|
||||
'user': user.currentNick,
|
||||
'read': profile.read,
|
||||
'currently_reading': profile['currently-reading']
|
||||
}));
|
||||
} else {
|
||||
event.reply(dbot.t('gr_unknown'));
|
||||
catch(e) { this.internalAPI.outputError(evt, e); }
|
||||
},
|
||||
|
||||
'~booksummary': async evt => {
|
||||
try {
|
||||
console.log(evt.input[1]);
|
||||
const book = await this.api.findBook(evt.input[1]);
|
||||
const summary = await this.api.getSummaryForBook(book.id);
|
||||
evt.reply(dbot.t('gr_summary', {
|
||||
title: book.title,
|
||||
summary: summary,
|
||||
link: this.apiRoot + '/book/show/' + book.id
|
||||
}));
|
||||
}
|
||||
catch(e) { this.internalAPI.outputError(evt, e); }
|
||||
},
|
||||
|
||||
'~author' : async evt => {
|
||||
try {
|
||||
evt.reply(dbot.t('gr_author', await this.api.findAuthor(evt.input[1])));
|
||||
}
|
||||
catch(e) { this.internalAPI.outputError(evt, e); }
|
||||
},
|
||||
|
||||
'~reading': async (evt, profile) => {
|
||||
try {
|
||||
let books = await this.api.getShelfForUserId(profile.id, 'currently-reading');
|
||||
const booksCount = books.length;
|
||||
if(!booksCount) {
|
||||
evt.reply(dbot.t('gr_not_reading', { user: evt.rUser.currentNick }));
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
let tooMany = booksCount > 5;
|
||||
if (tooMany) books = _.sample(books, 5);
|
||||
|
||||
evt.reply(dbot.t('gr_is_reading', { user: evt.rUser.currentNick, count: booksCount }));
|
||||
_.each(books, b => {
|
||||
evt.reply(ostr = b.title + ' - https://www.goodreads.com/book/show/' + b.id);
|
||||
});
|
||||
|
||||
if (tooMany) {
|
||||
evt.reply('... And ' + (booksCount - 5) + ' more - https://www.goodreads.com/review/list/' + profile.id + '?shelf=currently-reading');
|
||||
}
|
||||
}
|
||||
catch(e) { this.internalAPI.outputError(evt, e); }
|
||||
}
|
||||
};
|
||||
this.commands['~book'].regex = [/^book ([\d\w\s-]*)/, 2];
|
||||
|
||||
_.each(this.commands, function(command) {
|
||||
command.resolver = function(event, callback) {
|
||||
if(event.rProfile && _.has(event.rProfile, 'goodreads')) {
|
||||
if(event.params[1]) {
|
||||
this.internalAPI.getGoodreads(event.server, event.params[1], function(user, gr) {
|
||||
if(user && gr) {
|
||||
event.res.push({
|
||||
'user': user,
|
||||
'gr': gr
|
||||
});
|
||||
callback(false);
|
||||
} else {
|
||||
if(!user) {
|
||||
event.reply('Unknown user.');
|
||||
} else {
|
||||
event.reply(user.currentNick + ': Set a Goodreads username with "~set goodreads username"');
|
||||
}
|
||||
callback(true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(false);
|
||||
|
||||
this.commands['~book'].regex = [/^book (.*)/, 2];
|
||||
this.commands['~booksummary'].regex = [/^booksummary (.*)/, 2];
|
||||
this.commands['~author'].regex = [/^author ([\d\w\s-]*)/, 2];
|
||||
|
||||
this.commands['~reading'].requiresProfile = true;
|
||||
|
||||
_.each(this.commands, ((cmd, cmdName) => {
|
||||
if(cmd.requiresProfile) {
|
||||
this.commands[cmdName] = (async evt => {
|
||||
const grUsername = evt.rProfile.goodreads;
|
||||
|
||||
if(!grUsername) {
|
||||
evt.reply(evt.rUser.currentNick + ': Set a Goodreads username with "~set goodreads username"');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
event.reply(event.user + ': Set a goodreads username with "~set goodreads username"');
|
||||
callback(true);
|
||||
}
|
||||
}.bind(this);
|
||||
}, this);
|
||||
|
||||
let grId = evt.rProfile.goodreads_id;
|
||||
|
||||
try {
|
||||
var profile;
|
||||
if(grId) {
|
||||
profile = await this.api.getProfileById(grId);
|
||||
} else {
|
||||
profile = await this.api.getProfileByName(grUsername);
|
||||
grId = profile.id;
|
||||
dbot.api.profile.setProperty(evt.server, evt.user, 'goodreads_id', grId, function(){});
|
||||
}
|
||||
|
||||
};
|
||||
await cmd(evt, profile);
|
||||
}
|
||||
catch(e) {
|
||||
if(e === 'user-not-found') evt.reply('User not found. Is your GoodReads username set correctly?');
|
||||
else this.internalAPI.outputError(evt, e);
|
||||
}
|
||||
}).bind(this);
|
||||
}
|
||||
}).bind(this))
|
||||
|
||||
}
|
||||
|
||||
exports.fetch = function(dbot) {
|
||||
return new goodreads(dbot);
|
||||
};
|
||||
|
||||
exports.fetch = dbot => new GoodReads(dbot);
|
@ -2,13 +2,22 @@
|
||||
"gr_book": {
|
||||
"en": "[{title} by {author} - {rating}] - {link}"
|
||||
},
|
||||
"gr_summary": {
|
||||
"en": "[{title}] - {summary} - {link}"
|
||||
},
|
||||
"gr_author": {
|
||||
"en": "[{author}] - https://www.goodreads.com/author/show/{id}"
|
||||
},
|
||||
"gr_nobook": {
|
||||
"en": "No books found."
|
||||
"en": "No book by that name was found."
|
||||
},
|
||||
"gr_books": {
|
||||
"en": "{user} has read {read} books and is currently reading {currently_reading} books."
|
||||
"gr_noauthor": {
|
||||
"en": "No author by that name was found."
|
||||
},
|
||||
"gr_unknown": {
|
||||
"en": "Unknown Goodreads username!"
|
||||
"gr_not_reading": {
|
||||
"en": "{user} is not currently reading any books."
|
||||
},
|
||||
"gr_is_reading": {
|
||||
"en": "{user} is currently reading the following {count} books:"
|
||||
}
|
||||
}
|
||||
|
6
modules/goodreads/usage.json
Normal file
6
modules/goodreads/usage.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"~book": "~book [bookname] - returns the title, author, rating, and GoodReads link",
|
||||
"~booksummary": "~booksummary [bookname] - returns the summary for the requested book",
|
||||
"~author": "~author [authorname] - returns the GoodReads link for the requested author",
|
||||
"~reading": "~reading - displays up to 5 of the books you are currently reading"
|
||||
}
|
Loading…
Reference in New Issue
Block a user