Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ phantomjs.exe

## Don't commit settings past default ones
.settings

## Don't commit any encryption/settings files
.fbmessenger*
test.*
2 changes: 1 addition & 1 deletion cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
function verifyLogon(callback) {
crypt.load((cryptLoadErr, data) => {
// Load settings before performing any other actions
Settings.read((readSettingsErr) => {
Settings.read((readSettingsErr, settings) => {
if (readSettingsErr) {
callback(readSettingsErr);
}
Expand Down
15 changes: 8 additions & 7 deletions lib/crypt.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { getDataDirectory } = require('./data_directory');

class Crypt {
constructor() {
this.algorithm = 'aes-256-ctr';
this.filename = '.kryptonite';
this.filename = '.fbmessenger.enc';
this.filepath = path.resolve(getDataDirectory(), this.filename);
this.data = undefined;
this.password = 'password';
}
Expand Down Expand Up @@ -34,20 +36,19 @@ class Crypt {

save(data) {
const encrypted = this.encrypt(data);
const savePath = path.resolve(__dirname, '../', this.filename);
fs.writeFileSync(savePath, encrypted);
fs.writeFileSync(this.filepath, encrypted);
}

load(callback) {
if (!this.data) {
fs.readFile(path.resolve(__dirname, '../', this.filename), (err, data) => {
fs.readFile(this.filepath, (err, data) => {
if (err) {
callback('No saved profile, please login');
} else {
this.decrypt(data.toString(), (err, dec) => {
if (err)
if (err) {
callback(err);
else {
} else {
this.data = dec;
callback(null, this.data);
}
Expand All @@ -61,7 +62,7 @@ class Crypt {

flush() {
this.data = undefined;
fs.unlink(path.resolve(__dirname, '../', this.filename), (err) => {
fs.unlink(this.filepath, (err) => {
err('Error while logging out')
});
}
Expand Down
37 changes: 37 additions & 0 deletions lib/data_directory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const fs = require('fs');
const os = require('os');
const path = require('path');

const normalizeFilePath = filePath => {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This expands the home directory if it contains a tilde.

This is copied from the untildify javascript library.

if (typeof filePath !== 'string') {
throw new TypeError(`Expected a string, got ${typeof filePath}`);
}

const homeDirectory = os.homedir();

if (homeDirectory) {
return filePath.replace(/^~(?=$|\/|\\)/, homeDirectory);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a path helper that sanitize strings instead of a regex? I always prefer platform agnostic helper/libs for those sort of things

}

return filePath;
};

const getDataDirectory = () => {
const environmentDataDirectory = process.env.FB_MESSENGER_DATA_DIR;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows the user to specify a custom location for their dotfiles.

const homeDirectory = os.homedir();
const thisDirectory = path.resolve(__dirname, '../');

if (environmentDataDirectory) {
return normalizeFilePath(environmentDataDirectory);
}

if (homeDirectory) {
return homeDirectory;
}

return thisDirectory;
};

module.exports = {
getDataDirectory,
};
19 changes: 11 additions & 8 deletions lib/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

const fs = require('fs');
const path = require('path');
const { getDataDirectory } = require('./data_directory');

class Settings {
constructor () {
this.filename = '.settings';
this.path = path.resolve(__dirname, '../', this.filename);
this.filename = '.fbmessengerrc';
this.filepath = path.resolve(getDataDirectory(), this.filename);
this.properties = {
disableColors: false,
groupColors: true,
Expand All @@ -29,19 +30,19 @@ class Settings {

// Save the current properties dictionary on disk
save() {
fs.writeFile(this.path, JSON.stringify(this.properties, null, ' '), (err) => {
fs.writeFile(this.filepath, JSON.stringify(this.properties, null, ' '), (err) => {
if (!err) {
console.log('Settings have been saved');
} else {
console.log(`Error saving .settings file: ${err}`);
console.log(`Error saving ${this.filepath} file: ${err}`);
}
});
}

// Load previously saved properties from disk
// callback(error, properties), where properties is a dictionary
read(callback) {
fs.readFile(path.resolve(__dirname, '../', this.filename), (err, data) => {
fs.readFile(this.filepath, (err, data) => {
if (!err) {
try {
const fileProperties = JSON.parse(data.toString());
Expand All @@ -54,8 +55,11 @@ class Settings {
else diff = true;
});

if (diff) this.save();
if (diff) {
this.save();
}

return callback(null, fileProperties);
} catch (parseErr) {
this.save();
return callback('Warning: Settings are invalid, saving default values');
Expand All @@ -64,13 +68,12 @@ class Settings {
this.save();
return callback('Warning: Settings not found, saving default values');
}
callback();
});
}

// Delete properties from disk and wipe dictionary in memory
flush() {
fs.unlink(this.filename);
fs.unlink(this.filepath);
this.properties = {};
}

Expand Down
139 changes: 80 additions & 59 deletions lib/test/regression.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,39 @@ const Crypt = require('../crypt.js');
const Messenger = require('../messenger.js');
const Settings = require('../settings.js');
const path = require('path');
const oldCryptFilename = Crypt.filename;
const oldCryptFilepath = Crypt.filepath;

describe('Crypt', () => {
it('getInstance() should create a new singleton object', () => {
const crypt = Crypt.getInstance();

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had an issue using getInstance, so instead this PR monkey patches Crypt's filename and filepath for testing.

It creates a .test.fbmessenger.enc file in the lib/test directory.

expect(crypt).to.not.equal(undefined);
before(() => {
const filename = 'test.fbmessenger.enc';
Crypt.filename = filename;
Crypt.filepath = path.resolve('.', filename);
});

it('getInstance() always returns the same instance', () => {
let crypt = Crypt.getInstance();
crypt.password = 'test123_%';
crypt = Crypt.getInstance();
expect(crypt.password).to.equal('test123_%');
afterEach(() => {
Crypt.data = undefined;
});

after(() => {
Crypt.filename = oldCryptFilename;
Crypt.filepath = oldCryptFilepath;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We restore Crypt back to it's original state.

});

it('encrypt() and decrypt() should return original data', () => {
const data = "{some: 'things', are: 'not equal'}";
const crypt = Crypt.getInstance();
const encrypted = crypt.encrypt(data);
const decrypted = crypt.decrypt(encrypted);
expect(decrypted).to.equal(data);
const encrypted = Crypt.encrypt(data);
Crypt.decrypt(encrypted, (_err, decrypted) => {
expect(decrypted).to.equal(data);
});
});

it('save() and load()', (done) => {
const crypt = Crypt.getInstance();
const file = '.test_kryptonite';
const data = JSON.stringify({theTest: 'data', is: 5012344});

crypt.filename = file;
crypt.save(data);
Crypt.save(data);

crypt.load((err, result) => {
Crypt.load((err, result) => {
expect(result).to.equal(data);
done();
});
Expand All @@ -48,15 +50,14 @@ describe('Messenger', () => {
let json;
let messenger;

it('Load cookie', (done) => {
// Reset crypt
const crypt = new Crypt();
crypt.filename = '.kryptonite';

crypt.load((err, result) => {
afterEach(() => {
Crypt.data = undefined;
});

it('Load cookie', (done) => {
// Reset crypt
Crypt.load((err, result) => {
expect(() => {JSON.parse(result);}).not.throw(Error);

json = JSON.parse(result);
expect(json.cookie).to.not.equal.undefined;
expect(json.fb_dtsg).to.not.equal.undefined;
Expand All @@ -65,61 +66,81 @@ describe('Messenger', () => {
});
});

it('Create Messenger', () => {
messenger = new Messenger(json.cookie, json.c_user, json.fb_dtsg);
});
describe('when data is loaded from the cookie', () => {
beforeEach(() => {
Crypt.load((err, data) => {
json = JSON.parse(data);
});
});

it('Send message', function(done) {
// Allow more time for network calls
this.timeout(5000);
it('Create Messenger', () => {
messenger = new Messenger(json.cookie, json.c_user, json.fb_dtsg);
});

messenger.sendMessage('ar.alexandre.rose', '731419306', 'Running tests - Send message', done);
});
it('getFriends', function(done) {
// Set a higher timeout for network calls
this.timeout(5000);
messenger.getFriends((_err, friends) => {
const friendIds = Object.keys(friends);
expect(friendIds.length).to.be.above(1);
done();
});
});

it('GetLastMessage', function(done) {
// Allow more time for network calls
this.timeout(5000);
it('Get threads', function (done) {
// Allow more time for network calls
this.timeout(5000);

messenger.getMessages('ar.alexandre.rose', '731419306', 10, (err, messages) => {
expect(messages.length).is.equal(10);
done();
messenger.getThreads((_err, messages) => {
expect(messages.length).to.be.above(1);
done();
});
});
});

it('Get threads', function(done) {
// Allow more time for network calls
this.timeout(5000);

messenger.getThreads(true, done);
describe('interacting with friends', () => {
it('Send message', function (done) {
// Allow more time for network calls
this.timeout(5000);

messenger.getFriends((_, friends) => {
const myself = friends[json.c_user];
messenger.sendMessage(myself.vanity, myself.id, 'Running tests - Send message', done);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will ping the user who is using the library:

image

});
});

it('GetLastMessage', function (done) {
// Set a higher timeout for network calls
this.timeout(5000);
messenger.getFriends((_, friends) => {
const myself = friends[json.c_user];

messenger.getMessagesGraphQl(myself.vanity, myself.id, 10, (_err, messages) => {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this test to use getMessagesGraphQL, since I could not retrieve any messages using getMessages anymore.

expect(messages.length).to.be.equal(10);
done();
});
});
});
});
});
});

describe('Settings', () => {
it('getInstance() should create a new singleton object', () => {
const settings = Settings.getInstance();
expect(settings).to.not.equal(undefined);
});

it('getInstance() always returns the same instance', () => {
let settings = Settings.getInstance();
settings.filename = 'test123_%';
settings = Settings.getInstance();
expect(settings.filename).to.equal('test123_%');
beforeEach(() => {
const filename = 'test.fbmessengerrc';
Settings.filename = filename;
Settings.filepath = path.resolve('.', filename);
});

it('save() and load()', (done) => {
const options = {'lights': 'on', 'engine': 'on', 'fuel_pump': 'on', 'running': true};
const settings = Settings.getInstance();
const file = '.test_settings';
const settings = Settings;

settings.properties = options;
settings.filename = file;
settings.save();

settings.load((err, result) => {
settings.read((err, result) => {
expect(result).to.deep.equal(options);
done();
});

});
});