<commit_before>import Deferred from 'dojo/Deferred';
import lang from 'dojo/_base/lang';

import { urls} from 'js/config';
import resources from 'resources';
import { getUrlParams } from 'utils/params';
import arcgisUtils from 'esri/arcgis/utils';
import layerKeys from 'constants/LayerConstants';

const SEPARATOR = ';';

const pruneValues = (dict) => {
  Object.keys(dict).forEach((key) => {
    if (dict[key] === '' || dict[key] === undefined || dict[key] === null) {
      delete dict[key];
    }
  });
  return dict;
};

/**
* Takes a string and parse it into an array based on separator, e.g hey;you; => ['hey', 'you', '']
* This will remove any blanks from the array as well, e.g. ['hey', 'you', ''] => ['hey', 'you']
*/
const parseIntoArray = (resourceString) => {
  return resourceString.split(SEPARATOR).filter((value) => {
    return value !== undefined && value !== '';
  });
};

/**
* This function formats the resources object by adding information to it
* The default values in the resources match the values from AGOL, to make
* them easier to consume in my components, do all the formatting here
*/
const formatResources = () => {
  const appUrl = location.href.replace(location.search, '');
  //- LANGUAGE SETTINGS START
  resources.labels = {};
  resources.labels[resources.language] = {
    title: resources.title,
    subtitle: resources.subtitle,
    narrative: resources.narrative,
    webmapMenuName: resources.webmapMenuName
  };
  //- parse map themes for default laguage if present
  const names = resources.mapThemes ? parseIntoArray(resources.mapThemes) : [];
  const appids = resources.mapThemeIds ? parseIntoArray(resources.mapThemeIds) : [];

  if (names.length === appids.length && names.length > 0) {
    resources.labels[resources.language].themes = [];
    names.forEach((name, i) => {
      let url = `${appUrl}?appid=${appids[i].trim()}`;
      if (appids[i] === '#') {
        url = appUrl + '?';
      }

      resources.labels[resources.language].themes.push({
        label: name.trim(),
        url: url
      });
    });
  }

  const langUrlParam = getUrlParams(location.href).l;
  if (langUrlParam && langUrlParam === resources.alternativeLanguage) {
    resources.useAlternativeLanguage = true;
  }
  //- Add content for second language if configured
  if (resources.useAlternativeLanguage && resources.alternativeLanguage) {
    resources.labels[resources.alternativeLanguage] = {
      title: resources.alternativeLanguageTitle,
      subtitle: resources.alternativeLanguageSubtitle,
      narrative: resources.alternativeNarrative || resources.narrative,
      webmapMenuName: resources.alternativeWebmapMenuName || resources.webmapMenuName
    };
    //- parse map themes for second laguage if present
    const secondNames = resources.alternativeMapThemes ? parseIntoArray(resources.alternativeMapThemes) : [];
    if (secondNames.length === appids.length && names.length > 0) {
      resources.labels[resources.alternativeLanguage].themes = [];
      secondNames.forEach((name, i) => {
        resources.labels[resources.alternativeLanguage].themes.push({
          label: name.trim(),
          url: `${appUrl}?appid=${appids[i].trim()}`
        });
      });
    }
  }
  //- LANGUAGE SETTINGS END

  //- Restoration Module Configurations
  if (resources.restorationModule) {
    //- Parse the restoration module options if they are in AGOL
    if (resources.restorationOptions) {
      let optionLabels = parseIntoArray(resources.restorationOptions);
      const rasterIds = parseIntoArray(resources.restorationOptionsRasterIds);
      //- Make it in a format easier to consume in our components
      resources.labels[resources.language].restorationOptions = [];
      optionLabels.forEach((label, index) => {
        resources.labels[resources.language].restorationOptions.push({
          id: `$${rasterIds[index]}`,
          label: label
        });
      });

      //- Parse the options, colors, and any other content that will be used in the analysis
      resources.slopeAnalysisPotentialColors = parseIntoArray(resources.slopePotentialColors);
      resources.labels[resources.language].slopeAnalysisPotentialOptions = parseIntoArray(resources.slopePotentialOptions);
      //- Add description text
      resources.labels[resources.language].restorationChartDescription = resources.restorationChartDescription;
      resources.labels[resources.language].restorationTableDescription = resources.restorationTableDescription;
      resources.labels[resources.language].slopeDescription = resources.slopeDescription;
      //- Add the slope options in another language if configured
      if (resources.useAlternativeLanguage) {
        // Slope Options
        resources.labels[resources.alternativeLanguage].slopeAnalysisPotentialOptions = parseIntoArray(resources.alternativeSlopePotentialOptions);
        // Restoration Options
        optionLabels = parseIntoArray(resources.alternativeRestorationOptions);
        resources.labels[resources.alternativeLanguage].restorationOptions = [];
        optionLabels.forEach((label, index) => {
          resources.labels[resources.alternativeLanguage].restorationOptions.push({
            id: `$${rasterIds[index]}`,
            label: label
          });
        });
        //- Add description text
        resources.labels[resources.alternativeLanguage].restorationChartDescription = resources.alternativeRestorationChartDescription;
        resources.labels[resources.alternativeLanguage].restorationTableDescription = resources.alternativeRestorationTableDescription;
        resources.labels[resources.alternativeLanguage].slopeDescription = resources.alternativeSlopeDescription;
      }
    }
    //- Parse slope class names if present
    if (resources.slopeClassNames) {
      resources.slopeClasses = parseIntoArray(resources.slopeClassNames);
      resources.slopeColors = parseIntoArray(resources.slopeClassColors);
    }
    //- Parse tree cover class names if present
    if (resources.treeCoverClassNames) {
      resources.treeCoverClasses = parseIntoArray(resources.treeCoverClassNames);
      resources.treeCoverColors = parseIntoArray(resources.treeCoverClassColors);
    }
    //- Parse land cover class names if present
    if (resources.landCoverClassNames) {
      resources.landCoverClasses = parseIntoArray(resources.landCoverClassNames);
      resources.landCoverColors = parseIntoArray(resources.landCoverClassColors);
    }
    //- Parse population density class names if present
    if (resources.populationClassNames) {
      resources.populationClasses = parseIntoArray(resources.populationClassNames);
      resources.populationColors = parseIntoArray(resources.populationClassColors);
    }

    //- Parse rainfall class names if present
    if (resources.rainfallClassNames) {
      resources.rainfallClasses = parseIntoArray(resources.rainfallClassNames);
      resources.rainfallColors = parseIntoArray(resources.rainfallClassColors);
    }
  }

  //- Remove Layers from resources.layers if configured
  // Object.keys(resources.layers).forEach((language) => {
  //   resources.layers[language] = resources.layers[language].filter((layer) => {
      // switch (layer.id) {
      //   case layerKeys.VIIRS_ACTIVE_FIRES:
      //     return resources.activeFires;
      //   case layerKeys.LAND_COVER:
      //     return resources.landCover;
      //   case layerKeys.AG_BIOMASS:
      //     return resources.aboveGroundBiomass;
      //   case layerKeys.IFL:
      //     return resources.intactForests;
      //   case layerKeys.GLOB_MANGROVE:
      //     return resources.mangroves;
      //   case layerKeys.IMAZON_SAD:
      //     return resources.sadAlerts;
      //   case layerKeys.GLAD_ALERTS:
      //     return resources.gladAlerts;
      //   case layerKeys.TERRA_I_ALERTS:
      //     return resources.terraIAlerts;
      //   default:
      //     return true;
      // }
  //   });
  // });

  //- Remove Layers from resources.layers if configured, while separating remote data layers
  const remoteDataLayers = [];
  Object.keys(resources.layerPanel).forEach(group => {
    if (!remoteDataLayers[group]) { remoteDataLayers[group] = []; }
    const groupSettings = resources.layerPanel[group];
    if (!groupSettings.layers) { return; }
    resources.layerPanel[group].layers = resources.layerPanel[group].layers.filter(layer => {
      if (layer.type === 'remoteDataLayer') {
        remoteDataLayers.push({
          group,
          layer
        });
        return false;
      }
      switch (layer.id) {
        case layerKeys.VIIRS_ACTIVE_FIRES:
          return resources.viirsFires;
        case layerKeys.MODIS_ACTIVE_FIRES:
          return resources.modisFires;
        case layerKeys.LAND_COVER:
          return resources.landCover;
        case layerKeys.AG_BIOMASS:
          return resources.aboveGroundBiomass;
        case layerKeys.IFL:
          return resources.intactForests;
        case layerKeys.PRIMARY_FORESTS:
          return resources.primaryForests;
        case layerKeys.FORMA_ALERTS:
          return resources.forma;
        case layerKeys.GLOB_MANGROVE:
          return resources.mangroves;
        case layerKeys.IMAZON_SAD:
          return resources.sadAlerts;
        case layerKeys.GLAD_ALERTS:
          return resources.gladAlerts;
        case layerKeys.TERRA_I_ALERTS:
          return resources.terraIAlerts;
        case layerKeys.RECENT_IMAGERY:
          return resources.recentImagery;
        default:
          return true;
      }
    });
  });

  const remoteDataLayerRequests = remoteDataLayers
    .map(item => fetch(`${urls.forestWatchLayerApi}/${item.layer.uuid}`)
      .then(response => response.json())
      .then(json => json.data)
      .then(layer => fetch(layer.attributes.layerConfig.metadata)
      .then(response => response.json())
      .then(metadata => {
        const attributes = layer.attributes;
        const itemGroup = item.group;
        item.layer = layer.attributes.layerConfig;
        item.group = itemGroup;
        item.layer.metadata = {
          metadata,
          legendConfig: attributes.legendConfig
        };
        return item;
      })
    )
  );

  //- Update path if it is relative to point to local
  const base = window._app.base ? window._app.base + '/' : '';
  if (resources.logoUrl && resources.logoUrl.indexOf('.') === 0) {
    resources.logoUrl = base + resources.logoUrl;
  }

  // Object.keys(resources.basemaps).forEach((language) => {
    //   Object.keys(resources.basemaps[language]).forEach((bm) => {
      //     const basemap = resources.basemaps[language][bm];
      //     if (basemap.thumbnailUrl && basemap.thumbnailUrl.indexOf('.') === 0) {
        //       basemap.thumbnailUrl = base + basemap.thumbnailUrl;
        //     }
        //   });
        // });

  resources.layerPanel.GROUP_BASEMAP.layers.forEach((basemap) => {
    if (basemap.thumbnailUrl && basemap.thumbnailUrl.indexOf('.') === 0) {
      basemap.thumbnailUrl = base + basemap.thumbnailUrl;
    }
  });

  return Promise.all(remoteDataLayerRequests)
  .then(remoteLayers => {
    remoteLayers.forEach(item => {
      resources.layerPanel[item.group].layers.push(item.layer);
    });
  });

};

export default {

  /**
  * Fetch application information from arcgis online and merge it in to our local resources
  * @param {string=} id - optional app id, if you dont provide it, it will attempt to get it from url
  * @return {promise} promise
  */
  getAppInfo: (id) => {
    const promise = new Deferred();
    const appid = id ? id : getUrlParams(location.href).appid;

    // Set the sharinghost to the correct location so the app can find the webmap content
    if (!resources.sharinghost) { resources.sharinghost = 'https://www.arcgis.com'; }
    arcgisUtils.arcgisUrl = `${resources.sharinghost}/sharing/rest/content/items`;

    if (!appid) {
      //- Format the resources before resolving
      formatResources().then(() => {
        promise.resolve(resources);
      });
      return promise;
    }

    arcgisUtils.getItem(appid).then(res => {
      let agolValues = res.itemData && res.itemData.values;

      //- If we dont have agol settings, save the defaults, else merge them in
      if (!agolValues) {
        //- Format the resources before resolving
        formatResources().then(() => {
          promise.resolve(resources);
        });
        return promise;
      } else {
        //- Prune agolValues by removing null keys
        agolValues = pruneValues(agolValues);

        //- This will merge all the settings in, but some things need a little massaging
        lang.mixin(resources, agolValues);

        //- Put the appid in settings so its easy to get to elsewhere in the app without rereading the url
        resources.appid = appid;

        //- Format the resources before resolving
        formatResources();
        promise.resolve(resources);
      }

    }, err => {
      if (brApp.debug) { console.warn(`template.getAppInfo >> ${err.message}`); }
      formatResources().then(() => {
        promise.resolve(resources);
      });
    });

    return promise;
  }

};
<commit_msg>Fix spacing<commit_after>import Deferred from 'dojo/Deferred';
import lang from 'dojo/_base/lang';

import { urls} from 'js/config';
import resources from 'resources';
import { getUrlParams } from 'utils/params';
import arcgisUtils from 'esri/arcgis/utils';
import layerKeys from 'constants/LayerConstants';

const SEPARATOR = ';';

const pruneValues = (dict) => {
  Object.keys(dict).forEach((key) => {
    if (dict[key] === '' || dict[key] === undefined || dict[key] === null) {
      delete dict[key];
    }
  });
  return dict;
};

/**
* Takes a string and parse it into an array based on separator, e.g hey;you; => ['hey', 'you', '']
* This will remove any blanks from the array as well, e.g. ['hey', 'you', ''] => ['hey', 'you']
*/
const parseIntoArray = (resourceString) => {
  return resourceString.split(SEPARATOR).filter((value) => {
    return value !== undefined && value !== '';
  });
};

/**
* This function formats the resources object by adding information to it
* The default values in the resources match the values from AGOL, to make
* them easier to consume in my components, do all the formatting here
*/
const formatResources = () => {
  const appUrl = location.href.replace(location.search, '');
  //- LANGUAGE SETTINGS START
  resources.labels = {};
  resources.labels[resources.language] = {
    title: resources.title,
    subtitle: resources.subtitle,
    narrative: resources.narrative,
    webmapMenuName: resources.webmapMenuName
  };
  //- parse map themes for default laguage if present
  const names = resources.mapThemes ? parseIntoArray(resources.mapThemes) : [];
  const appids = resources.mapThemeIds ? parseIntoArray(resources.mapThemeIds) : [];

  if (names.length === appids.length && names.length > 0) {
    resources.labels[resources.language].themes = [];
    names.forEach((name, i) => {
      let url = `${appUrl}?appid=${appids[i].trim()}`;
      if (appids[i] === '#') {
        url = appUrl + '?';
      }

      resources.labels[resources.language].themes.push({
        label: name.trim(),
        url: url
      });
    });
  }

  const langUrlParam = getUrlParams(location.href).l;
  if (langUrlParam && langUrlParam === resources.alternativeLanguage) {
    resources.useAlternativeLanguage = true;
  }
  //- Add content for second language if configured
  if (resources.useAlternativeLanguage && resources.alternativeLanguage) {
    resources.labels[resources.alternativeLanguage] = {
      title: resources.alternativeLanguageTitle,
      subtitle: resources.alternativeLanguageSubtitle,
      narrative: resources.alternativeNarrative || resources.narrative,
      webmapMenuName: resources.alternativeWebmapMenuName || resources.webmapMenuName
    };
    //- parse map themes for second laguage if present
    const secondNames = resources.alternativeMapThemes ? parseIntoArray(resources.alternativeMapThemes) : [];
    if (secondNames.length === appids.length && names.length > 0) {
      resources.labels[resources.alternativeLanguage].themes = [];
      secondNames.forEach((name, i) => {
        resources.labels[resources.alternativeLanguage].themes.push({
          label: name.trim(),
          url: `${appUrl}?appid=${appids[i].trim()}`
        });
      });
    }
  }
  //- LANGUAGE SETTINGS END

  //- Restoration Module Configurations
  if (resources.restorationModule) {
    //- Parse the restoration module options if they are in AGOL
    if (resources.restorationOptions) {
      let optionLabels = parseIntoArray(resources.restorationOptions);
      const rasterIds = parseIntoArray(resources.restorationOptionsRasterIds);
      //- Make it in a format easier to consume in our components
      resources.labels[resources.language].restorationOptions = [];
      optionLabels.forEach((label, index) => {
        resources.labels[resources.language].restorationOptions.push({
          id: `$${rasterIds[index]}`,
          label: label
        });
      });

      //- Parse the options, colors, and any other content that will be used in the analysis
      resources.slopeAnalysisPotentialColors = parseIntoArray(resources.slopePotentialColors);
      resources.labels[resources.language].slopeAnalysisPotentialOptions = parseIntoArray(resources.slopePotentialOptions);
      //- Add description text
      resources.labels[resources.language].restorationChartDescription = resources.restorationChartDescription;
      resources.labels[resources.language].restorationTableDescription = resources.restorationTableDescription;
      resources.labels[resources.language].slopeDescription = resources.slopeDescription;
      //- Add the slope options in another language if configured
      if (resources.useAlternativeLanguage) {
        // Slope Options
        resources.labels[resources.alternativeLanguage].slopeAnalysisPotentialOptions = parseIntoArray(resources.alternativeSlopePotentialOptions);
        // Restoration Options
        optionLabels = parseIntoArray(resources.alternativeRestorationOptions);
        resources.labels[resources.alternativeLanguage].restorationOptions = [];
        optionLabels.forEach((label, index) => {
          resources.labels[resources.alternativeLanguage].restorationOptions.push({
            id: `$${rasterIds[index]}`,
            label: label
          });
        });
        //- Add description text
        resources.labels[resources.alternativeLanguage].restorationChartDescription = resources.alternativeRestorationChartDescription;
        resources.labels[resources.alternativeLanguage].restorationTableDescription = resources.alternativeRestorationTableDescription;
        resources.labels[resources.alternativeLanguage].slopeDescription = resources.alternativeSlopeDescription;
      }
    }
    //- Parse slope class names if present
    if (resources.slopeClassNames) {
      resources.slopeClasses = parseIntoArray(resources.slopeClassNames);
      resources.slopeColors = parseIntoArray(resources.slopeClassColors);
    }
    //- Parse tree cover class names if present
    if (resources.treeCoverClassNames) {
      resources.treeCoverClasses = parseIntoArray(resources.treeCoverClassNames);
      resources.treeCoverColors = parseIntoArray(resources.treeCoverClassColors);
    }
    //- Parse land cover class names if present
    if (resources.landCoverClassNames) {
      resources.landCoverClasses = parseIntoArray(resources.landCoverClassNames);
      resources.landCoverColors = parseIntoArray(resources.landCoverClassColors);
    }
    //- Parse population density class names if present
    if (resources.populationClassNames) {
      resources.populationClasses = parseIntoArray(resources.populationClassNames);
      resources.populationColors = parseIntoArray(resources.populationClassColors);
    }

    //- Parse rainfall class names if present
    if (resources.rainfallClassNames) {
      resources.rainfallClasses = parseIntoArray(resources.rainfallClassNames);
      resources.rainfallColors = parseIntoArray(resources.rainfallClassColors);
    }
  }

  //- Remove Layers from resources.layers if configured
  // Object.keys(resources.layers).forEach((language) => {
  //   resources.layers[language] = resources.layers[language].filter((layer) => {
      // switch (layer.id) {
      //   case layerKeys.VIIRS_ACTIVE_FIRES:
      //     return resources.activeFires;
      //   case layerKeys.LAND_COVER:
      //     return resources.landCover;
      //   case layerKeys.AG_BIOMASS:
      //     return resources.aboveGroundBiomass;
      //   case layerKeys.IFL:
      //     return resources.intactForests;
      //   case layerKeys.GLOB_MANGROVE:
      //     return resources.mangroves;
      //   case layerKeys.IMAZON_SAD:
      //     return resources.sadAlerts;
      //   case layerKeys.GLAD_ALERTS:
      //     return resources.gladAlerts;
      //   case layerKeys.TERRA_I_ALERTS:
      //     return resources.terraIAlerts;
      //   default:
      //     return true;
      // }
  //   });
  // });

  //- Remove Layers from resources.layers if configured, while separating remote data layers
  const remoteDataLayers = [];
  Object.keys(resources.layerPanel).forEach(group => {
    if (!remoteDataLayers[group]) { remoteDataLayers[group] = []; }
    const groupSettings = resources.layerPanel[group];
    if (!groupSettings.layers) { return; }
    resources.layerPanel[group].layers = resources.layerPanel[group].layers.filter(layer => {
      if (layer.type === 'remoteDataLayer') {
        remoteDataLayers.push({
          group,
          layer
        });
        return false;
      }
      switch (layer.id) {
        case layerKeys.VIIRS_ACTIVE_FIRES:
          return resources.viirsFires;
        case layerKeys.MODIS_ACTIVE_FIRES:
          return resources.modisFires;
        case layerKeys.LAND_COVER:
          return resources.landCover;
        case layerKeys.AG_BIOMASS:
          return resources.aboveGroundBiomass;
        case layerKeys.IFL:
          return resources.intactForests;
        case layerKeys.PRIMARY_FORESTS:
          return resources.primaryForests;
        case layerKeys.FORMA_ALERTS:
          return resources.forma;
        case layerKeys.GLOB_MANGROVE:
          return resources.mangroves;
        case layerKeys.IMAZON_SAD:
          return resources.sadAlerts;
        case layerKeys.GLAD_ALERTS:
          return resources.gladAlerts;
        case layerKeys.TERRA_I_ALERTS:
          return resources.terraIAlerts;
        case layerKeys.RECENT_IMAGERY:
          return resources.recentImagery;
        default:
          return true;
      }
    });
  });

  const remoteDataLayerRequests = remoteDataLayers
    .map(item => fetch(`${urls.forestWatchLayerApi}/${item.layer.uuid}`)
      .then(response => response.json())
      .then(json => json.data)
      .then(layer => fetch(layer.attributes.layerConfig.metadata)
      .then(response => response.json())
      .then(metadata => {
        const attributes = layer.attributes;
        const itemGroup = item.group;
        item.layer = layer.attributes.layerConfig;
        item.group = itemGroup;
        item.layer.metadata = {
          metadata,
          legendConfig: attributes.legendConfig
        };
        return item;
      })
    )
  );

  //- Update path if it is relative to point to local
  const base = window._app.base ? window._app.base + '/' : '';
  if (resources.logoUrl && resources.logoUrl.indexOf('.') === 0) {
    resources.logoUrl = base + resources.logoUrl;
  }

  // Object.keys(resources.basemaps).forEach((language) => {
  //   Object.keys(resources.basemaps[language]).forEach((bm) => {
  //     const basemap = resources.basemaps[language][bm];
  //     if (basemap.thumbnailUrl && basemap.thumbnailUrl.indexOf('.') === 0) {
  //       basemap.thumbnailUrl = base + basemap.thumbnailUrl;
  //#endregion//     }
  //   });
  // });

  resources.layerPanel.GROUP_BASEMAP.layers.forEach((basemap) => {
    if (basemap.thumbnailUrl && basemap.thumbnailUrl.indexOf('.') === 0) {
      basemap.thumbnailUrl = base + basemap.thumbnailUrl;
    }
  });

  return Promise.all(remoteDataLayerRequests)
  .then(remoteLayers => {
    remoteLayers.forEach(item => {
      resources.layerPanel[item.group].layers.push(item.layer);
    });
  });

};

export default {

  /**
  * Fetch application information from arcgis online and merge it in to our local resources
  * @param {string=} id - optional app id, if you dont provide it, it will attempt to get it from url
  * @return {promise} promise
  */
  getAppInfo: (id) => {
    const promise = new Deferred();
    const appid = id ? id : getUrlParams(location.href).appid;

    // Set the sharinghost to the correct location so the app can find the webmap content
    if (!resources.sharinghost) { resources.sharinghost = 'https://www.arcgis.com'; }
    arcgisUtils.arcgisUrl = `${resources.sharinghost}/sharing/rest/content/items`;

    if (!appid) {
      //- Format the resources before resolving
      formatResources().then(() => {
        promise.resolve(resources);
      });
      return promise;
    }

    arcgisUtils.getItem(appid).then(res => {
      let agolValues = res.itemData && res.itemData.values;

      //- If we dont have agol settings, save the defaults, else merge them in
      if (!agolValues) {
        //- Format the resources before resolving
        formatResources().then(() => {
          promise.resolve(resources);
        });
        return promise;
      } else {
        //- Prune agolValues by removing null keys
        agolValues = pruneValues(agolValues);

        //- This will merge all the settings in, but some things need a little massaging
        lang.mixin(resources, agolValues);

        //- Put the appid in settings so its easy to get to elsewhere in the app without rereading the url
        resources.appid = appid;

        //- Format the resources before resolving
        formatResources();
        promise.resolve(resources);
      }

    }, err => {
      if (brApp.debug) { console.warn(`template.getAppInfo >> ${err.message}`); }
      formatResources().then(() => {
        promise.resolve(resources);
      });
    });

    return promise;
  }

};
<|endoftext|>


<commit_before>let config = require('../config');
module.exports = function(req1, res1, next1) {

	function middleware(req, res, next) {
		let options = { defaultFormat: config.webserver.response.defaultFormat };
		if(req1 !== req) {
			options = Object.assign(options, req1);
		}
		if(req.query.responseFormat) {
			options.defaultFormat = req.query.responseFormat;
		}
		if(!req.user) {
			res.error.add('auth.NOT_LOGGED_IN');
		}

		let fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
		if(!res.error.send('/auth/login?redirect='+encodeURIComponent(fullUrl), options.defaultFormat)) {
			next();
		}
	}

	if(!req1.hasOwnProperty('res')) {
		return middleware;
	}

	middleware(req1, res1, next1);
};<commit_msg>changed "let" to "const" where required<commit_after>const config = require('../config');
module.exports = function(req1, res1, next1) {

	function middleware(req, res, next) {
		let options = { defaultFormat: config.webserver.response.defaultFormat };
		if(req1 !== req) {
			options = Object.assign(options, req1);
		}
		if(req.query.responseFormat) {
			options.defaultFormat = req.query.responseFormat;
		}
		if(!req.user) {
			res.error.add('auth.NOT_LOGGED_IN');
		}

		let fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
		if(!res.error.send('/auth/login?redirect='+encodeURIComponent(fullUrl), options.defaultFormat)) {
			next();
		}
	}

	if(!req1.hasOwnProperty('res')) {
		return middleware;
	}

	middleware(req1, res1, next1);
};<|endoftext|>


<commit_before>// activityobject.js
//
// utility superclass for activity stuff
//
// Copyright 2012, StatusNet Inc.
//
// 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"),
    _ = require("underscore"),
    Step = require("step"),
    NoSuchThingError = databank.NoSuchThingError,
    AlreadyExistsError = databank.AlreadyExistsError,
    DatabankObject = databank.DatabankObject,
    uuid = require("node-uuid"),
    URLMaker = require("../urlmaker").URLMaker,
    IDMaker = require("../idmaker").IDMaker,
    Stamper = require("../stamper").Stamper,
    Stream = require("./stream").Stream;

var UnknownTypeError = function(type) {
    Error.captureStackTrace(this, UnknownTypeError);
    this.name = "UnknownTypeError";
    this.type = type;
    this.message = "Unknown type: " + type;
};

UnknownTypeError.prototype = new Error();
UnknownTypeError.prototype.constructor = UnknownTypeError;

var ActivityObject = function(properties) {
    ActivityObject.init(this, properties);
};

ActivityObject.init = DatabankObject.init;

ActivityObject.prototype = new DatabankObject({});

ActivityObject.beforeCreate = function(props, callback) {

    // XXX: I hope this works

    props.objectType = this.type;

    var now = Stamper.stamp();

    props.published = props.updated = now;

    if (!_(props).has("id")) {
        props.uuid = IDMaker.makeID();
        props.id   = ActivityObject.makeURI(this.type, props.uuid);
    }

    if (_(props).has("likes")) {
        delete props.likes;
    }

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(props, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(props, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

ActivityObject.prototype.afterUpdate =
ActivityObject.prototype.afterSave =
ActivityObject.prototype.afterGet = function(callback) {
    this.expand(callback);
};

ActivityObject.prototype.afterCreate = function(callback) {
    var obj = this;

    Step(
        function() {
            obj.expand(this);
        },
        function(err) {
            if (err) throw err;
            if (!_(obj).has("inReplyTo") || !_(obj.inReplyTo).isObject()) {
                callback(null);
            } else {
                ActivityObject.ensureObject(obj.inReplyTo, this);
            }
        },
        function(err, irt) {
            if (err) throw err;
            irt.getRepliesStream(this);
        },
        function(err, replies) {
            var compressed;
            if (err) throw err;
            compressed = {id: obj.id,
                          objectType: obj.objectType};
            replies.deliverObject(compressed, this);
        },
        callback
    );
};

ActivityObject.prototype.expand = function(callback) {
    ActivityObject.expandProperty(this, "author", callback);
};

ActivityObject.prototype.beforeSave = function(callback) {

    var obj = this,
        now = Stamper.stamp();

    this.updated = now;

    if (_(this).has("likes")) {
        delete this.likes;
    }

    // Save the author by reference; don't save the whole thing

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(obj, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(obj, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, obj);
            }
        }
    );
};

ActivityObject.prototype.beforeUpdate = function(props, callback) {

    var immutable = ["id", "objectType", "uuid", "published"],
        i, prop;

    for (i = 0; i < immutable.length; i++) {
        prop = immutable[i];
        if (props.hasOwnProperty(prop)) {
            delete props[prop];
        }
    }

    if (_(props).has("likes")) {
        delete props.likes;
    }

    var now = Stamper.stamp();

    props.updated = now;

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(props, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(props, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

// For now, we make HTTP URIs. Maybe someday we'll
// do something else. I like HTTP URIs, though.

ActivityObject.makeURI = function(type, uuid) {
    return URLMaker.makeURL("api/" + type + "/" + uuid);
};

ActivityObject.toClass = function(type) {
    var module, className;

    if (!type ||
        ActivityObject.objectTypes.indexOf(type.toLowerCase()) == -1) {
        return null;
    }

    module = require("./" + type);
    className = type.substring(0,1).toUpperCase() + type.substring(1, type.length).toLowerCase();
    return module[className];
};

ActivityObject.toObject = function(props, defaultType) {
    var Cls, type;

    // Try rational fallbacks
    type = props.objectType || defaultType || ActivityObject.NOTE;

    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        return new Cls(props);
    } else {
        // XXX: is this really the best we can do?
        // XXX: extension mechanism
        // XXX: "Other" object type
        return props;
    }
};

ActivityObject.getObject = function(type, id, callback) {
    var Cls;
    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        Cls.get(id, callback);
    } else {
        callback(new UnknownTypeError(type), null);
    }
};

ActivityObject.createObject = function(obj, callback) {
    var Cls, type = obj.objectType;
    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        Cls.create(obj, callback);
    } else {
        callback(new UnknownTypeError(type), null);
    }
};

ActivityObject.ensureObject = function(obj, callback) {
    var Cls, type = obj.objectType, id = obj.id;
    if (ActivityObject.objectTypes.indexOf(type) === -1) {
        callback(new UnknownTypeError(type), null);
    } else {
        Cls = ActivityObject.toClass(type);
        Cls.create(obj, function(err, result) {
            if (err) {
                if (err instanceof AlreadyExistsError) {
                    Cls.get(id, callback);
                } else {
                    callback(err, null);
                }
            } else {
                callback(null, result);
            }
        });
    }
};

ActivityObject.compressProperty = function(obj, name, callback) {
    // Easy enough!
    if (!_(obj).has(name)) {
        callback(null);
        return;
    }
    Step(
        function() {
            ActivityObject.ensureObject(obj[name], this);
        },
        function(err, sub) {
            var Cls;
            if (err) {
                callback(err);
            } else {
                Cls = ActivityObject.toClass(sub.objectType);
                if (!Cls) {
                    callback(new UnknownTypeError(sub.objectType));
                } else {
                    obj[name] = new Cls({id: sub.id,
                                         objectType: sub.objectType});
                    callback(null);
                }
            }
        }
    );
};

ActivityObject.expandProperty = function(obj, name, callback) {
    // Easy enough!
    if (!_(obj).has(name)) {
        callback(null);
        return;
    }
    Step(
        function() {
            ActivityObject.ensureObject(obj[name], this);
        },
        function(err, sub) {
            if (err) {
                callback(err);
            } else {
                obj[name] = sub;
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.favoritedBy = function(id, callback) {
    var obj = this,
        name = "favoriters:"+obj.id;
    Step(
        function() {
            Stream.get(name, this);
        },
        function(err, stream) {
            if (err && err instanceof NoSuchThingError) {
                Stream.create({name: name}, this);
            } else if (err) {
                throw err;
            } else {
                this(null, stream);
            }
        },
        function(err, stream) {
            if (err) throw err;
            stream.deliver(id, this);
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.unfavoritedBy = function(id, callback) {
    var obj = this,
        name = "favoriters:"+obj.id;
    Step(
        function() {
            Stream.get(name, this);
        },
        function(err, stream) {
            if (err && err instanceof NoSuchThingError) {
                Stream.create({name: name}, this);
            } else if (err) {
                throw err;
            } else {
                this(null, stream);
            }
        },
        function(err, stream) {
            if (err) throw err;
            stream.remove(id, this);
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

ActivityObject.getObjectStream = function(className, streamName, start, end, callback) {

    var ids,
        Cls = ActivityObject.toClass(className);

    Step(
        function() {
            Stream.get(streamName, this);
        },
        function(err, stream) {
            if (err) throw err;
            stream.getIDs(start, end, this);
        },
        function(err, results) {
            if (err) throw err;
            ids = results;
            if (ids.length === 0) {
                callback(null, []);
            } else {
                Cls.readAll(ids, this);
            }
        },
        function(err, map) {
            var i, objects = [];
            if (err) {
                if (err instanceof NoSuchThingError) {
                    callback(null, []);
                } else {
                    callback(err, null);
                }
            } else {
                objects = new Array(ids.length);
                // Try to get it in the right order
                for (i = 0; i < ids.length; i++) {
                    objects[i] = map[ids[i]];
                }
                callback(null, objects);
            }
        }
    );
};

ActivityObject.prototype.getFavoriters = function(start, end, callback) {
    ActivityObject.getObjectStream("person", "favoriters:"+this.id, start, end, callback);
};

ActivityObject.prototype.favoritersCount = function(callback) {
    Stream.count("favoriters:"+this.id, function(err, count) {
        if (err && err instanceof NoSuchThingError) {
            callback(null, 0);
        } else if (err) {
            callback(err, null);
        } else {
            callback(null, count);
        }
    });
};

ActivityObject.prototype.expandFeeds = function(callback) {
    var obj = this;
    Step(
        function() {
            obj.favoritersCount(this);
        },
        function(err, count) {
            if (err) {
                callback(err);
            } else {
                obj.likes = {
                    totalItems: count,
                    url: URLMaker.makeURL("api/"+obj.objectType+"/"+obj.uuid+"/likes")
                };
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.getRepliesStream = function(callback) {
    var obj = this,
        name = "activityobject:replies:"+obj.id;

    Step(
        function() {
            Stream.get(name, this);
        },
        function(err, stream) {
            if (err && err instanceof NoSuchThingError) {
                Stream.create({name: name}, this);
            } else if (err) {
                callback(err, null);
            } else {
                callback(null, stream);
            }
        },
        function(err, stream) {
            if (err && err instanceof AlreadyExistsError) { // race condition; just get it
                Stream.get(name, callback);
            } else if (err) {
                callback(err, null);
            } else {
                callback(null, stream);
            }
        }
    );
};

ActivityObject.prototype.getReplies = function(start, end, callback) {

    var obj = this;

    Step(
        function() {
            obj.getRepliesStream(this);
        },
        function(err, stream) {
            if (err) throw err;
            stream.getObjects(start, end, this);
        },
        function(err, compressed) {
            var i, group = this.group();
            if (err) throw err;
            for (i = 0; i < compressed.length; i++) {
                ActivityObject.ensureObject(compressed[i], group());
            }
        },
        callback
    );
};

ActivityObject.objectTypes = ["article",
                              "audio",
                              "badge",
                              "bookmark",
                              "collection",
                              "comment",
                              "event",
                              "file",
                              "group",
                              "image",
                              "note",
                              "person",
                              "place",
                              "product",
                              "question",
                              "review",
                              "service",
                              "video"];

var objectType, i;

// Constants-like members for activity object types

for (i = 0; i < ActivityObject.objectTypes.length; i++) {
    objectType = ActivityObject.objectTypes[i];
    ActivityObject[objectType.toUpperCase().replace("-", "_")] = objectType;
}

exports.ActivityObject = ActivityObject;
exports.UnknownTypeError = UnknownTypeError;
<commit_msg>Add a replies feed when expanding feeds<commit_after>// activityobject.js
//
// utility superclass for activity stuff
//
// Copyright 2012, StatusNet Inc.
//
// 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"),
    _ = require("underscore"),
    Step = require("step"),
    NoSuchThingError = databank.NoSuchThingError,
    AlreadyExistsError = databank.AlreadyExistsError,
    DatabankObject = databank.DatabankObject,
    uuid = require("node-uuid"),
    URLMaker = require("../urlmaker").URLMaker,
    IDMaker = require("../idmaker").IDMaker,
    Stamper = require("../stamper").Stamper,
    Stream = require("./stream").Stream;

var UnknownTypeError = function(type) {
    Error.captureStackTrace(this, UnknownTypeError);
    this.name = "UnknownTypeError";
    this.type = type;
    this.message = "Unknown type: " + type;
};

UnknownTypeError.prototype = new Error();
UnknownTypeError.prototype.constructor = UnknownTypeError;

var ActivityObject = function(properties) {
    ActivityObject.init(this, properties);
};

ActivityObject.init = DatabankObject.init;

ActivityObject.prototype = new DatabankObject({});

ActivityObject.beforeCreate = function(props, callback) {

    // XXX: I hope this works

    props.objectType = this.type;

    var now = Stamper.stamp();

    props.published = props.updated = now;

    if (!_(props).has("id")) {
        props.uuid = IDMaker.makeID();
        props.id   = ActivityObject.makeURI(this.type, props.uuid);
    }

    if (_(props).has("likes")) {
        delete props.likes;
    }

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(props, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(props, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

ActivityObject.prototype.afterUpdate =
ActivityObject.prototype.afterSave =
ActivityObject.prototype.afterGet = function(callback) {
    this.expand(callback);
};

ActivityObject.prototype.afterCreate = function(callback) {
    var obj = this;

    Step(
        function() {
            obj.expand(this);
        },
        function(err) {
            if (err) throw err;
            if (!_(obj).has("inReplyTo") || !_(obj.inReplyTo).isObject()) {
                callback(null);
            } else {
                ActivityObject.ensureObject(obj.inReplyTo, this);
            }
        },
        function(err, irt) {
            if (err) throw err;
            irt.getRepliesStream(this);
        },
        function(err, replies) {
            var compressed;
            if (err) throw err;
            compressed = {id: obj.id,
                          objectType: obj.objectType};
            replies.deliverObject(compressed, this);
        },
        callback
    );
};

ActivityObject.prototype.expand = function(callback) {
    ActivityObject.expandProperty(this, "author", callback);
};

ActivityObject.prototype.beforeSave = function(callback) {

    var obj = this,
        now = Stamper.stamp();

    this.updated = now;

    if (_(this).has("likes")) {
        delete this.likes;
    }

    // Save the author by reference; don't save the whole thing

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(obj, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(obj, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, obj);
            }
        }
    );
};

ActivityObject.prototype.beforeUpdate = function(props, callback) {

    var immutable = ["id", "objectType", "uuid", "published"],
        i, prop;

    for (i = 0; i < immutable.length; i++) {
        prop = immutable[i];
        if (props.hasOwnProperty(prop)) {
            delete props[prop];
        }
    }

    if (_(props).has("likes")) {
        delete props.likes;
    }

    var now = Stamper.stamp();

    props.updated = now;

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(props, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(props, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

// For now, we make HTTP URIs. Maybe someday we'll
// do something else. I like HTTP URIs, though.

ActivityObject.makeURI = function(type, uuid) {
    return URLMaker.makeURL("api/" + type + "/" + uuid);
};

ActivityObject.toClass = function(type) {
    var module, className;

    if (!type ||
        ActivityObject.objectTypes.indexOf(type.toLowerCase()) == -1) {
        return null;
    }

    module = require("./" + type);
    className = type.substring(0,1).toUpperCase() + type.substring(1, type.length).toLowerCase();
    return module[className];
};

ActivityObject.toObject = function(props, defaultType) {
    var Cls, type;

    // Try rational fallbacks
    type = props.objectType || defaultType || ActivityObject.NOTE;

    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        return new Cls(props);
    } else {
        // XXX: is this really the best we can do?
        // XXX: extension mechanism
        // XXX: "Other" object type
        return props;
    }
};

ActivityObject.getObject = function(type, id, callback) {
    var Cls;
    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        Cls.get(id, callback);
    } else {
        callback(new UnknownTypeError(type), null);
    }
};

ActivityObject.createObject = function(obj, callback) {
    var Cls, type = obj.objectType;
    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        Cls.create(obj, callback);
    } else {
        callback(new UnknownTypeError(type), null);
    }
};

ActivityObject.ensureObject = function(obj, callback) {
    var Cls, type = obj.objectType, id = obj.id;
    if (ActivityObject.objectTypes.indexOf(type) === -1) {
        callback(new UnknownTypeError(type), null);
    } else {
        Cls = ActivityObject.toClass(type);
        Cls.create(obj, function(err, result) {
            if (err) {
                if (err instanceof AlreadyExistsError) {
                    Cls.get(id, callback);
                } else {
                    callback(err, null);
                }
            } else {
                callback(null, result);
            }
        });
    }
};

ActivityObject.compressProperty = function(obj, name, callback) {
    // Easy enough!
    if (!_(obj).has(name)) {
        callback(null);
        return;
    }
    Step(
        function() {
            ActivityObject.ensureObject(obj[name], this);
        },
        function(err, sub) {
            var Cls;
            if (err) {
                callback(err);
            } else {
                Cls = ActivityObject.toClass(sub.objectType);
                if (!Cls) {
                    callback(new UnknownTypeError(sub.objectType));
                } else {
                    obj[name] = new Cls({id: sub.id,
                                         objectType: sub.objectType});
                    callback(null);
                }
            }
        }
    );
};

ActivityObject.expandProperty = function(obj, name, callback) {
    // Easy enough!
    if (!_(obj).has(name)) {
        callback(null);
        return;
    }
    Step(
        function() {
            ActivityObject.ensureObject(obj[name], this);
        },
        function(err, sub) {
            if (err) {
                callback(err);
            } else {
                obj[name] = sub;
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.favoritedBy = function(id, callback) {
    var obj = this,
        name = "favoriters:"+obj.id;
    Step(
        function() {
            Stream.get(name, this);
        },
        function(err, stream) {
            if (err && err instanceof NoSuchThingError) {
                Stream.create({name: name}, this);
            } else if (err) {
                throw err;
            } else {
                this(null, stream);
            }
        },
        function(err, stream) {
            if (err) throw err;
            stream.deliver(id, this);
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.unfavoritedBy = function(id, callback) {
    var obj = this,
        name = "favoriters:"+obj.id;
    Step(
        function() {
            Stream.get(name, this);
        },
        function(err, stream) {
            if (err && err instanceof NoSuchThingError) {
                Stream.create({name: name}, this);
            } else if (err) {
                throw err;
            } else {
                this(null, stream);
            }
        },
        function(err, stream) {
            if (err) throw err;
            stream.remove(id, this);
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

ActivityObject.getObjectStream = function(className, streamName, start, end, callback) {

    var ids,
        Cls = ActivityObject.toClass(className);

    Step(
        function() {
            Stream.get(streamName, this);
        },
        function(err, stream) {
            if (err) throw err;
            stream.getIDs(start, end, this);
        },
        function(err, results) {
            if (err) throw err;
            ids = results;
            if (ids.length === 0) {
                callback(null, []);
            } else {
                Cls.readAll(ids, this);
            }
        },
        function(err, map) {
            var i, objects = [];
            if (err) {
                if (err instanceof NoSuchThingError) {
                    callback(null, []);
                } else {
                    callback(err, null);
                }
            } else {
                objects = new Array(ids.length);
                // Try to get it in the right order
                for (i = 0; i < ids.length; i++) {
                    objects[i] = map[ids[i]];
                }
                callback(null, objects);
            }
        }
    );
};

ActivityObject.prototype.getFavoriters = function(start, end, callback) {
    ActivityObject.getObjectStream("person", "favoriters:"+this.id, start, end, callback);
};

ActivityObject.prototype.favoritersCount = function(callback) {
    Stream.count("favoriters:"+this.id, function(err, count) {
        if (err && err instanceof NoSuchThingError) {
            callback(null, 0);
        } else if (err) {
            callback(err, null);
        } else {
            callback(null, count);
        }
    });
};

ActivityObject.prototype.expandFeeds = function(callback) {
    var obj = this;
    Step(
        function() {
            obj.repliesCount(this.parallel());
            obj.favoritersCount(this.parallel());
        },
        function(err, repliesCount, favoritersCount) {
            if (err) {
                callback(err);
            } else {
                obj.likes = {
                    totalItems: favoritersCount,
                    url: URLMaker.makeURL("api/"+obj.objectType+"/"+obj.uuid+"/likes")
                };
                obj.replies = {
                    totalItems: repliesCount,
                    url: URLMaker.makeURL("api/"+obj.objectType+"/"+obj.uuid+"/replies")
                };
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.getRepliesStream = function(callback) {
    var obj = this,
        name = "activityobject:replies:"+obj.id;

    Step(
        function() {
            Stream.get(name, this);
        },
        function(err, stream) {
            if (err && err instanceof NoSuchThingError) {
                Stream.create({name: name}, this);
            } else if (err) {
                callback(err, null);
            } else {
                callback(null, stream);
            }
        },
        function(err, stream) {
            if (err && err instanceof AlreadyExistsError) { // race condition; just get it
                Stream.get(name, callback);
            } else if (err) {
                callback(err, null);
            } else {
                callback(null, stream);
            }
        }
    );
};

ActivityObject.prototype.getReplies = function(start, end, callback) {

    var obj = this;

    Step(
        function() {
            obj.getRepliesStream(this);
        },
        function(err, stream) {
            if (err) throw err;
            stream.getObjects(start, end, this);
        },
        function(err, compressed) {
            var i, group = this.group();
            if (err) throw err;
            for (i = 0; i < compressed.length; i++) {
                ActivityObject.ensureObject(compressed[i], group());
            }
        },
        callback
    );
};

ActivityObject.objectTypes = ["article",
                              "audio",
                              "badge",
                              "bookmark",
                              "collection",
                              "comment",
                              "event",
                              "file",
                              "group",
                              "image",
                              "note",
                              "person",
                              "place",
                              "product",
                              "question",
                              "review",
                              "service",
                              "video"];

var objectType, i;

// Constants-like members for activity object types

for (i = 0; i < ActivityObject.objectTypes.length; i++) {
    objectType = ActivityObject.objectTypes[i];
    ActivityObject[objectType.toUpperCase().replace("-", "_")] = objectType;
}

exports.ActivityObject = ActivityObject;
exports.UnknownTypeError = UnknownTypeError;
<|endoftext|>


<commit_before>// activityobject.js
//
// utility superclass for activity stuff
//
// Copyright 2012, StatusNet Inc.
//
// 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"),
    _ = require("underscore"),
    Step = require("step"),
    NoSuchThingError = databank.NoSuchThingError,
    AlreadyExistsError = databank.AlreadyExistsError,
    DatabankObject = databank.DatabankObject,
    uuid = require("node-uuid"),
    URLMaker = require("../urlmaker").URLMaker,
    IDMaker = require("../idmaker").IDMaker,
    Stamper = require("../stamper").Stamper,
    Stream = require("./stream").Stream;

var UnknownTypeError = function(type) {
    Error.captureStackTrace(this, UnknownTypeError);
    this.name = "UnknownTypeError";
    this.type = type;
    this.message = "Unknown type: " + type;
};

UnknownTypeError.prototype = new Error();
UnknownTypeError.prototype.constructor = UnknownTypeError;

var ActivityObject = function(properties) {
    ActivityObject.init(this, properties);
};

ActivityObject.init = DatabankObject.init;

ActivityObject.prototype = new DatabankObject({});

ActivityObject.beforeCreate = function(props, callback) {

    // XXX: I hope this works

    props.objectType = this.type;

    var now = Stamper.stamp();

    props.published = props.updated = now;

    if (!_(props).has("id")) {
        props.uuid = IDMaker.makeID();
        props.id   = ActivityObject.makeURI(this.type, props.uuid);
    }

    if (_(props).has("likes")) {
        delete props.likes;
    }

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(props, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(props, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

ActivityObject.prototype.afterUpdate =
ActivityObject.prototype.afterSave =
ActivityObject.prototype.afterGet = function(callback) {
    this.expand(callback);
};

ActivityObject.prototype.afterCreate = function(callback) {
    var obj = this;

    Step(
        function() {
            var name = "activityobject:replies:"+obj.id;
            Stream.create({name: name}, this);
        },
        function(err, stream) {
            obj.expand(this);
        },
        function(err) {
            if (err) throw err;
            if (!_(obj).has("inReplyTo") || !_(obj.inReplyTo).isObject()) {
                callback(null);
            } else {
                ActivityObject.ensureObject(obj.inReplyTo, this);
            }
        },
        function(err, irt) {
            if (err) throw err;
            irt.getRepliesStream(this);
        },
        function(err, replies) {
            var compressed;
            if (err) throw err;
            compressed = {id: obj.id,
                          objectType: obj.objectType};
            replies.deliverObject(compressed, this);
        },
        callback
    );
};

ActivityObject.prototype.afterDel =
ActivityObject.prototype.afterEfface = function(callback) {
    var obj = this;

    Step(
        function() {
            if (!_(obj).has("inReplyTo") || !_(obj.inReplyTo).isObject()) {
                callback(null);
            } else {
                ActivityObject.ensureObject(obj.inReplyTo, this);
            }
        },
        function(err, irt) {
            if (err) throw err;
            irt.getRepliesStream(this);
        },
        function(err, replies) {
            var compressed;
            if (err) throw err;
            compressed = {id: obj.id,
                          objectType: obj.objectType};
            replies.removeObject(compressed, this);
        },
        callback
    );
};

ActivityObject.prototype.expand = function(callback) {
    ActivityObject.expandProperty(this, "author", callback);
};

ActivityObject.prototype.beforeSave = function(callback) {

    var obj = this,
        now = Stamper.stamp();

    this.updated = now;

    if (_(this).has("likes")) {
        delete this.likes;
    }

    // Save the author by reference; don't save the whole thing

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(obj, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(obj, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, obj);
            }
        }
    );
};

ActivityObject.prototype.beforeUpdate = function(props, callback) {

    var immutable = ["id", "objectType", "uuid", "published"],
        i, prop;

    for (i = 0; i < immutable.length; i++) {
        prop = immutable[i];
        if (props.hasOwnProperty(prop)) {
            delete props[prop];
        }
    }

    if (_(props).has("likes")) {
        delete props.likes;
    }

    var now = Stamper.stamp();

    props.updated = now;

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(props, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(props, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

// For now, we make HTTP URIs. Maybe someday we'll
// do something else. I like HTTP URIs, though.

ActivityObject.makeURI = function(type, uuid) {
    return URLMaker.makeURL("api/" + type + "/" + uuid);
};

ActivityObject.toClass = function(type) {
    var module, className;

    if (!type ||
        ActivityObject.objectTypes.indexOf(type.toLowerCase()) == -1) {
        return null;
    }

    module = require("./" + type);
    className = type.substring(0,1).toUpperCase() + type.substring(1, type.length).toLowerCase();
    return module[className];
};

ActivityObject.toObject = function(props, defaultType) {
    var Cls, type;

    // Try rational fallbacks
    type = props.objectType || defaultType || ActivityObject.NOTE;

    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        return new Cls(props);
    } else {
        // XXX: is this really the best we can do?
        // XXX: extension mechanism
        // XXX: "Other" object type
        return props;
    }
};

ActivityObject.getObject = function(type, id, callback) {
    var Cls;
    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        Cls.get(id, callback);
    } else {
        callback(new UnknownTypeError(type), null);
    }
};

ActivityObject.createObject = function(obj, callback) {
    var Cls, type = obj.objectType;
    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        Cls.create(obj, callback);
    } else {
        callback(new UnknownTypeError(type), null);
    }
};

ActivityObject.ensureObject = function(obj, callback) {
    var Cls, type = obj.objectType, id = obj.id;
    if (ActivityObject.objectTypes.indexOf(type) === -1) {
        callback(new UnknownTypeError(type), null);
    } else {
        Cls = ActivityObject.toClass(type);
        Cls.create(obj, function(err, result) {
            if (err) {
                if (err instanceof AlreadyExistsError) {
                    Cls.get(id, callback);
                } else {
                    callback(err, null);
                }
            } else {
                callback(null, result);
            }
        });
    }
};

ActivityObject.compressProperty = function(obj, name, callback) {
    // Easy enough!
    if (!_(obj).has(name)) {
        callback(null);
        return;
    }
    Step(
        function() {
            ActivityObject.ensureObject(obj[name], this);
        },
        function(err, sub) {
            var Cls;
            if (err) {
                callback(err);
            } else {
                Cls = ActivityObject.toClass(sub.objectType);
                if (!Cls) {
                    callback(new UnknownTypeError(sub.objectType));
                } else {
                    obj[name] = new Cls({id: sub.id,
                                         objectType: sub.objectType});
                    callback(null);
                }
            }
        }
    );
};

ActivityObject.expandProperty = function(obj, name, callback) {
    // Easy enough!
    if (!_(obj).has(name)) {
        callback(null);
        return;
    }
    Step(
        function() {
            ActivityObject.ensureObject(obj[name], this);
        },
        function(err, sub) {
            if (err) {
                callback(err);
            } else {
                obj[name] = sub;
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.favoritedBy = function(id, callback) {
    var obj = this,
        name = "favoriters:"+obj.id;
    Step(
        function() {
            Stream.get(name, this);
        },
        function(err, stream) {
            if (err && err instanceof NoSuchThingError) {
                Stream.create({name: name}, this);
            } else if (err) {
                throw err;
            } else {
                this(null, stream);
            }
        },
        function(err, stream) {
            if (err) throw err;
            stream.deliver(id, this);
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.unfavoritedBy = function(id, callback) {
    var obj = this,
        name = "favoriters:"+obj.id;
    Step(
        function() {
            Stream.get(name, this);
        },
        function(err, stream) {
            if (err && err instanceof NoSuchThingError) {
                Stream.create({name: name}, this);
            } else if (err) {
                throw err;
            } else {
                this(null, stream);
            }
        },
        function(err, stream) {
            if (err) throw err;
            stream.remove(id, this);
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

ActivityObject.getObjectStream = function(className, streamName, start, end, callback) {

    var ids,
        Cls = ActivityObject.toClass(className);

    Step(
        function() {
            Stream.get(streamName, this);
        },
        function(err, stream) {
            if (err) throw err;
            stream.getIDs(start, end, this);
        },
        function(err, results) {
            if (err) throw err;
            ids = results;
            if (ids.length === 0) {
                callback(null, []);
            } else {
                Cls.readAll(ids, this);
            }
        },
        function(err, map) {
            var i, objects = [];
            if (err) {
                if (err instanceof NoSuchThingError) {
                    callback(null, []);
                } else {
                    callback(err, null);
                }
            } else {
                objects = new Array(ids.length);
                // Try to get it in the right order
                for (i = 0; i < ids.length; i++) {
                    objects[i] = map[ids[i]];
                }
                callback(null, objects);
            }
        }
    );
};

ActivityObject.prototype.getFavoriters = function(start, end, callback) {
    ActivityObject.getObjectStream("person", "favoriters:"+this.id, start, end, callback);
};

ActivityObject.prototype.favoritersCount = function(callback) {
    Stream.count("favoriters:"+this.id, function(err, count) {
        if (err && err instanceof NoSuchThingError) {
            callback(null, 0);
        } else if (err) {
            callback(err, null);
        } else {
            callback(null, count);
        }
    });
};

ActivityObject.prototype.expandFeeds = function(callback) {
    var obj = this;
    Step(
        function() {
            obj.repliesCount(this.parallel());
            obj.favoritersCount(this.parallel());
            obj.getReplies(0, 4, this.parallel());
        },
        function(err, repliesCount, favoritersCount, replies) {
            if (err) {
                callback(err);
            } else {
                obj.likes = {
                    totalItems: favoritersCount,
                    url: URLMaker.makeURL("api/"+obj.objectType+"/"+obj.uuid+"/likes")
                };
                obj.replies = {
                    totalItems: repliesCount,
                    url: URLMaker.makeURL("api/"+obj.objectType+"/"+obj.uuid+"/replies")
                };
                if (repliesCount > 0) {
                    obj.replies.items = replies;
                }
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.getRepliesStream = function(callback) {

    var obj = this,
        name = "activityobject:replies:"+obj.id;

    Stream.get(name, callback);
};

ActivityObject.prototype.getReplies = function(start, end, callback) {

    var obj = this,
        full = [];

    Step(
        function() {
            obj.getRepliesStream(this);
        },
        function(err, stream) {
            if (err) throw err;
            stream.getObjects(start, end, this);
        },
        function(err, compressed) {
            var i, group = this.group();
            if (err) throw err;
            for (i = 0; i < compressed.length; i++) {
                ActivityObject.ensureObject(compressed[i], group());
            }
        },
        function(err, results) {
            var i, group = this.group();
            if (err) throw err;
            full = results;
            for (i = 0; i < full.length; i++) {
                full[i].expandFeeds(group());
            }
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, full);
            }
        }
    );
};

ActivityObject.prototype.repliesCount = function(callback) {
    var name = "activityobject:replies:"+this.id;
    Stream.count(name, function(err, count) {
        if (err && err instanceof NoSuchThingError) {
            callback(null, 0);
        } else if (err) {
            callback(err, null);
        } else {
            callback(null, count);
        }
    });
};

ActivityObject.prototype.keepers = function() {
    return ["id", "objectType", "author", "published", "updated", "uuid", "inReplyTo"];
};

// Default hooks for efface()

ActivityObject.prototype.beforeEfface = function(callback) {
    callback(null);
};

ActivityObject.prototype.efface = function(callback) {

    var keepers = this.keepers(),
        prop,
        obj = this;

    Step(
        function() {
            obj.beforeEfface(this);
        },
        function(err) {
            if (err) throw err;

            for (prop in obj) {
                if (obj.hasOwnProperty(prop) && keepers.indexOf(prop) === -1) {
                    delete obj[prop];
                }
            }

            var now = Stamper.stamp();

            obj.deleted = obj.updated = now;

            obj.save(this);
        },
        function(err) {
            obj.afterEfface(this);
        },
        callback
    );
};

ActivityObject.objectTypes = ["article",
                              "audio",
                              "badge",
                              "bookmark",
                              "collection",
                              "comment",
                              "event",
                              "file",
                              "group",
                              "image",
                              "note",
                              "person",
                              "place",
                              "product",
                              "question",
                              "review",
                              "service",
                              "video"];

var objectType, i;

// Constants-like members for activity object types

for (i = 0; i < ActivityObject.objectTypes.length; i++) {
    objectType = ActivityObject.objectTypes[i];
    ActivityObject[objectType.toUpperCase().replace("-", "_")] = objectType;
}

exports.ActivityObject = ActivityObject;
exports.UnknownTypeError = UnknownTypeError;
<commit_msg>Three changes to activityobject beforeCreate()<commit_after>// activityobject.js
//
// utility superclass for activity stuff
//
// Copyright 2012, StatusNet Inc.
//
// 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"),
    _ = require("underscore"),
    Step = require("step"),
    NoSuchThingError = databank.NoSuchThingError,
    AlreadyExistsError = databank.AlreadyExistsError,
    DatabankObject = databank.DatabankObject,
    uuid = require("node-uuid"),
    URLMaker = require("../urlmaker").URLMaker,
    IDMaker = require("../idmaker").IDMaker,
    Stamper = require("../stamper").Stamper,
    Stream = require("./stream").Stream;

var UnknownTypeError = function(type) {
    Error.captureStackTrace(this, UnknownTypeError);
    this.name = "UnknownTypeError";
    this.type = type;
    this.message = "Unknown type: " + type;
};

UnknownTypeError.prototype = new Error();
UnknownTypeError.prototype.constructor = UnknownTypeError;

var ActivityObject = function(properties) {
    ActivityObject.init(this, properties);
};

ActivityObject.init = DatabankObject.init;

ActivityObject.prototype = new DatabankObject({});

ActivityObject.beforeCreate = function(props, callback) {

    props.objectType = this.type;

    var now = Stamper.stamp();

    props.published = props.updated = now;

    if (!_(props).has("id")) {
        props.uuid = IDMaker.makeID();
        props.id   = ActivityObject.makeURI(this.type, props.uuid);

        props.links = {
            self: {
                href: URLMaker.makeURL("api/" + this.type + "/" + props.uuid)
            }
        };
    }

    if (_(props).has("likes")) {
        delete props.likes;
    }

    if (_(props).has("replies")) {
        delete props.replies;
    }

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(props, "author", this.parallel());
            ActivityObject.compressProperty(props, "inReplyTo", this.parallel());
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

ActivityObject.prototype.afterUpdate =
ActivityObject.prototype.afterSave =
ActivityObject.prototype.afterGet = function(callback) {
    this.expand(callback);
};

ActivityObject.prototype.afterCreate = function(callback) {
    var obj = this;

    Step(
        function() {
            var name = "activityobject:replies:"+obj.id;
            Stream.create({name: name}, this);
        },
        function(err, stream) {
            obj.expand(this);
        },
        function(err) {
            if (err) throw err;
            if (!_(obj).has("inReplyTo") || !_(obj.inReplyTo).isObject()) {
                callback(null);
            } else {
                ActivityObject.ensureObject(obj.inReplyTo, this);
            }
        },
        function(err, irt) {
            if (err) throw err;
            irt.getRepliesStream(this);
        },
        function(err, replies) {
            var compressed;
            if (err) throw err;
            compressed = {id: obj.id,
                          objectType: obj.objectType};
            replies.deliverObject(compressed, this);
        },
        callback
    );
};

ActivityObject.prototype.afterDel =
ActivityObject.prototype.afterEfface = function(callback) {
    var obj = this;

    Step(
        function() {
            if (!_(obj).has("inReplyTo") || !_(obj.inReplyTo).isObject()) {
                callback(null);
            } else {
                ActivityObject.ensureObject(obj.inReplyTo, this);
            }
        },
        function(err, irt) {
            if (err) throw err;
            irt.getRepliesStream(this);
        },
        function(err, replies) {
            var compressed;
            if (err) throw err;
            compressed = {id: obj.id,
                          objectType: obj.objectType};
            replies.removeObject(compressed, this);
        },
        callback
    );
};

ActivityObject.prototype.expand = function(callback) {
    ActivityObject.expandProperty(this, "author", callback);
};

ActivityObject.prototype.beforeSave = function(callback) {

    var obj = this,
        now = Stamper.stamp();

    this.updated = now;

    if (_(this).has("likes")) {
        delete this.likes;
    }

    // Save the author by reference; don't save the whole thing

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(obj, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(obj, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, obj);
            }
        }
    );
};

ActivityObject.prototype.beforeUpdate = function(props, callback) {

    var immutable = ["id", "objectType", "uuid", "published"],
        i, prop;

    for (i = 0; i < immutable.length; i++) {
        prop = immutable[i];
        if (props.hasOwnProperty(prop)) {
            delete props[prop];
        }
    }

    if (_(props).has("likes")) {
        delete props.likes;
    }

    var now = Stamper.stamp();

    props.updated = now;

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(props, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(props, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

// For now, we make HTTP URIs. Maybe someday we'll
// do something else. I like HTTP URIs, though.

ActivityObject.makeURI = function(type, uuid) {
    return URLMaker.makeURL("api/" + type + "/" + uuid);
};

ActivityObject.toClass = function(type) {
    var module, className;

    if (!type ||
        ActivityObject.objectTypes.indexOf(type.toLowerCase()) == -1) {
        return null;
    }

    module = require("./" + type);
    className = type.substring(0,1).toUpperCase() + type.substring(1, type.length).toLowerCase();
    return module[className];
};

ActivityObject.toObject = function(props, defaultType) {
    var Cls, type;

    // Try rational fallbacks
    type = props.objectType || defaultType || ActivityObject.NOTE;

    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        return new Cls(props);
    } else {
        // XXX: is this really the best we can do?
        // XXX: extension mechanism
        // XXX: "Other" object type
        return props;
    }
};

ActivityObject.getObject = function(type, id, callback) {
    var Cls;
    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        Cls.get(id, callback);
    } else {
        callback(new UnknownTypeError(type), null);
    }
};

ActivityObject.createObject = function(obj, callback) {
    var Cls, type = obj.objectType;
    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        Cls.create(obj, callback);
    } else {
        callback(new UnknownTypeError(type), null);
    }
};

ActivityObject.ensureObject = function(obj, callback) {
    var Cls, type = obj.objectType, id = obj.id;
    if (ActivityObject.objectTypes.indexOf(type) === -1) {
        callback(new UnknownTypeError(type), null);
    } else {
        Cls = ActivityObject.toClass(type);
        Cls.create(obj, function(err, result) {
            if (err) {
                if (err instanceof AlreadyExistsError) {
                    Cls.get(id, callback);
                } else {
                    callback(err, null);
                }
            } else {
                callback(null, result);
            }
        });
    }
};

ActivityObject.compressProperty = function(obj, name, callback) {
    // Easy enough!
    if (!_(obj).has(name)) {
        callback(null);
        return;
    }
    Step(
        function() {
            ActivityObject.ensureObject(obj[name], this);
        },
        function(err, sub) {
            var Cls;
            if (err) {
                callback(err);
            } else {
                Cls = ActivityObject.toClass(sub.objectType);
                if (!Cls) {
                    callback(new UnknownTypeError(sub.objectType));
                } else {
                    obj[name] = new Cls({id: sub.id,
                                         objectType: sub.objectType});
                    callback(null);
                }
            }
        }
    );
};

ActivityObject.expandProperty = function(obj, name, callback) {
    // Easy enough!
    if (!_(obj).has(name)) {
        callback(null);
        return;
    }
    Step(
        function() {
            ActivityObject.ensureObject(obj[name], this);
        },
        function(err, sub) {
            if (err) {
                callback(err);
            } else {
                obj[name] = sub;
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.favoritedBy = function(id, callback) {
    var obj = this,
        name = "favoriters:"+obj.id;
    Step(
        function() {
            Stream.get(name, this);
        },
        function(err, stream) {
            if (err && err instanceof NoSuchThingError) {
                Stream.create({name: name}, this);
            } else if (err) {
                throw err;
            } else {
                this(null, stream);
            }
        },
        function(err, stream) {
            if (err) throw err;
            stream.deliver(id, this);
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.unfavoritedBy = function(id, callback) {
    var obj = this,
        name = "favoriters:"+obj.id;
    Step(
        function() {
            Stream.get(name, this);
        },
        function(err, stream) {
            if (err && err instanceof NoSuchThingError) {
                Stream.create({name: name}, this);
            } else if (err) {
                throw err;
            } else {
                this(null, stream);
            }
        },
        function(err, stream) {
            if (err) throw err;
            stream.remove(id, this);
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

ActivityObject.getObjectStream = function(className, streamName, start, end, callback) {

    var ids,
        Cls = ActivityObject.toClass(className);

    Step(
        function() {
            Stream.get(streamName, this);
        },
        function(err, stream) {
            if (err) throw err;
            stream.getIDs(start, end, this);
        },
        function(err, results) {
            if (err) throw err;
            ids = results;
            if (ids.length === 0) {
                callback(null, []);
            } else {
                Cls.readAll(ids, this);
            }
        },
        function(err, map) {
            var i, objects = [];
            if (err) {
                if (err instanceof NoSuchThingError) {
                    callback(null, []);
                } else {
                    callback(err, null);
                }
            } else {
                objects = new Array(ids.length);
                // Try to get it in the right order
                for (i = 0; i < ids.length; i++) {
                    objects[i] = map[ids[i]];
                }
                callback(null, objects);
            }
        }
    );
};

ActivityObject.prototype.getFavoriters = function(start, end, callback) {
    ActivityObject.getObjectStream("person", "favoriters:"+this.id, start, end, callback);
};

ActivityObject.prototype.favoritersCount = function(callback) {
    Stream.count("favoriters:"+this.id, function(err, count) {
        if (err && err instanceof NoSuchThingError) {
            callback(null, 0);
        } else if (err) {
            callback(err, null);
        } else {
            callback(null, count);
        }
    });
};

ActivityObject.prototype.expandFeeds = function(callback) {
    var obj = this;
    Step(
        function() {
            obj.repliesCount(this.parallel());
            obj.favoritersCount(this.parallel());
            obj.getReplies(0, 4, this.parallel());
        },
        function(err, repliesCount, favoritersCount, replies) {
            if (err) {
                callback(err);
            } else {
                obj.likes = {
                    totalItems: favoritersCount,
                    url: URLMaker.makeURL("api/"+obj.objectType+"/"+obj.uuid+"/likes")
                };
                obj.replies = {
                    totalItems: repliesCount,
                    url: URLMaker.makeURL("api/"+obj.objectType+"/"+obj.uuid+"/replies")
                };
                if (repliesCount > 0) {
                    obj.replies.items = replies;
                }
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.getRepliesStream = function(callback) {

    var obj = this,
        name = "activityobject:replies:"+obj.id;

    Stream.get(name, callback);
};

ActivityObject.prototype.getReplies = function(start, end, callback) {

    var obj = this,
        full = [];

    Step(
        function() {
            obj.getRepliesStream(this);
        },
        function(err, stream) {
            if (err) throw err;
            stream.getObjects(start, end, this);
        },
        function(err, compressed) {
            var i, group = this.group();
            if (err) throw err;
            for (i = 0; i < compressed.length; i++) {
                ActivityObject.ensureObject(compressed[i], group());
            }
        },
        function(err, results) {
            var i, group = this.group();
            if (err) throw err;
            full = results;
            for (i = 0; i < full.length; i++) {
                full[i].expandFeeds(group());
            }
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, full);
            }
        }
    );
};

ActivityObject.prototype.repliesCount = function(callback) {
    var name = "activityobject:replies:"+this.id;
    Stream.count(name, function(err, count) {
        if (err && err instanceof NoSuchThingError) {
            callback(null, 0);
        } else if (err) {
            callback(err, null);
        } else {
            callback(null, count);
        }
    });
};

ActivityObject.prototype.keepers = function() {
    return ["id", "objectType", "author", "published", "updated", "uuid", "inReplyTo"];
};

// Default hooks for efface()

ActivityObject.prototype.beforeEfface = function(callback) {
    callback(null);
};

ActivityObject.prototype.efface = function(callback) {

    var keepers = this.keepers(),
        prop,
        obj = this;

    Step(
        function() {
            obj.beforeEfface(this);
        },
        function(err) {
            if (err) throw err;

            for (prop in obj) {
                if (obj.hasOwnProperty(prop) && keepers.indexOf(prop) === -1) {
                    delete obj[prop];
                }
            }

            var now = Stamper.stamp();

            obj.deleted = obj.updated = now;

            obj.save(this);
        },
        function(err) {
            obj.afterEfface(this);
        },
        callback
    );
};

ActivityObject.objectTypes = ["article",
                              "audio",
                              "badge",
                              "bookmark",
                              "collection",
                              "comment",
                              "event",
                              "file",
                              "group",
                              "image",
                              "note",
                              "person",
                              "place",
                              "product",
                              "question",
                              "review",
                              "service",
                              "video"];

var objectType, i;

// Constants-like members for activity object types

for (i = 0; i < ActivityObject.objectTypes.length; i++) {
    objectType = ActivityObject.objectTypes[i];
    ActivityObject[objectType.toUpperCase().replace("-", "_")] = objectType;
}

exports.ActivityObject = ActivityObject;
exports.UnknownTypeError = UnknownTypeError;
<|endoftext|>