<commit_before>const config = require('./config');
const api = require('./api');
const md5File = require('md5-file/promise');
const fs = require('fs-extra');
const path = require('path');
const mkdirp = require('mkdirp');
const JSZip = require('jszip');

function json_stringify_readable(content) {
  return JSON.stringify(content, null, 4);
}

// Remove dir with files recursively
const rmdir = async function(dir) {
    // TODO: Handle file operations concurrently, but probably is overkill.
    let files = await fs.readdir(dir);

    for (let i = 0; i < files.length; i++) {
      let file = files[i];
      let fileName = path.join(dir, file);
      let stat = fs.statSync(fileName);

      if (fileName == "." || fileName == "..") {
          // pass these files
          continue;
      } else if (stat.isDirectory()) {
          // rmdir recursively
          await rmdir(fileName);
      } else {
          // rm filename
          await fs.unlink(fileName);
      }
    }
    await fs.rmdir(dir);
};

/* TODO: I'm absolutely certain there is a good use case for generators here,
   but i can't figure out how to use them with nested promises.
   Is there a workaround without babel until this is available?
   https://github.com/tc39/proposal-async-iteration
*/
/* TODO: Can be optimized by not simply awaiting everything. Next file could be read in parallel. */
async function listFiles(folder, filter) {
  let result = [];

  let files = await fs.readdir(folder);
  for (let i = 0; i < files.length; i++) {
    let file = files[i];
    let fileWithPath = path.join(folder, file);

    if (filter && !filter(fileWithPath)) {
      continue;
    }
    let stats = await fs.stat(fileWithPath);
    if (stats.isDirectory()) {
      let subFiles = await listFiles(path.join(folder, file), filter);
      subFiles.forEach(subFile => {
        result.push(path.join(file, subFile));
      });
    } else {
      result.push(file);
    }
  }

  return result.slice(0);
}

function filterNoNodeModules(fileName) {
  return fileName.indexOf('node_modules/') === -1;
}

function filterMetaFiles(fileName) {
  return !fileName.endsWith('.meta.json');
}

class DynappObjects {
  constructor(folder, fileExt, filter) {
    this.folder = folder;
    // TODO: This is not good, dirt hacky workaround.
    // Should just use a single meta-file per type (data-items, data-source-items, data-objects)
    this.fileExt = fileExt || '';
    this.filter = filter || (() => true);
  }

  _objectsPath () {
    return path.join(config.workPath(), this.folder);
  }

  async upload () {
    let objectsPath = this._objectsPath();
    let operations = [];
    let [newObjects, changedObjects, deletedObjects] = await this.dirty();

    for (let obj of newObjects) {
      operations.push(this.createObject(obj, path.join(objectsPath, obj)));
    }
    for (let obj of changedObjects) {
      operations.push(this.updateObject(obj, path.join(objectsPath, obj)));
    }
    for (let obj of deletedObjects) {
      operations.push(this.deleteObject(obj).catch(err => {
        // File has been removed by other means, everything is good
        if (err.statusCode && err.statusCode === 404)
          return err.message;
        else
          throw err;
      }));
    }

    return await Promise.all(operations);
  }

  createObject (obj, file) {
    throw new Error('Not implemented');
  }

  updateObject (obj, file) {
    throw new Error('Not implemented');
  }

  deleteObject (obj, file) {
    throw new Error('Not implemented');
  }

  async hashes() {
    let operations = [];
    let objectsPath = this._objectsPath();
    // TODO: Reuse list from dirty() and hashes() ?
    let localFiles = await listFiles(objectsPath, this.filter);

    for (let i = 0; i < localFiles.length; i++) {
      let fileName = localFiles[i];
      let operation = md5File(path.join(objectsPath, fileName)).then(function(hash) {
        return {
          name: fileName,
          hash: hash
        };
      });

      operations.push(operation);
    }

    let files = await Promise.all(operations);
    let result = {};

    for (let i = 0; i < files.length; i++) {
      let file = files[i];
      result[file.name] = {
        hash: file.hash
      };
    }

    return result;
  }

  setHashes (hashes) {
    this._hashes = hashes;
  }

  async readMeta (file) {
    let metaFilePath = file + '.meta.json';
    let metaRaw = '{}'
    try {
      let metaRaw = await fs.readFile(metaFilePath, 'utf8');
    } catch (err) {
      if (err.code !== 'ENOENT')
        throw err;
    }
    return JSON.parse(metaRaw);
  }

  async generateMeta (file) {
    // Generates a meta json file, used for updating data-sources and -objects
    let metaFilePath = file.substring(0, file.lastIndexOf('.py')) + '.meta.json';

    let bodyRaw = await fs.readFile(file, 'utf8');
    let body = Buffer.from(bodyRaw).toString('base64');

    let metaRaw = '{}';
    try {
      metaRaw = await fs.readFile(metaFilePath, 'utf8');
    } catch(err) {
      if (err.code !== 'ENOENT') {
        throw err;
      }
    }
    let meta = JSON.parse(metaRaw);

    meta.stylesheet = body;
    return json_stringify_readable(meta);
  }

  async dirty() {
    let objectsPath = this._objectsPath();
    let localFiles = await listFiles(objectsPath, this.filter);
    let newObjects = [];
    let deletedObjects = [];
    let changedObjectsOperations = [];

    for (let file of localFiles) {
      if (file in this._hashes) {
        let operation = md5File(path.join(objectsPath, file)).then((hash) => {
          return this._hashes[file].hash !== hash ? file : null;
        });
        changedObjectsOperations.push(operation);
      } else if (!file.endsWith('.meta.json')) {
        newObjects.push(file);
      }
    }

    for (let fileName in this._hashes) {
      if (localFiles.indexOf(fileName) === -1) {
        deletedObjects.push(fileName);
      }
    }

    // Remove all nulls returned in above loop
    let changedObjects = (await Promise.all(changedObjectsOperations))
      .filter(item => !!item);
    changedObjects = changedObjects.map(item => item.replace(/\.meta\.json$/, this.fileExt));
    // Remove duplicates. We might have caught meta files that we formatted to be origin files.
    // Because if meta changes origin should be updated.
    changedObjects = [...new Set(changedObjects)];

    return [newObjects, changedObjects, deletedObjects];
  }
}

class DataItems extends DynappObjects {
  constructor() {
    super('data-items', null, filterNoNodeModules);
  }

  async createObject (dataItem, file) {
    // TODO: Should we use PUT? Then we have to catch eventual error and try PUT if POST already exists.
    // Seems like PUT just overrides everything, and that is kind of what we want.
    return await api.updateDataItem(dataItem, fs.createReadStream(file), await this.readMeta(file));
  }

  async updateObject (dataItem, file) {
    return await api.updateDataItem(dataItem, fs.createReadStream(file), await this.readMeta(file));
  }

  deleteObject (dataItem) {
    return api.deleteDataItem(dataItem);
  }
}

class DataSourceItems extends DynappObjects {
  constructor() {
    super('data-source-items', '.py');
  }

  async createObject (dataSourceItem, file) {
    return await api.updateDataSourceItem(dataSourceItem.substring(0, dataSourceItem.lastIndexOf('.py')), await this.generateMeta(file));
  }

  async updateObject (dataSourceItem, file) {
    return await api.updateDataSourceItem(dataSourceItem.substring(0, dataSourceItem.lastIndexOf('.py')), await this.generateMeta(file));
  }

  deleteObject (dataSourceItem) {
    return api.deleteDataSourceItem(dataSourceItem);
  }
}

class DataObjects extends DynappObjects {
  constructor() {
    super('data-objects', '.py');
  }

  async createObject (dataObject, file) {
    return await api.updateDataObject(dataObject.substring(0, dataObject.lastIndexOf('.py')), await this.generateMeta(file));
  }

  async updateObject (dataObject, file) {
    return await api.updateDataObject(dataObject.substring(0, dataObject.lastIndexOf('.py')), await this.generateMeta(file));
  }

  deleteObject (dataObject) {
    return api.deleteDataObject(dataObject);
  }
}

class Sync {
  constructor () {
    this.dataItems = new DataItems();
    this.dataSourceItems = new DataSourceItems();
    this.dataObjects = new DataObjects();
  }

  getStateFilePath () {
    return config.workFilePath('.dynapp-state');
  }

  async state () {
    let stateContent;

    try {
      stateContent = await fs.readFile(this.getStateFilePath(), 'utf8');
    } catch(err) {
      // Ensure we have a state-file
      if (err.code === 'ENOENT') {
        stateContent = json_stringify_readable({});
        await fs.writeFile(this.getStateFilePath(), stateContent, 'utf8');
      } else {
        throw err;
      }
    }

    return JSON.parse(stateContent);
  }

  async setHashes () {
    let state = await this.state();
    this.dataItems.setHashes(state['data-items'] || {});
    this.dataSourceItems.setHashes(state['data-source-items'] || {});
    this.dataObjects.setHashes(state['data-objects'] || {});
  }

  async dumpState () {
    let hashes = await Promise.all([
      this.dataItems.hashes(),
      this.dataSourceItems.hashes(),
      this.dataObjects.hashes()
    ]);

    let state = {
      'data-items': hashes[0],
      'data-source-items': hashes[1],
      'data-objects': hashes[2]
    };
    return await fs.writeFile(this.getStateFilePath(), json_stringify_readable(state), 'utf8');
  }

  async upload () {
    await this.setHashes();

    await Promise.all([
      this.dataItems.upload(),
      this.dataSourceItems.upload(),
      this.dataObjects.upload()
    ]);

    await this.dumpState();
  }

  async download () {
    // TODO: Break into parts and put in respective class

    // TODO: Now we load all of the zip into memory.
    // Can we stream it somehow?
    const appZip = await api.downloadApp();
    const unpacked = await JSZip.loadAsync(appZip);
    const workpath = config.workPath();

    // TODO: Do in parallel? Probably overkill though
    await rmdir(path.join(workpath, 'data-items'));
    await rmdir(path.join(workpath, 'data-source-items'));
    await rmdir(path.join(workpath, 'data-objects'));
    // TODO: Do in parallel? Probably overkill though
    await fs.mkdir(path.join(workpath, 'data-items'));
    await fs.mkdir(path.join(workpath, 'data-source-items'));
    await fs.mkdir(path.join(workpath, 'data-objects'));

    let operations = [];
    let dataItemsMeta = await unpacked.file('data-items.json').async('text');
    dataItemsMeta = JSON.parse(dataItemsMeta);

    for (let fileName in unpacked.files) {
      if (!fileName.startsWith('data-items/')) {
        continue;
      }

      let file = unpacked.file(fileName);
      let dataItemFileCreation = new Promise(function(resolve, reject) {
        let localFileName = path.join(workpath, fileName);
        mkdirp(path.dirname(localFileName), function() {
          file.nodeStream()
            .pipe(fs.createWriteStream(localFileName))
            .on('finish', function() {
              resolve();
            });
        });
      }).then(function() {
        let currentFileKey = fileName.substring('data-items/'.length);
        let currentFileMeta = dataItemsMeta.find(m => m.name === currentFileKey);
        let metaContent = {
          category: currentFileMeta.category
        };
        if (currentFileMeta.key)
          metaContent.key = currentFileMeta.key;

        return fs.writeFile(path.join(workpath, fileName + '.meta.json'), JSON.stringify(metaContent));
      });

      operations.push(dataItemFileCreation);
    }

    if ('data-objects.json' in unpacked.files) {
      operations.push(new Promise(function(resolve, reject) {
        unpacked.file('data-objects.json').async('text').then(function(dataObjectsRaw) {
          let dataObjects = JSON.parse(dataObjectsRaw);
          let dataObjectOperations = [];

          for (let dataObject of dataObjects) {
            let code;
            if (dataObject.stylesheet) {
              code = Buffer.from(dataObject.stylesheet, 'base64').toString('utf8');
            } else {
              code = '';
            }
            dataObject.stylesheet = '<See corresponding .py file>';
            let pyName = dataObject.name + '.py';
            let metaName = dataObject.name + '.meta.json';
            let pyOperation = fs.writeFile(path.join(workpath, 'data-objects', pyName), code);
            let metaOperation = fs.writeFile(path.join(workpath, 'data-objects', metaName), json_stringify_readable(dataObject));

            dataObjectOperations.push(pyOperation, metaOperation);
          }

          Promise.all(dataObjectOperations).then(resolve);
        });
      }));
    }

    // TODO: Duplicated code form data-objects
    if ('data-source-items.json' in unpacked.files) {
      operations.push(new Promise(function(resolve, reject) {
        unpacked.file('data-source-items.json').async('text').then(function(dataSourceItemsRaw) {
          let dataSourceItems = JSON.parse(dataSourceItemsRaw);
          let dataSourceItemOperations = [];

          for (let dataSourceItem of dataSourceItems) {
            let code;
            if (dataSourceItem.stylesheet) {
              code = Buffer.from(dataSourceItem.stylesheet, 'base64').toString('utf8');
            }
            else {
              code = '';
            }
            dataSourceItem.stylesheet = '<See corresponding .py file>';
            let pyName = dataSourceItem.name + '.py';
            let metaName = dataSourceItem.name + '.meta.json';

            let pyOperation = fs.writeFile(path.join(workpath, 'data-source-items', pyName), code);
            let metaOperation = fs.writeFile(path.join(workpath, 'data-source-items', metaName), json_stringify_readable(dataSourceItem));

            dataSourceItemOperations.push(pyOperation, metaOperation);
          }

          Promise.all(dataSourceItemOperations).then(resolve);
        });
      }));
    }

    await Promise.all(operations);
    await this.dumpState();
  }
}

// TODO: Defer instantiation to the caller instead.
module.exports = {
  Sync: Sync
}
<commit_msg>Fix for meta data not being used. Issue was that the variable got redefined in the context of the try catch<commit_after>const config = require('./config');
const api = require('./api');
const md5File = require('md5-file/promise');
const fs = require('fs-extra');
const path = require('path');
const mkdirp = require('mkdirp');
const JSZip = require('jszip');

function json_stringify_readable(content) {
  return JSON.stringify(content, null, 4);
}

// Remove dir with files recursively
const rmdir = async function(dir) {
    // TODO: Handle file operations concurrently, but probably is overkill.
    let files = await fs.readdir(dir);

    for (let i = 0; i < files.length; i++) {
      let file = files[i];
      let fileName = path.join(dir, file);
      let stat = fs.statSync(fileName);

      if (fileName == "." || fileName == "..") {
          // pass these files
          continue;
      } else if (stat.isDirectory()) {
          // rmdir recursively
          await rmdir(fileName);
      } else {
          // rm filename
          await fs.unlink(fileName);
      }
    }
    await fs.rmdir(dir);
};

/* TODO: I'm absolutely certain there is a good use case for generators here,
   but i can't figure out how to use them with nested promises.
   Is there a workaround without babel until this is available?
   https://github.com/tc39/proposal-async-iteration
*/
/* TODO: Can be optimized by not simply awaiting everything. Next file could be read in parallel. */
async function listFiles(folder, filter) {
  let result = [];

  let files = await fs.readdir(folder);
  for (let i = 0; i < files.length; i++) {
    let file = files[i];
    let fileWithPath = path.join(folder, file);

    if (filter && !filter(fileWithPath)) {
      continue;
    }
    let stats = await fs.stat(fileWithPath);
    if (stats.isDirectory()) {
      let subFiles = await listFiles(path.join(folder, file), filter);
      subFiles.forEach(subFile => {
        result.push(path.join(file, subFile));
      });
    } else {
      result.push(file);
    }
  }

  return result.slice(0);
}

function filterNoNodeModules(fileName) {
  return fileName.indexOf('node_modules/') === -1;
}

function filterMetaFiles(fileName) {
  return !fileName.endsWith('.meta.json');
}

class DynappObjects {
  constructor(folder, fileExt, filter) {
    this.folder = folder;
    // TODO: This is not good, dirt hacky workaround.
    // Should just use a single meta-file per type (data-items, data-source-items, data-objects)
    this.fileExt = fileExt || '';
    this.filter = filter || (() => true);
  }

  _objectsPath () {
    return path.join(config.workPath(), this.folder);
  }

  async upload () {
    let objectsPath = this._objectsPath();
    let operations = [];
    let [newObjects, changedObjects, deletedObjects] = await this.dirty();

    for (let obj of newObjects) {
      operations.push(this.createObject(obj, path.join(objectsPath, obj)));
    }
    for (let obj of changedObjects) {
      operations.push(this.updateObject(obj, path.join(objectsPath, obj)));
    }
    for (let obj of deletedObjects) {
      operations.push(this.deleteObject(obj).catch(err => {
        // File has been removed by other means, everything is good
        if (err.statusCode && err.statusCode === 404)
          return err.message;
        else
          throw err;
      }));
    }

    return await Promise.all(operations);
  }

  createObject (obj, file) {
    throw new Error('Not implemented');
  }

  updateObject (obj, file) {
    throw new Error('Not implemented');
  }

  deleteObject (obj, file) {
    throw new Error('Not implemented');
  }

  async hashes() {
    let operations = [];
    let objectsPath = this._objectsPath();
    // TODO: Reuse list from dirty() and hashes() ?
    let localFiles = await listFiles(objectsPath, this.filter);

    for (let i = 0; i < localFiles.length; i++) {
      let fileName = localFiles[i];
      let operation = md5File(path.join(objectsPath, fileName)).then(function(hash) {
        return {
          name: fileName,
          hash: hash
        };
      });

      operations.push(operation);
    }

    let files = await Promise.all(operations);
    let result = {};

    for (let i = 0; i < files.length; i++) {
      let file = files[i];
      result[file.name] = {
        hash: file.hash
      };
    }

    return result;
  }

  setHashes (hashes) {
    this._hashes = hashes;
  }

  async readMeta (file) {
    let metaFilePath = file + '.meta.json';
    let metaRaw = '{}'
    try {
      metaRaw = await fs.readFile(metaFilePath, 'utf8');
    } catch (err) {
      if (err.code !== 'ENOENT')
        throw err;
    }
    return JSON.parse(metaRaw);
  }

  async generateMeta (file) {
    // Generates a meta json file, used for updating data-sources and -objects
    let metaFilePath = file.substring(0, file.lastIndexOf('.py')) + '.meta.json';

    let bodyRaw = await fs.readFile(file, 'utf8');
    let body = Buffer.from(bodyRaw).toString('base64');

    let metaRaw = '{}';
    try {
      metaRaw = await fs.readFile(metaFilePath, 'utf8');
    } catch(err) {
      if (err.code !== 'ENOENT') {
        throw err;
      }
    }
    let meta = JSON.parse(metaRaw);

    meta.stylesheet = body;
    return json_stringify_readable(meta);
  }

  async dirty() {
    let objectsPath = this._objectsPath();
    let localFiles = await listFiles(objectsPath, this.filter);
    let newObjects = [];
    let deletedObjects = [];
    let changedObjectsOperations = [];

    for (let file of localFiles) {
      if (file in this._hashes) {
        let operation = md5File(path.join(objectsPath, file)).then((hash) => {
          return this._hashes[file].hash !== hash ? file : null;
        });
        changedObjectsOperations.push(operation);
      } else if (!file.endsWith('.meta.json')) {
        newObjects.push(file);
      }
    }

    for (let fileName in this._hashes) {
      if (localFiles.indexOf(fileName) === -1) {
        deletedObjects.push(fileName);
      }
    }

    // Remove all nulls returned in above loop
    let changedObjects = (await Promise.all(changedObjectsOperations))
      .filter(item => !!item);
    changedObjects = changedObjects.map(item => item.replace(/\.meta\.json$/, this.fileExt));
    // Remove duplicates. We might have caught meta files that we formatted to be origin files.
    // Because if meta changes origin should be updated.
    changedObjects = [...new Set(changedObjects)];

    return [newObjects, changedObjects, deletedObjects];
  }
}

class DataItems extends DynappObjects {
  constructor() {
    super('data-items', null, filterNoNodeModules);
  }

  async createObject (dataItem, file) {
    // TODO: Should we use PUT? Then we have to catch eventual error and try PUT if POST already exists.
    // Seems like PUT just overrides everything, and that is kind of what we want.
    return await api.updateDataItem(dataItem, fs.createReadStream(file), await this.readMeta(file));
  }

  async updateObject (dataItem, file) {
    return await api.updateDataItem(dataItem, fs.createReadStream(file), await this.readMeta(file));
  }

  deleteObject (dataItem) {
    return api.deleteDataItem(dataItem);
  }
}

class DataSourceItems extends DynappObjects {
  constructor() {
    super('data-source-items', '.py');
  }

  async createObject (dataSourceItem, file) {
    return await api.updateDataSourceItem(dataSourceItem.substring(0, dataSourceItem.lastIndexOf('.py')), await this.generateMeta(file));
  }

  async updateObject (dataSourceItem, file) {
    return await api.updateDataSourceItem(dataSourceItem.substring(0, dataSourceItem.lastIndexOf('.py')), await this.generateMeta(file));
  }

  deleteObject (dataSourceItem) {
    return api.deleteDataSourceItem(dataSourceItem);
  }
}

class DataObjects extends DynappObjects {
  constructor() {
    super('data-objects', '.py');
  }

  async createObject (dataObject, file) {
    return await api.updateDataObject(dataObject.substring(0, dataObject.lastIndexOf('.py')), await this.generateMeta(file));
  }

  async updateObject (dataObject, file) {
    return await api.updateDataObject(dataObject.substring(0, dataObject.lastIndexOf('.py')), await this.generateMeta(file));
  }

  deleteObject (dataObject) {
    return api.deleteDataObject(dataObject);
  }
}

class Sync {
  constructor () {
    this.dataItems = new DataItems();
    this.dataSourceItems = new DataSourceItems();
    this.dataObjects = new DataObjects();
  }

  getStateFilePath () {
    return config.workFilePath('.dynapp-state');
  }

  async state () {
    let stateContent;

    try {
      stateContent = await fs.readFile(this.getStateFilePath(), 'utf8');
    } catch(err) {
      // Ensure we have a state-file
      if (err.code === 'ENOENT') {
        stateContent = json_stringify_readable({});
        await fs.writeFile(this.getStateFilePath(), stateContent, 'utf8');
      } else {
        throw err;
      }
    }

    return JSON.parse(stateContent);
  }

  async setHashes () {
    let state = await this.state();
    this.dataItems.setHashes(state['data-items'] || {});
    this.dataSourceItems.setHashes(state['data-source-items'] || {});
    this.dataObjects.setHashes(state['data-objects'] || {});
  }

  async dumpState () {
    let hashes = await Promise.all([
      this.dataItems.hashes(),
      this.dataSourceItems.hashes(),
      this.dataObjects.hashes()
    ]);

    let state = {
      'data-items': hashes[0],
      'data-source-items': hashes[1],
      'data-objects': hashes[2]
    };
    return await fs.writeFile(this.getStateFilePath(), json_stringify_readable(state), 'utf8');
  }

  async upload () {
    await this.setHashes();

    await Promise.all([
      this.dataItems.upload(),
      this.dataSourceItems.upload(),
      this.dataObjects.upload()
    ]);

    await this.dumpState();
  }

  async download () {
    // TODO: Break into parts and put in respective class

    // TODO: Now we load all of the zip into memory.
    // Can we stream it somehow?
    const appZip = await api.downloadApp();
    const unpacked = await JSZip.loadAsync(appZip);
    const workpath = config.workPath();

    // TODO: Do in parallel? Probably overkill though
    await rmdir(path.join(workpath, 'data-items'));
    await rmdir(path.join(workpath, 'data-source-items'));
    await rmdir(path.join(workpath, 'data-objects'));
    // TODO: Do in parallel? Probably overkill though
    await fs.mkdir(path.join(workpath, 'data-items'));
    await fs.mkdir(path.join(workpath, 'data-source-items'));
    await fs.mkdir(path.join(workpath, 'data-objects'));

    let operations = [];
    let dataItemsMeta = await unpacked.file('data-items.json').async('text');
    dataItemsMeta = JSON.parse(dataItemsMeta);

    for (let fileName in unpacked.files) {
      if (!fileName.startsWith('data-items/')) {
        continue;
      }

      let file = unpacked.file(fileName);
      let dataItemFileCreation = new Promise(function(resolve, reject) {
        let localFileName = path.join(workpath, fileName);
        mkdirp(path.dirname(localFileName), function() {
          file.nodeStream()
            .pipe(fs.createWriteStream(localFileName))
            .on('finish', function() {
              resolve();
            });
        });
      }).then(function() {
        let currentFileKey = fileName.substring('data-items/'.length);
        let currentFileMeta = dataItemsMeta.find(m => m.name === currentFileKey);
        let metaContent = {
          category: currentFileMeta.category
        };
        if (currentFileMeta.key)
          metaContent.key = currentFileMeta.key;

        return fs.writeFile(path.join(workpath, fileName + '.meta.json'), JSON.stringify(metaContent));
      });

      operations.push(dataItemFileCreation);
    }

    if ('data-objects.json' in unpacked.files) {
      operations.push(new Promise(function(resolve, reject) {
        unpacked.file('data-objects.json').async('text').then(function(dataObjectsRaw) {
          let dataObjects = JSON.parse(dataObjectsRaw);
          let dataObjectOperations = [];

          for (let dataObject of dataObjects) {
            let code;
            if (dataObject.stylesheet) {
              code = Buffer.from(dataObject.stylesheet, 'base64').toString('utf8');
            } else {
              code = '';
            }
            dataObject.stylesheet = '<See corresponding .py file>';
            let pyName = dataObject.name + '.py';
            let metaName = dataObject.name + '.meta.json';
            let pyOperation = fs.writeFile(path.join(workpath, 'data-objects', pyName), code);
            let metaOperation = fs.writeFile(path.join(workpath, 'data-objects', metaName), json_stringify_readable(dataObject));

            dataObjectOperations.push(pyOperation, metaOperation);
          }

          Promise.all(dataObjectOperations).then(resolve);
        });
      }));
    }

    // TODO: Duplicated code form data-objects
    if ('data-source-items.json' in unpacked.files) {
      operations.push(new Promise(function(resolve, reject) {
        unpacked.file('data-source-items.json').async('text').then(function(dataSourceItemsRaw) {
          let dataSourceItems = JSON.parse(dataSourceItemsRaw);
          let dataSourceItemOperations = [];

          for (let dataSourceItem of dataSourceItems) {
            let code;
            if (dataSourceItem.stylesheet) {
              code = Buffer.from(dataSourceItem.stylesheet, 'base64').toString('utf8');
            }
            else {
              code = '';
            }
            dataSourceItem.stylesheet = '<See corresponding .py file>';
            let pyName = dataSourceItem.name + '.py';
            let metaName = dataSourceItem.name + '.meta.json';

            let pyOperation = fs.writeFile(path.join(workpath, 'data-source-items', pyName), code);
            let metaOperation = fs.writeFile(path.join(workpath, 'data-source-items', metaName), json_stringify_readable(dataSourceItem));

            dataSourceItemOperations.push(pyOperation, metaOperation);
          }

          Promise.all(dataSourceItemOperations).then(resolve);
        });
      }));
    }

    await Promise.all(operations);
    await this.dumpState();
  }
}

// TODO: Defer instantiation to the caller instead.
module.exports = {
  Sync: Sync
}
<|endoftext|>


<commit_before>// Client for pump.io
//
// Copyright 2012 E14N https://e14n.com/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

var databank = require("databank"),
    Stamper = require("../stamper").Stamper,
    crypto  = require("crypto"),
    _ = require("underscore"),
    randomString = require("../randomstring").randomString,
    uuidv5 = require("../uuidv5"),
    DatabankObject = databank.DatabankObject,
    NoSuchThingError = databank.NoSuchThingError;

var Client = DatabankObject.subClass("client");

Client.schema = {
    pkey: "consumer_key",
    fields: ["title",
             "description",
             "host",
             "webfinger",
             "secret",
             "contacts",
             "logo_url",
             "redirect_uris",
             "type",
             "created",
             "updated"],
    indices: ["host", "webfinger"]
};

Client.keyPair = function(callback) {
    randomString(16, function(err, consumer_key) {
        if (err) {
            callback(err, null);
        } else {
            randomString(32, function(err, secret) {
                if (err) {
                    callback(err, null);
                } else {
                    callback(null, {consumer_key: consumer_key,
                                    secret: secret});
                }
            });
        }
    });
};

// For creating

Client.beforeCreate = function(properties, callback) {

    var now = Stamper.stamp();

    properties.created = properties.updated = now;

    if (_(properties).has("host") && _(properties).has("webfinger")) {
        callback(new Error("can't have both a 'host' and 'webfinger' property"), null);
        return;
    }

    if (properties.consumer_key) {
        callback(null, properties);
    } else {
        Client.keyPair(function(err, pair) {
            if (err) {
                callback(err, null);
            } else {
                properties.consumer_key = pair.consumer_key;
                properties.secret       = pair.secret;

                callback(null, properties);
            }
        });
    }
};

Client.prototype.beforeUpdate = function(newClient, callback) {

    var now = Stamper.stamp();

    newClient.updated      = now;
    newClient.created      = this.created;
    newClient.consumer_key = this.consumer_key;
    newClient.secret       = this.secret;
    newClient.owner        = this.owner;

    callback(null, newClient);
};

Client.prototype.asActivityObject = function(callback) {

    var client = this,
        ActivityObject = require("./activityobject").ActivityObject,
        props = {};

    props._consumer_key = client.consumer_key;

    if (client.title) {
        props.displayName = client.title;
    }

    if (client.description) {
        props.content = client.description;
    }

    if (client.webfinger) {
        props.id = ActivityObject.canonicalID(client.webfinger);
        props.objectType = ActivityObject.PERSON;
    } else if (client.host) {
        props.url = "http://"+client.host+"/";
        props.id = props.url; // XXX: ???
        props.objectType = ActivityObject.SERVICE;
    } else {
	props.id = "urn:uuid:"+uuidv5(client.consumer_key);
        switch (client.type) {
        case "web":
            props.objectType = ActivityObject.SERVICE;
            break;
        case "native":
        default:
            props.objectType = ActivityObject.APPLICATION;
            break;
        }
    }

    ActivityObject.ensureObject(props, callback);
};

exports.Client = Client;
<commit_msg>Some people are getting no-key clients. Trying to avoid problems<commit_after>// Client for pump.io
//
// Copyright 2012 E14N https://e14n.com/
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

var databank = require("databank"),
    Stamper = require("../stamper").Stamper,
    crypto  = require("crypto"),
    _ = require("underscore"),
    randomString = require("../randomstring").randomString,
    uuidv5 = require("../uuidv5"),
    DatabankObject = databank.DatabankObject,
    NoSuchThingError = databank.NoSuchThingError;

var Client = DatabankObject.subClass("client");

Client.schema = {
    pkey: "consumer_key",
    fields: ["title",
             "description",
             "host",
             "webfinger",
             "secret",
             "contacts",
             "logo_url",
             "redirect_uris",
             "type",
             "created",
             "updated"],
    indices: ["host", "webfinger"]
};

Client.keyPair = function(callback) {
    randomString(16, function(err, consumer_key) {
        if (err) {
            callback(err, null);
        } else {
            randomString(32, function(err, secret) {
                if (err) {
                    callback(err, null);
                } else {
                    callback(null, {consumer_key: consumer_key,
                                    secret: secret});
                }
            });
        }
    });
};

// For creating

Client.beforeCreate = function(properties, callback) {

    var now = Stamper.stamp();

    properties.created = properties.updated = now;

    if (_(properties).has("host") && _(properties).has("webfinger")) {
        callback(new Error("can't have both a 'host' and 'webfinger' property"), null);
        return;
    }

    if (properties.consumer_key) {
        callback(null, properties);
    } else {
        Client.keyPair(function(err, pair) {
            if (err) {
                callback(err, null);
            } else {
                properties.consumer_key = pair.consumer_key;
                properties.secret       = pair.secret;

                callback(null, properties);
            }
        });
    }
};

Client.prototype.beforeUpdate = function(newClient, callback) {

    var now = Stamper.stamp();

    newClient.updated      = now;
    newClient.created      = this.created;
    newClient.consumer_key = this.consumer_key;
    newClient.secret       = this.secret;
    newClient.owner        = this.owner;

    callback(null, newClient);
};

Client.prototype.asActivityObject = function(callback) {

    var client = this,
        ActivityObject = require("./activityobject").ActivityObject,
        props = {};

    props._consumer_key = client.consumer_key;

    if (client.title) {
        props.displayName = client.title;
    }

    if (client.description) {
        props.content = client.description;
    }

    if (client.webfinger) {
        props.id = ActivityObject.canonicalID(client.webfinger);
        props.objectType = ActivityObject.PERSON;
    } else if (client.host) {
        props.url = "http://"+client.host+"/";
        props.id = props.url; // XXX: ???
        props.objectType = ActivityObject.SERVICE;
    } else {
        // Not sure why this wouldn't be a string, but... OK.
        if (_.isString(client.consumer_key)) {
	    props.id = "urn:uuid:"+uuidv5(client.consumer_key);
        }
        switch (client.type) {
        case "web":
            props.objectType = ActivityObject.SERVICE;
            break;
        case "native":
        default:
            props.objectType = ActivityObject.APPLICATION;
            break;
        }
    }

    ActivityObject.ensureObject(props, callback);
};

exports.Client = Client;
<|endoftext|>


<commit_before>/*
Copyright 2015, 2016 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";

/**
 * This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for
 * the public classes.
 * @module models/event
 */

/**
 * Enum for event statuses.
 * @readonly
 * @enum {string}
 */
module.exports.EventStatus = {
    /** The event was not sent and will no longer be retried. */
    NOT_SENT: "not_sent",

    /** The message is being encrypted */
    ENCRYPTING: "encrypting",

    /** The event is in the process of being sent. */
    SENDING: "sending",
    /** The event is in a queue waiting to be sent. */
    QUEUED: "queued",
    /** The event has been sent to the server, but we have not yet received the
     * echo. */
    SENT: "sent",

    /** The event was cancelled before it was successfully sent. */
    CANCELLED: "cancelled",
};

/**
 * Construct a Matrix Event object
 * @constructor
 *
 * @param {Object} event The raw event to be wrapped in this DAO
 *
 * @param {Object=} clearEvent For encrypted events, the plaintext payload for
 * the event (typically containing <tt>type</tt> and <tt>content</tt> fields).
 *
 * @prop {Object} event The raw (possibly encrypted) event. Do not access
 * this property directly unless you absolutely have to. Prefer the getter
 * methods defined on this class. Using the getter methods shields your app
 * from changes to event JSON between Matrix versions.
 *
 * @prop {RoomMember} sender The room member who sent this event, or null e.g.
 * this is a presence event.
 * @prop {RoomMember} target The room member who is the target of this event, e.g.
 * the invitee, the person being banned, etc.
 * @prop {EventStatus} status The sending status of the event.
 * @prop {boolean} forwardLooking True if this event is 'forward looking', meaning
 * that getDirectionalContent() will return event.content and not event.prev_content.
 * Default: true. This property is experimental and may change.
 */
module.exports.MatrixEvent = function MatrixEvent(event, clearEvent) {
    this.event = event || {};
    this.sender = null;
    this.target = null;
    this.status = null;
    this.forwardLooking = true;

    this._clearEvent = clearEvent || {};
    this._pushActions = null;
};

module.exports.MatrixEvent.prototype = {

    /**
     * Get the event_id for this event.
     * @return {string} The event ID, e.g. $143350589368169JsLZx:localhost
     *
     */
    getId: function() {
        return this.event.event_id;
    },

    /**
     * Get the user_id for this event.
     * @return {string} The user ID, e.g. @alice:matrix.org
     */
    getSender: function() {
        return this.event.sender || this.event.user_id; // v2 / v1
    },

    /**
     * Get the (decrypted, if necessary) type of event.
     *
     * @return {string} The event type, e.g. m.room.message
     */
    getType: function() {
        return this._clearEvent.type || this.event.type;
    },

    /**
     * Get the (possibly encrypted) type of the event that will be sent to the
     * homeserver.
     *
     * @return {string} The event type.
     */
    getWireType: function() {
        return this.event.type;
    },

    /**
     * Get the room_id for this event. This will return undefined
     * for m.presence events.
     * @return {string} The room ID, e.g. !cURbafjkfsMDVwdRDQ:matrix.org
     *
     */
    getRoomId: function() {
        return this.event.room_id;
    },

    /**
     * Get the timestamp of this event.
     * @return {Number} The event timestamp, e.g. 1433502692297
     */
    getTs: function() {
        return this.event.origin_server_ts;
    },

    /**
     * Get the (decrypted, if necessary) event content JSON.
     *
     * @return {Object} The event content JSON, or an empty object.
     */
    getContent: function() {
        return this._clearEvent.content || this.event.content || {};
    },

    /**
     * Get the (possibly encrypted) event content JSON that will be sent to the
     * homeserver.
     *
     * @return {Object} The event content JSON, or an empty object.
     */
    getWireContent: function() {
        return this.event.content || {};
    },

    /**
     * Get the previous event content JSON. This will only return something for
     * state events which exist in the timeline.
     * @return {Object} The previous event content JSON, or an empty object.
     */
    getPrevContent: function() {
        // v2 then v1 then default
        return this.getUnsigned().prev_content || this.event.prev_content || {};
    },

    /**
     * Get either 'content' or 'prev_content' depending on if this event is
     * 'forward-looking' or not. This can be modified via event.forwardLooking.
     * In practice, this means we get the chronologically earlier content value
     * for this event (this method should surely be called getEarlierContent)
     * This method is experimental and may change.
     * @return {Object} event.content if this event is forward-looking, else
     * event.prev_content.
     */
    getDirectionalContent: function() {
        return this.forwardLooking ? this.getContent() : this.getPrevContent();
    },

    /**
     * Get the age of this event. This represents the age of the event when the
     * event arrived at the device, and not the age of the event when this
     * function was called.
     * @return {Number} The age of this event in milliseconds.
     */
    getAge: function() {
        return this.getUnsigned().age || this.event.age; // v2 / v1
    },

    /**
     * Get the event state_key if it has one. This will return undefined
     *  for message events.
     * @return {string} The event's state_key.
     */
    getStateKey: function() {
        return this.event.state_key;
    },

    /**
     * Check if this event is a state event.
     * @return {boolean} True if this is a state event.
     */
    isState: function() {
        return this.event.state_key !== undefined;
    },

    /**
     * Replace the content of this event with encrypted versions.
     * (This is used when sending an event; it should not be used by applications).
     *
     * @internal
     *
     * @param {string} crypto_type type of the encrypted event - typically
     * <tt>"m.room.encrypted"</tt>
     *
     * @param {object} crypto_content raw 'content' for the encrypted event.
     * @param {object} keys The local keys claimed and proved by this event.
     */
    makeEncrypted: function(crypto_type, crypto_content, keys) {
        // keep the plain-text data for 'view source'
        this._clearEvent = {
            type: this.event.type,
            content: this.event.content,
            keysProved: keys,
            keysClaimed: keys,
        };
        this.event.type = crypto_type;
        this.event.content = crypto_content;
    },

    /**
     * Check if the event is encrypted.
     * @return {boolean} True if this event is encrypted.
     */
    isEncrypted: function() {
        return Boolean(this._clearEvent.type);
    },

    /**
     * The curve25519 key that sent this event
     * @return {string}
     */
    getSenderKey: function() {
        return this.getKeysProved().curve25519 || null;
    },

    /**
     * The keys that must have been owned by the sender of this encrypted event.
     * These don't necessarily have to come from this event itself, but may be
     * implied by the cryptographic session.
     * For example megolm messages don't prove keys directly, but instead
     * inherit a proof from the olm message that established the session.
     * @return {object}
     */
    getKeysProved: function() {
        return this._clearEvent.keysProved || {};
    },

    /**
     * The additional keys the sender of this encrypted event claims to possess
     * These don't necessarily have to come from this event itself, but may be
     * implied by the cryptographic session.
     * For example megolm messages don't claim keys directly, but instead
     * inherit a claim from the olm message that established the session.
     * @return {object}
     */
    getKeysClaimed: function() {
        return this._clearEvent.keysClaimed || {};
    },

    getUnsigned: function() {
        return this.event.unsigned || {};
    },

    /**
     * Update the content of an event in the same way it would be by the server
     * if it were redacted before it was sent to us
     *
     * @param {module:models/event.MatrixEvent} redaction_event
     *     event causing the redaction
     */
    makeRedacted: function(redaction_event) {
        // quick sanity-check
        if (!redaction_event.event) {
            throw new Error("invalid redaction_event in makeRedacted");
        }

        // we attempt to replicate what we would see from the server if
        // the event had been redacted before we saw it.
        //
        // The server removes (most of) the content of the event, and adds a
        // "redacted_because" key to the unsigned section containing the
        // redacted event.
        if (!this.event.unsigned) {
            this.event.unsigned = {};
        }
        this.event.unsigned.redacted_because = redaction_event.event;

        var key;
        for (key in this.event) {
            if (!this.event.hasOwnProperty(key)) { continue; }
            if (!_REDACT_KEEP_KEY_MAP[key]) {
                delete this.event[key];
            }
        }

        var keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
        var content = this.getContent();
        for (key in content) {
            if (!content.hasOwnProperty(key)) { continue; }
            if (!keeps[key]) {
                delete content[key];
            }
        }
    },

    /**
     * Check if this event has been redacted
     *
     * @return {boolean} True if this event has been redacted
     */
    isRedacted: function() {
        return Boolean(this.getUnsigned().redacted_because);
    },

    /**
     * Get the push actions, if known, for this event
     *
     * @return {?Object} push actions
     */
     getPushActions: function() {
        return this._pushActions;
     },

    /**
     * Set the push actions for this event.
     *
     * @param {Object} pushActions push actions
     */
     setPushActions: function(pushActions) {
        this._pushActions = pushActions;
     },
};

/* http://matrix.org/docs/spec/r0.0.1/client_server.html#redactions says:
 *
 * the server should strip off any keys not in the following list:
 *    event_id
 *    type
 *    room_id
 *    user_id
 *    state_key
 *    prev_state
 *    content
 *    [we keep 'unsigned' as well, since that is created by the local server]
 *
 * The content object should also be stripped of all keys, unless it is one of
 * one of the following event types:
 *    m.room.member allows key membership
 *    m.room.create allows key creator
 *    m.room.join_rules allows key join_rule
 *    m.room.power_levels allows keys ban, events, events_default, kick,
 *        redact, state_default, users, users_default.
 *    m.room.aliases allows key aliases
 */
// a map giving the keys we keep when an event is redacted
var _REDACT_KEEP_KEY_MAP = [
    'event_id', 'type', 'room_id', 'user_id', 'state_key', 'prev_state',
    'content', 'unsigned',
].reduce(function(ret, val) { ret[val] = 1; return ret; }, {});

// a map from event type to the .content keys we keep when an event is redacted
var _REDACT_KEEP_CONTENT_MAP = {
    'm.room.member': {'membership': 1},
    'm.room.create': {'creator': 1},
    'm.room.join_rules': {'join_rule': 1},
    'm.room.power_levels': {'ban': 1, 'events': 1, 'events_default': 1,
                            'kick': 1, 'redact': 1, 'state_default': 1,
                            'users': 1, 'users_default': 1,
                           },
    'm.room.aliases': {'aliases': 1},
};
<commit_msg>More comments explaining the keysClaimed/keysProved properties<commit_after>/*
Copyright 2015, 2016 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";

/**
 * This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for
 * the public classes.
 * @module models/event
 */

/**
 * Enum for event statuses.
 * @readonly
 * @enum {string}
 */
module.exports.EventStatus = {
    /** The event was not sent and will no longer be retried. */
    NOT_SENT: "not_sent",

    /** The message is being encrypted */
    ENCRYPTING: "encrypting",

    /** The event is in the process of being sent. */
    SENDING: "sending",
    /** The event is in a queue waiting to be sent. */
    QUEUED: "queued",
    /** The event has been sent to the server, but we have not yet received the
     * echo. */
    SENT: "sent",

    /** The event was cancelled before it was successfully sent. */
    CANCELLED: "cancelled",
};

/**
 * Construct a Matrix Event object
 * @constructor
 *
 * @param {Object} event The raw event to be wrapped in this DAO
 *
 * @param {Object=} clearEvent For encrypted events, the plaintext payload for
 * the event (typically containing <tt>type</tt> and <tt>content</tt> fields).
 *
 * @prop {Object} event The raw (possibly encrypted) event. Do not access
 * this property directly unless you absolutely have to. Prefer the getter
 * methods defined on this class. Using the getter methods shields your app
 * from changes to event JSON between Matrix versions.
 *
 * @prop {RoomMember} sender The room member who sent this event, or null e.g.
 * this is a presence event.
 * @prop {RoomMember} target The room member who is the target of this event, e.g.
 * the invitee, the person being banned, etc.
 * @prop {EventStatus} status The sending status of the event.
 * @prop {boolean} forwardLooking True if this event is 'forward looking', meaning
 * that getDirectionalContent() will return event.content and not event.prev_content.
 * Default: true. This property is experimental and may change.
 */
module.exports.MatrixEvent = function MatrixEvent(event, clearEvent) {
    this.event = event || {};
    this.sender = null;
    this.target = null;
    this.status = null;
    this.forwardLooking = true;

    this._clearEvent = clearEvent || {};
    this._pushActions = null;
};

module.exports.MatrixEvent.prototype = {

    /**
     * Get the event_id for this event.
     * @return {string} The event ID, e.g. $143350589368169JsLZx:localhost
     *
     */
    getId: function() {
        return this.event.event_id;
    },

    /**
     * Get the user_id for this event.
     * @return {string} The user ID, e.g. @alice:matrix.org
     */
    getSender: function() {
        return this.event.sender || this.event.user_id; // v2 / v1
    },

    /**
     * Get the (decrypted, if necessary) type of event.
     *
     * @return {string} The event type, e.g. m.room.message
     */
    getType: function() {
        return this._clearEvent.type || this.event.type;
    },

    /**
     * Get the (possibly encrypted) type of the event that will be sent to the
     * homeserver.
     *
     * @return {string} The event type.
     */
    getWireType: function() {
        return this.event.type;
    },

    /**
     * Get the room_id for this event. This will return undefined
     * for m.presence events.
     * @return {string} The room ID, e.g. !cURbafjkfsMDVwdRDQ:matrix.org
     *
     */
    getRoomId: function() {
        return this.event.room_id;
    },

    /**
     * Get the timestamp of this event.
     * @return {Number} The event timestamp, e.g. 1433502692297
     */
    getTs: function() {
        return this.event.origin_server_ts;
    },

    /**
     * Get the (decrypted, if necessary) event content JSON.
     *
     * @return {Object} The event content JSON, or an empty object.
     */
    getContent: function() {
        return this._clearEvent.content || this.event.content || {};
    },

    /**
     * Get the (possibly encrypted) event content JSON that will be sent to the
     * homeserver.
     *
     * @return {Object} The event content JSON, or an empty object.
     */
    getWireContent: function() {
        return this.event.content || {};
    },

    /**
     * Get the previous event content JSON. This will only return something for
     * state events which exist in the timeline.
     * @return {Object} The previous event content JSON, or an empty object.
     */
    getPrevContent: function() {
        // v2 then v1 then default
        return this.getUnsigned().prev_content || this.event.prev_content || {};
    },

    /**
     * Get either 'content' or 'prev_content' depending on if this event is
     * 'forward-looking' or not. This can be modified via event.forwardLooking.
     * In practice, this means we get the chronologically earlier content value
     * for this event (this method should surely be called getEarlierContent)
     * This method is experimental and may change.
     * @return {Object} event.content if this event is forward-looking, else
     * event.prev_content.
     */
    getDirectionalContent: function() {
        return this.forwardLooking ? this.getContent() : this.getPrevContent();
    },

    /**
     * Get the age of this event. This represents the age of the event when the
     * event arrived at the device, and not the age of the event when this
     * function was called.
     * @return {Number} The age of this event in milliseconds.
     */
    getAge: function() {
        return this.getUnsigned().age || this.event.age; // v2 / v1
    },

    /**
     * Get the event state_key if it has one. This will return undefined
     *  for message events.
     * @return {string} The event's state_key.
     */
    getStateKey: function() {
        return this.event.state_key;
    },

    /**
     * Check if this event is a state event.
     * @return {boolean} True if this is a state event.
     */
    isState: function() {
        return this.event.state_key !== undefined;
    },

    /**
     * Replace the content of this event with encrypted versions.
     * (This is used when sending an event; it should not be used by applications).
     *
     * @internal
     *
     * @param {string} crypto_type type of the encrypted event - typically
     * <tt>"m.room.encrypted"</tt>
     *
     * @param {object} crypto_content raw 'content' for the encrypted event.
     * @param {object} keys The local keys claimed and proved by this event.
     */
    makeEncrypted: function(crypto_type, crypto_content, keys) {
        // keep the plain-text data for 'view source'
        this._clearEvent = {
            type: this.event.type,
            content: this.event.content,
            keysProved: keys,
            keysClaimed: keys,
        };
        this.event.type = crypto_type;
        this.event.content = crypto_content;
    },

    /**
     * Check if the event is encrypted.
     * @return {boolean} True if this event is encrypted.
     */
    isEncrypted: function() {
        return Boolean(this._clearEvent.type);
    },

    /**
     * The curve25519 key that sent this event
     * @return {string}
     */
    getSenderKey: function() {
        return this.getKeysProved().curve25519 || null;
    },

    /**
     * The keys that must have been owned by the sender of this encrypted event.
     * These don't necessarily have to come from this event itself, but may be
     * implied by the cryptographic session.
     * For example megolm messages don't prove keys directly, but instead
     * inherit a proof from the olm message that established the session.
     * @return {object}
     */
    getKeysProved: function() {
        // The keysProved property usually isn't actually part of the decrypted
        // plaintext. Instead it is added after decryption by the crypto
        // algorithm in lib/crypto/algorithms.
        return this._clearEvent.keysProved || {};
    },

    /**
     * The additional keys the sender of this encrypted event claims to possess
     * These don't necessarily have to come from this event itself, but may be
     * implied by the cryptographic session.
     * For example megolm messages don't claim keys directly, but instead
     * inherit a claim from the olm message that established the session.
     * @return {object}
     */
    getKeysClaimed: function() {
        // The keysClaimed property usually isn't actually part of the
        // decrypted plaintext. Instead it is added after decryption by the
        // crypto algorithm in lib/crypto/algorithms.
        return this._clearEvent.keysClaimed || {};
    },

    getUnsigned: function() {
        return this.event.unsigned || {};
    },

    /**
     * Update the content of an event in the same way it would be by the server
     * if it were redacted before it was sent to us
     *
     * @param {module:models/event.MatrixEvent} redaction_event
     *     event causing the redaction
     */
    makeRedacted: function(redaction_event) {
        // quick sanity-check
        if (!redaction_event.event) {
            throw new Error("invalid redaction_event in makeRedacted");
        }

        // we attempt to replicate what we would see from the server if
        // the event had been redacted before we saw it.
        //
        // The server removes (most of) the content of the event, and adds a
        // "redacted_because" key to the unsigned section containing the
        // redacted event.
        if (!this.event.unsigned) {
            this.event.unsigned = {};
        }
        this.event.unsigned.redacted_because = redaction_event.event;

        var key;
        for (key in this.event) {
            if (!this.event.hasOwnProperty(key)) { continue; }
            if (!_REDACT_KEEP_KEY_MAP[key]) {
                delete this.event[key];
            }
        }

        var keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
        var content = this.getContent();
        for (key in content) {
            if (!content.hasOwnProperty(key)) { continue; }
            if (!keeps[key]) {
                delete content[key];
            }
        }
    },

    /**
     * Check if this event has been redacted
     *
     * @return {boolean} True if this event has been redacted
     */
    isRedacted: function() {
        return Boolean(this.getUnsigned().redacted_because);
    },

    /**
     * Get the push actions, if known, for this event
     *
     * @return {?Object} push actions
     */
     getPushActions: function() {
        return this._pushActions;
     },

    /**
     * Set the push actions for this event.
     *
     * @param {Object} pushActions push actions
     */
     setPushActions: function(pushActions) {
        this._pushActions = pushActions;
     },
};

/* http://matrix.org/docs/spec/r0.0.1/client_server.html#redactions says:
 *
 * the server should strip off any keys not in the following list:
 *    event_id
 *    type
 *    room_id
 *    user_id
 *    state_key
 *    prev_state
 *    content
 *    [we keep 'unsigned' as well, since that is created by the local server]
 *
 * The content object should also be stripped of all keys, unless it is one of
 * one of the following event types:
 *    m.room.member allows key membership
 *    m.room.create allows key creator
 *    m.room.join_rules allows key join_rule
 *    m.room.power_levels allows keys ban, events, events_default, kick,
 *        redact, state_default, users, users_default.
 *    m.room.aliases allows key aliases
 */
// a map giving the keys we keep when an event is redacted
var _REDACT_KEEP_KEY_MAP = [
    'event_id', 'type', 'room_id', 'user_id', 'state_key', 'prev_state',
    'content', 'unsigned',
].reduce(function(ret, val) { ret[val] = 1; return ret; }, {});

// a map from event type to the .content keys we keep when an event is redacted
var _REDACT_KEEP_CONTENT_MAP = {
    'm.room.member': {'membership': 1},
    'm.room.create': {'creator': 1},
    'm.room.join_rules': {'join_rule': 1},
    'm.room.power_levels': {'ban': 1, 'events': 1, 'events_default': 1,
                            'kick': 1, 'redact': 1, 'state_default': 1,
                            'users': 1, 'users_default': 1,
                           },
    'm.room.aliases': {'aliases': 1},
};
<|endoftext|>


<commit_before>var assert = require('assert')
  , request = require('request')
  , http = require('http');

describe('Restaurant API', function() {
  describe('/restaurants', function() {
    before(function() {
    });
    it('should return list of restaurants', function(done) {
        request.get('http://localhost:3001/restaurants.json', function(err, res, body) {
        assert.ok(body);
        done();
      });
    });
  });
});
<commit_msg>quick change<commit_after>var assert = require('assert')
  , request = require('request')
  , http = require('http');

describe('Restaurant API', function() {
  describe('/restaurants', function() {

    before(function() {
    });

    it('should return list of restaurants', function(done) {
      request.get('http://localhost:3001/restaurants', function(err, res, body) {
        assert.ok(body);
        done();
      });
    });

    it('should handle content negotiation', function(done) {
      assert.ok(false);
      done();
    });

    it('should return restaurant by :id', function(done) {
      request.get('http://localhost:3001/restaurants/43141431', function(err, res, body) {
        assert(false);
        done();
      });
    });
  });
});
<|endoftext|>


<commit_before>const check = require('./checksum');
const crypto = require('crypto');
module.exports = (message) => {
    let res = message.content.trim().substring(6);
    let algo = res.split(' ')[0];
    res = res.substring(algo.length + 1).trim();
    if (crypto.getHashes().indexOf(algo) === -1) {
        message.channel.sendMessage('Invalid algorithm \n Valid hashing Algorithms are:\n' + crypto.getHashes().join(' , '));
        return;
    }
    res = checksum(res, algo);
    console.log(res); message.channel.sendMessage('hash of message:' + res);
    if (message.attachments.size > 0) {
        res = message.attachments.first().url;
        request(res).then(body => message.channel.sendMessage('Hash of file:\n' + checksum(body, algo)));
    }
}<commit_msg>Fix checksum<commit_after>const checksum = require('./checksum');
const crypto = require('crypto');
module.exports = (message) => {
    let res = message.content.trim().substring(6);
    let algo = res.split(' ')[0];
    res = res.substring(algo.length + 1).trim();
    if (crypto.getHashes().indexOf(algo) === -1) {
        message.channel.sendMessage('Invalid algorithm \n Valid hashing Algorithms are:\n' + crypto.getHashes().join(' , '));
        return;
    }
    res = checksum(res, algo);
    console.log(res); message.channel.sendMessage('hash of message:' + res);
    if (message.attachments.size > 0) {
        res = message.attachments.first().url;
        request(res).then(body => message.channel.sendMessage('Hash of file:\n' + checksum(body, algo)));
    }
}<|endoftext|>


<commit_before>var assert = require('assert'),
    request = require('supertest'),
    mongoose = require('mongoose');

//Define tests for the API
describe('Routing', function() {
  var palMessageID;
  var nonPalMessageID;
  var myApp = require('../index');

  before(function(done){
    mongoose.connection.on('connected', function (callback) {
      done();
    });
  });

  //Test POST /api/messages
  describe('Add Message', function() {
    it('Should add palindrome message', function(done) {

      request(myApp)
      .post('/api/messages')
      .send({text:'racecar'})
      .expect('Content-Type', /json/)
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        assert(res.body.isPalindrome);
        palMessageID = res.body._id;
        done();
      });
    });

    it('Should add non palindrome message', function(done) {
      request(myApp)
      .post('/api/messages')
      .send({text:'not palindrome'})
      .expect('Content-Type', /json/)
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        assert.equal(false, res.body.isPalindrome);
        nonPalMessageID = res.body._id;
        done();
      });
    });

    it('Should fail without text', function(done) {
      request(myApp)
      .post('/api/messages')
      .expect('Content-Type', /json/)
      .expect(400)
      .end(function(err, res){
        if (err) return done(err);
        done();
      });
    });
  });

  //Test GET /api/messages
  describe('Get Message', function() {

    it('Should retrieve palindrome message', function(done) {
      request(myApp)
      .get('/api/messages/'+palMessageID)
      .expect('Content-Type', /json/)
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        assert(res.body.isPalindrome);
        done();
      });
    });

    it('Should retrieve non palindrome message', function(done) {
      request(myApp)
      .get('/api/messages/'+nonPalMessageID)
      .expect('Content-Type', /json/)
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        assert.equal(false,res.body.isPalindrome);
        done();
      });
    });

    it('Should fail with improper ID', function(done) {
      request(myApp)
      .get('/api/messages/notarealmessageid')
      .expect('Content-Type', /json/)
      .expect(400)
      .end(function(err, res){
        if (err) return done(err);
        done();
      });
    });
  });

  //Test GET /api/messages
  describe('Get all Messages', function() {

    it('Should retrieve all messages', function(done) {
      request(myApp)
      .get('/api/messages')
      .expect('Content-Type', /json/)
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        assert(res.body.length >= 2);
        done();
      });
    });
  });

  //Test DELETE /api/messages
  describe('Delete Messages', function() {

    it('Should delete a message', function(done) {
      request(myApp)
      .delete('/api/messages/'+palMessageID)
      .expect('Content-Type', /json/)
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        assert(res.body.deleted);
        done();
      });
    });

    it('Should return 400 on invalid message ID', function(done) {
      request(myApp)
      .delete('/api/messages/nothinghere')
      .expect('Content-Type', /json/)
      .expect(400)
      .end(function(err, res){
        if (err) return done(err);
        done();
      });
    });
  });

  //Test DELETE /api/messages
  describe('Misc', function() {

    it('Should return 404 on non existent endpoint - GET', function(done) {
      request(myApp)
      .get('/api/something')
      .expect(404, done);
    });
  });

});<commit_msg>Add test for multi-word palindromes<commit_after>var assert = require('assert'),
    request = require('supertest'),
    mongoose = require('mongoose');

//Define tests for the API
describe('Routing', function() {
  var palMessageID;
  var nonPalMessageID;
  var myApp = require('../index');

  before(function(done){
    mongoose.connection.on('connected', function (callback) {
      done();
    });
  });

  //Test POST /api/messages
  describe('Add Message', function() {
    it('Should add palindrome message', function(done) {

      request(myApp)
      .post('/api/messages')
      .send({text:'racecar'})
      .expect('Content-Type', /json/)
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        assert(res.body.isPalindrome);
        palMessageID = res.body._id;
        done();
      });
    });

    it('Should add non palindrome message', function(done) {
      request(myApp)
      .post('/api/messages')
      .send({text:'not palindrome'})
      .expect('Content-Type', /json/)
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        assert.equal(false, res.body.isPalindrome);
        nonPalMessageID = res.body._id;
        done();
      });
    });

    it('Should add complex palindrome message', function(done) {

      request(myApp)
      .post('/api/messages')
      .send({text:'Poor Dan is in a droop'})
      .expect('Content-Type', /json/)
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        assert(res.body.isPalindrome);
        palMessageID = res.body._id;
        done();
      });
    });

    it('Should fail without text', function(done) {
      request(myApp)
      .post('/api/messages')
      .expect('Content-Type', /json/)
      .expect(400)
      .end(function(err, res){
        if (err) return done(err);
        done();
      });
    });
  });

  //Test GET /api/messages
  describe('Get Message', function() {

    it('Should retrieve palindrome message', function(done) {
      request(myApp)
      .get('/api/messages/'+palMessageID)
      .expect('Content-Type', /json/)
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        assert(res.body.isPalindrome);
        done();
      });
    });

    it('Should retrieve non palindrome message', function(done) {
      request(myApp)
      .get('/api/messages/'+nonPalMessageID)
      .expect('Content-Type', /json/)
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        assert.equal(false,res.body.isPalindrome);
        done();
      });
    });

    it('Should fail with improper ID', function(done) {
      request(myApp)
      .get('/api/messages/notarealmessageid')
      .expect('Content-Type', /json/)
      .expect(400)
      .end(function(err, res){
        if (err) return done(err);
        done();
      });
    });
  });

  //Test GET /api/messages
  describe('Get all Messages', function() {

    it('Should retrieve all messages', function(done) {
      request(myApp)
      .get('/api/messages')
      .expect('Content-Type', /json/)
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        assert(res.body.length >= 2);
        done();
      });
    });
  });

  //Test DELETE /api/messages
  describe('Delete Messages', function() {

    it('Should delete a message', function(done) {
      request(myApp)
      .delete('/api/messages/'+palMessageID)
      .expect('Content-Type', /json/)
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        assert(res.body.deleted);
        done();
      });
    });

    it('Should return 400 on invalid message ID', function(done) {
      request(myApp)
      .delete('/api/messages/nothinghere')
      .expect('Content-Type', /json/)
      .expect(400)
      .end(function(err, res){
        if (err) return done(err);
        done();
      });
    });
  });

  //Test DELETE /api/messages
  describe('Misc', function() {

    it('Should return 404 on non existent endpoint - GET', function(done) {
      request(myApp)
      .get('/api/something')
      .expect(404, done);
    });
  });

});<|endoftext|>